From 3235b196b7d36b4e088327dbd287f9373b7b9fe8 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 17 Jun 2026 12:06:54 -0700 Subject: [PATCH] YouTube enricher: observable + fast-fail proxy + live channel re-poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses 'says running but I see nothing': - Real logs: 'enriching …', 'proxy returned N', 'done — N proxy + N per-video (X/Y dated)' so you can see exactly what it's doing. - Proxy timeout 8s→4s so dead instances (most public ones) fail fast instead of hanging ~40s before the yt-dlp fallback. - Channel page now re-fetches a few times (25/60/110s) after load while videos are still undated, re-rendering only when NEW year-seasons appear — so dates the background enricher fills in pop in without a manual reload. --- core/video/youtube.py | 4 +++- core/video/youtube_enrichment.py | 8 ++++---- webui/static/video/video-detail.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/core/video/youtube.py b/core/video/youtube.py index 48860471..75c0afb8 100644 --- a/core/video/youtube.py +++ b/core/video/youtube.py @@ -309,7 +309,9 @@ def _proxy_get(url, fetch): if fetch is not None: return fetch(url) import requests - r = requests.get(url, timeout=8, headers={"User-Agent": _UA, "Accept": "application/json"}) + # Short timeout so a dead instance (most public ones are flaky) fails fast and + # we fall through to the next / to the yt-dlp fallback instead of hanging. + r = requests.get(url, timeout=4, headers={"User-Agent": _UA, "Accept": "application/json"}) return r.json() if r.status_code == 200 else None diff --git a/core/video/youtube_enrichment.py b/core/video/youtube_enrichment.py index ec07ad77..41789d48 100644 --- a/core/video/youtube_enrichment.py +++ b/core/video/youtube_enrichment.py @@ -116,15 +116,16 @@ class YoutubeDateEnricher: if not cid or db.channel_dates_enriched_recently(cid): return self._current = self._titles.get(cid) or cid + logger.info("YouTube dates: enriching %s (%s)…", self._current, cid) dates = {} try: dates = yt.proxy_channel_dates(cid) or {} except Exception: logger.info("proxy date fetch failed for %s", cid, exc_info=True) + logger.info("YouTube dates: proxy returned %d for %s", len(dates), cid) if dates: db.cache_video_dates([{"youtube_id": k, "published_at": v} for k, v in dates.items()]) - logger.info("YouTube dates: %d cached for %s (proxy)", len(dates), cid) # Fallback (proxy down): date the channel's RECENT UPLOADS via yt-dlp, not # just wished videos — so years populate fully even with no working proxy. @@ -150,12 +151,11 @@ class YoutubeDateEnricher: db.cache_video_dates([{"youtube_id": vid, "published_at": v["published_at"]}]) filled += 1 time.sleep(_FALLBACK_DELAY) - if filled: - logger.info("YouTube dates: %d cached for %s (per-video fallback)", filled, cid) - self._channels_done += 1 self._dates_total += len(dates) + filled db.mark_channel_dates_enriched(cid, len(dates) + filled) + logger.info("YouTube dates: %s done — %d proxy + %d per-video (%d/%d dated)", + cid, len(dates), filled, len(dates) + filled, len(ids)) _enricher = None diff --git a/webui/static/video/video-detail.js b/webui/static/video/video-detail.js index 52856f83..2f68a001 100644 --- a/webui/static/video/video-detail.js +++ b/webui/static/video/video-detail.js @@ -1341,11 +1341,38 @@ episode_total: (ch.videos || []).length, episode_owned: 0 }; } + // Freshly-followed channels get their upload dates filled in over the next + // minute+ by the background enricher. Re-fetch a few times so new year-seasons + // pop in WITHOUT a manual reload (only re-renders if more years appeared). + var ytRepollTimers = []; + function ytClearRepoll() { ytRepollTimers.forEach(function (t) { clearTimeout(t); }); ytRepollTimers = []; } + function ytRefetch(id) { + var before = ((data && data.seasons) || []).length; + fetch('/api/video/youtube/channel/' + encodeURIComponent(id) + '?limit=90', { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (resp) { + if (!resp || !resp.success || currentId !== id || currentSource !== 'youtube') return; + var nd = ytToShow(resp); + if (nd.seasons.length <= before) return; // no new years yet → don't disrupt the view + data = nd; + if (!seasonByNum(selectedSeason)) selectedSeason = nd.seasons.length ? nd.seasons[0].season_number : null; + renderBillboard(nd); renderSeasonNav(); renderEpisodes(); + }) + .catch(function () { /* ignore */ }); + } + function ytScheduleRepoll(id) { + ytClearRepoll(); + [25000, 60000, 110000].forEach(function (ms) { + ytRepollTimers.push(setTimeout(function () { ytRefetch(id); }, ms)); + }); + } + function loadChannel(id) { currentKind = 'show'; currentSource = 'youtube'; // render into the show container if (currentId !== id) artAttemptedFor = null; currentId = id; if (!root()) return; + ytClearRepoll(); showLoading(true); resetExtras(); showEpSyncing(false); ['[data-vd-episodes]', '[data-vd-season-nav]'].forEach(function (s) { var n = q(s); if (n) n.innerHTML = ''; }); var r0 = root(); if (r0) r0.style.removeProperty('--vd-accent-rgb'); @@ -1364,6 +1391,9 @@ var sub = document.querySelector('.video-subpage[data-video-subpage="video-show-detail"]'); if (sub) sub.scrollTop = 0; ytLoadPlaylists(id); + // If videos are still undated, the enricher is (or will be) filling + // them in — poll a few times so the years appear live. + if (data.seasons.some(function (s) { return s.season_number === 0; })) ytScheduleRepoll(id); }) .catch(function () { showLoading(false); setText('[data-vd-title]', 'Could not load channel'); }); }