Implement UI improvements and download management features
- Switch to light mode with black and white color scheme - Simplify channel subscription to use channel ID only instead of RSS URL - Add Downloads page to track all video download jobs - Fix Flask-Login session management bug in user loader - Always filter YouTube Shorts from feeds (case-insensitive) - Fix download service video URL attribute error - Fix watch page enum comparison for download status display UI Changes: - Update CSS to pure black/white/grayscale theme - Remove colored text and buttons - Use underlines for hover states instead of color changes - Improve visual hierarchy with grayscale shades Channel Subscription: - Accept channel ID directly instead of full RSS URL - Add validation for channel ID format (UC/UU prefix) - Update help text and examples for easier onboarding Downloads Page: - New route at /downloads showing all video download jobs - Display status, progress, and metadata for each download - Sortable by status (downloading, pending, failed, completed) - Actions to download, retry, or watch videos - Responsive grid layout with thumbnails Bug Fixes: - Fix user loader to properly use database session context manager - Fix download service accessing wrong attribute (link → video_url) - Fix watch page template enum value comparisons - Fix session detachment issues when accessing channel data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
60
main.py
60
main.py
@@ -24,15 +24,12 @@ login_manager.login_message_category = "info"
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""Load user by ID for Flask-Login."""
|
||||
session = next(get_db_session())
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
user = session.query(User).get(int(user_id))
|
||||
if user:
|
||||
# Expire the user from this session so it can be used in request context
|
||||
session.expunge(user)
|
||||
return user
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Default channel ID for demonstration
|
||||
DEFAULT_CHANNEL_ID = "UCtTWOND3uyl4tVc_FarDmpw"
|
||||
@@ -184,6 +181,32 @@ def channels_page():
|
||||
return render_template("channels.html", channels=[])
|
||||
|
||||
|
||||
@app.route("/downloads", methods=["GET"])
|
||||
@login_required
|
||||
def downloads_page():
|
||||
"""Render the downloads page showing all download jobs."""
|
||||
try:
|
||||
with get_db_session() as session:
|
||||
# Get all videos for current user's channels with download activity
|
||||
# Order by: downloading first, then pending, then failed, then completed
|
||||
# Within each status, order by download_started_at or created_at
|
||||
videos = session.query(VideoEntry).join(Channel).filter(
|
||||
Channel.user_id == current_user.id
|
||||
).order_by(
|
||||
# Custom sort: downloading=0, pending=1, failed=2, completed=3
|
||||
desc(VideoEntry.download_status == DownloadStatus.DOWNLOADING),
|
||||
desc(VideoEntry.download_status == DownloadStatus.PENDING),
|
||||
desc(VideoEntry.download_status == DownloadStatus.FAILED),
|
||||
desc(VideoEntry.download_started_at),
|
||||
desc(VideoEntry.created_at)
|
||||
).all()
|
||||
|
||||
return render_template("downloads.html", videos=videos)
|
||||
except Exception as e:
|
||||
flash(f"Error loading downloads: {str(e)}", "error")
|
||||
return render_template("downloads.html", videos=[])
|
||||
|
||||
|
||||
@app.route("/add-channel", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def add_channel_page():
|
||||
@@ -192,34 +215,33 @@ def add_channel_page():
|
||||
return render_template("add_channel.html")
|
||||
|
||||
# Handle POST - add new channel
|
||||
rss_url = request.form.get("rss_url")
|
||||
if not rss_url:
|
||||
flash("RSS URL is required", "error")
|
||||
channel_id = request.form.get("channel_id")
|
||||
if not channel_id:
|
||||
flash("Channel ID is required", "error")
|
||||
return redirect(url_for("add_channel_page"))
|
||||
|
||||
# Extract channel_id from RSS URL
|
||||
# Format: https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
|
||||
try:
|
||||
if "channel_id=" in rss_url:
|
||||
channel_id = rss_url.split("channel_id=")[1].split("&")[0]
|
||||
else:
|
||||
flash("Invalid RSS URL format. Must contain channel_id parameter.", "error")
|
||||
return redirect(url_for("add_channel_page"))
|
||||
# Validate channel ID format (should start with UC or UU)
|
||||
channel_id = channel_id.strip()
|
||||
if not channel_id.startswith(("UC", "UU")):
|
||||
flash("Invalid channel ID format. Channel IDs should start with UC or UU.", "error")
|
||||
return redirect(url_for("add_channel_page"))
|
||||
|
||||
try:
|
||||
# Fetch feed using parser
|
||||
parser = YouTubeFeedParser(channel_id)
|
||||
result = parser.fetch_feed()
|
||||
|
||||
if result is None:
|
||||
flash("Failed to fetch feed from YouTube", "error")
|
||||
flash("Failed to fetch feed from YouTube. Please check the channel ID.", "error")
|
||||
return redirect(url_for("add_channel_page"))
|
||||
|
||||
# Save to database with current user
|
||||
with get_db_session() as session:
|
||||
channel = parser.save_to_db(session, result, current_user.id)
|
||||
channel_title = channel.title
|
||||
video_count = len(result["entries"])
|
||||
|
||||
flash(f"Successfully subscribed to {channel.title}! Added {video_count} videos.", "success")
|
||||
flash(f"Successfully subscribed to {channel_title}! Added {video_count} videos.", "success")
|
||||
return redirect(url_for("channels_page"))
|
||||
|
||||
except Exception as e:
|
||||
@@ -255,18 +277,16 @@ def get_feed():
|
||||
|
||||
Query parameters:
|
||||
channel_id: YouTube channel ID (optional, uses default if not provided)
|
||||
filter_shorts: Whether to filter out Shorts (default: true)
|
||||
save: Whether to save to database (default: true)
|
||||
|
||||
Returns:
|
||||
JSON response with feed data or error message
|
||||
"""
|
||||
channel_id = request.args.get("channel_id", DEFAULT_CHANNEL_ID)
|
||||
filter_shorts = request.args.get("filter_shorts", "true").lower() == "true"
|
||||
save_to_db = request.args.get("save", "true").lower() == "true"
|
||||
|
||||
parser = YouTubeFeedParser(channel_id)
|
||||
result = parser.fetch_feed(filter_shorts=filter_shorts)
|
||||
result = parser.fetch_feed(filter_shorts=True)
|
||||
|
||||
if result is None:
|
||||
return jsonify({"error": "Failed to fetch feed"}), 500
|
||||
|
||||
Reference in New Issue
Block a user