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:
2025-11-26 15:36:26 -05:00
parent 067926e80d
commit acb2ec0654
10 changed files with 296 additions and 80 deletions

60
main.py
View File

@@ -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