From 67197ce9569e5f824e1e9e68c56f592d16ee9a22 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 17 Jun 2026 22:07:50 -0700 Subject: [PATCH] Playlists: page the FULL list when YouTube cookies are set (like ytdl-sub) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correcting my earlier 'YouTube caps at ~200' claim — that's the ANONYMOUS ceiling. YouTube throttles unauthenticated playlist extraction to ~100 (yt-dlp) / ~200 (InnerTube); ytdl-sub gets everything because it runs with browser cookies. Our code already wires cookiesfrombrowser via youtube.cookies_browser (same as the music client) — it's just unset. The detail endpoint now resolves with a high limit so a cookie-authenticated yt-dlp pages the WHOLE playlist, and uses whichever source (yt-dlp vs InnerTube) returned more, with the true header count. The partial note now tells you to add cookies in Settings to load the full list. --- api/video/youtube.py | 15 +++++++++------ webui/static/video/video-detail.js | 5 +++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/api/video/youtube.py b/api/video/youtube.py index 7bbbda2a..fe0b8e71 100644 --- a/api/video/youtube.py +++ b/api/video/youtube.py @@ -378,17 +378,20 @@ def register_routes(bp): limit = 200 try: db = get_video_db() - pl = yt.resolve_playlist("https://www.youtube.com/playlist?list=" + playlist_id, limit=60) + # Pull the WHOLE playlist. With cookies configured (youtube.cookies_browser, + # like ytdl-sub) yt-dlp pages the full list; anonymous, YouTube throttles + # us to ~100, so the InnerTube catalog (~200 + the true header count) is the + # better source. Use whichever got more, with the true total. + pl = yt.resolve_playlist("https://www.youtube.com/playlist?list=" + playlist_id, limit=5000) if not pl: return jsonify({"success": False, "error": "Playlist not found"}), 404 - # yt-dlp flat caps playlists at ~100 and its count is often unset. Overlay - # the InnerTube catalog (curator order, up to ~200) + the TRUE header count. try: cat = yt.innertube_playlist_catalog(playlist_id) - if cat.get("videos"): + if len(cat.get("videos") or []) > len(pl.get("videos") or []): pl["videos"] = cat["videos"] - if cat.get("total"): - pl["video_count"] = cat["total"] + total = max(pl.get("video_count") or 0, cat.get("total") or 0) + if total: + pl["video_count"] = total except Exception: pass vids = pl.get("videos") or [] diff --git a/webui/static/video/video-detail.js b/webui/static/video/video-detail.js index 97a676b1..6fb344b6 100644 --- a/webui/static/video/video-detail.js +++ b/webui/static/video/video-detail.js @@ -1571,9 +1571,10 @@ ytVideoMap = {}; vids.forEach(function (v) { ytVideoMap[v.youtube_id] = v; }); var total = pl.video_count || vids.length; - // YouTube only exposes ~200 of a playlist via browsing — be honest when partial. + // Anonymous, YouTube throttles us to ~100-200; with YouTube cookies set in + // Settings, yt-dlp pages the whole playlist (like ytdl-sub). Be honest when partial. var note = total > vids.length - ? 'Showing the first ' + vids.length + ' of ' + total + ' videos (YouTube limits playlist browsing).' : ''; + ? 'Showing ' + vids.length + ' of ' + total + ' videos — add YouTube cookies in Settings to load the full playlist.' : ''; var season = { season_number: 1, title: 'Videos', poster_url: ytProx(pl.thumbnail_url), episode_owned: 0, episode_total: vids.length, episodes: vids.map(ytEpisodeOf) }; return { kind: 'playlist', source: 'youtube', id: pl.playlist_id, title: pl.title || 'Playlist',