Add playlists support and fix explore older videos
- New playlists router: fetch channel playlists from YouTube, index playlist videos, browse by playlist with pagination - Playlist model gets video_ids column to store ordered video list - Register playlists router in main.py with DB migration - Add Playlists tab to Channel page: grid of playlist cards, click to browse videos, index/re-index per playlist - Fix explore older videos skipping all entries without published_at; flat-playlist entries for older videos rarely include timestamp data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -353,6 +353,83 @@ def fetch_channel_metadata(channel_id: str, max_videos: int = 30, start_video: i
|
||||
return {"channel": channel_info, "videos": videos}
|
||||
|
||||
|
||||
def fetch_channel_playlists(channel_id: str, max_results: int = 100) -> list[dict]:
|
||||
"""Fetch the playlists listed on a channel's /playlists tab."""
|
||||
if channel_id.startswith("@"):
|
||||
url = f"https://www.youtube.com/{channel_id}/playlists"
|
||||
else:
|
||||
url = f"https://www.youtube.com/channel/{channel_id}/playlists"
|
||||
stdout, _, code = _run([
|
||||
"yt-dlp", url,
|
||||
"--dump-json", "--flat-playlist",
|
||||
"--playlist-end", str(max_results),
|
||||
"--quiet",
|
||||
*_cookie_args(),
|
||||
], timeout=60)
|
||||
|
||||
playlists = []
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
info = json.loads(line)
|
||||
pl_id = info.get("id") or info.get("playlist_id")
|
||||
title = info.get("title") or info.get("playlist_title") or ""
|
||||
if not pl_id or not title or pl_id == channel_id:
|
||||
continue
|
||||
playlists.append({
|
||||
"youtube_playlist_id": pl_id,
|
||||
"title": title,
|
||||
"description": info.get("description"),
|
||||
"thumbnail_url": _stable_thumbnail(info.get("id")) if info.get("_type") == "url" else None,
|
||||
"video_count": info.get("playlist_count") or info.get("n_entries") or 0,
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return playlists
|
||||
|
||||
|
||||
def fetch_playlist_videos(playlist_id: str, max_videos: int = 200) -> list[dict]:
|
||||
"""Fetch videos from a YouTube playlist by playlist ID."""
|
||||
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
||||
args = [
|
||||
"yt-dlp", url,
|
||||
"--dump-json", "--flat-playlist",
|
||||
"--quiet",
|
||||
*_cookie_args(),
|
||||
]
|
||||
if max_videos > 0:
|
||||
args += ["--playlist-end", str(max_videos)]
|
||||
stdout, _, code = _run(args, timeout=120)
|
||||
|
||||
videos = []
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
info = json.loads(line)
|
||||
vid_id = info.get("id")
|
||||
if not vid_id:
|
||||
continue
|
||||
videos.append({
|
||||
"youtube_video_id": vid_id,
|
||||
"title": info.get("title", ""),
|
||||
"thumbnail_url": _stable_thumbnail(vid_id),
|
||||
"duration_seconds": info.get("duration"),
|
||||
"published_at": _parse_published(info),
|
||||
"view_count": info.get("view_count"),
|
||||
"channel": {
|
||||
"youtube_channel_id": info.get("channel_id"),
|
||||
"name": info.get("channel") or info.get("uploader") or "",
|
||||
},
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return videos
|
||||
|
||||
|
||||
def fetch_featured_channels(channel_id: str) -> list[str]:
|
||||
"""Fetch channel IDs from the /channels tab of a YouTube channel.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user