YouTube enricher: observable + fast-fail proxy + live channel re-poll

Addresses 'says running but I see nothing':
- Real logs: 'enriching <ch>…', '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.
This commit is contained in:
BoulderBadgeDad 2026-06-17 12:06:54 -07:00
parent 3a24fc24b1
commit 3235b196b7
3 changed files with 37 additions and 5 deletions

View file

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

View file

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

View file

@ -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'); });
}