From 581735ac7d60611c8bae6ba0d1e43842bd672457 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 17 Jun 2026 11:37:26 -0700 Subject: [PATCH] YouTube enricher: coverage-aware retry + show in Manage Workers modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes 'stuck idle': a channel marked enriched with FEW dates (proxies were down) was locked out for 24h, so the improved yt-dlp fallback never re-ran. channel_dates_enriched_recently now retries in 15 min when the last run got <15 dates, and skips 24h only after a good run. So the catalog actually fills in once a source works. - The YouTube Dates worker now appears in the Manage Workers modal: added to WORKERS (red ▶), youtube-aware rail subtitle, and a dedicated simple panel (status + what-it-does + channels-enriched / dates-cached / queued counts) — no per-kind match queue. Pause/resume works; polls every 3s like the rail. --- database/video_database.py | 14 +++++-- .../static/video/video-enrichment-manager.js | 40 +++++++++++++++++-- webui/static/video/video-side.css | 9 +++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/database/video_database.py b/database/video_database.py index b622b253..c648e01e 100644 --- a/database/video_database.py +++ b/database/video_database.py @@ -2055,13 +2055,17 @@ class VideoDatabase: conn.close() def channel_dates_enriched_recently(self, channel_id, within_hours=24) -> bool: - """True if the channel was date-enriched within the window (don't re-sweep).""" + """True if the channel was date-enriched within the window (don't re-sweep). + Coverage-aware: a run that produced FEW dates (proxies were down) retries + soon instead of being locked out for the full window — so the catalog + actually fills in once a source works.""" if not channel_id: return False conn = self._get_connection() try: - row = conn.execute("SELECT enriched_at FROM youtube_channel_enrichment WHERE channel_id=?", - (str(channel_id),)).fetchone() + row = conn.execute( + "SELECT enriched_at, date_count FROM youtube_channel_enrichment WHERE channel_id=?", + (str(channel_id),)).fetchone() if not row or not row["enriched_at"]: return False try: @@ -2069,7 +2073,9 @@ class VideoDatabase: except (ValueError, TypeError): return False now = datetime.now(timezone.utc).replace(tzinfo=None) # naive UTC, matches CURRENT_TIMESTAMP - return (now - when) < timedelta(hours=within_hours) + # Good coverage → skip for the full window; thin result → retry in 15 min. + window = within_hours if (row["date_count"] or 0) >= 15 else 0.25 + return (now - when) < timedelta(hours=window) finally: conn.close() diff --git a/webui/static/video/video-enrichment-manager.js b/webui/static/video/video-enrichment-manager.js index cb48df24..1dabf288 100644 --- a/webui/static/video/video-enrichment-manager.js +++ b/webui/static/video/video-enrichment-manager.js @@ -18,6 +18,8 @@ { id: 'tmdb', name: 'TMDB', color: '#38bdf8', rgb: '56, 189, 248', kinds: ['movie', 'show'] }, { id: 'tvdb', name: 'TVDB', color: '#a855f7', rgb: '168, 85, 247', kinds: ['show'] }, { id: 'omdb', name: 'OMDb', color: '#f5c518', rgb: '245, 197, 24', kinds: ['movie', 'show'] }, + // The YouTube date enricher — no per-kind match queue; its own simple panel. + { id: 'youtube', name: 'YouTube Dates', color: '#ff3b3b', rgb: '255, 59, 59', kinds: [], glyph: '▶' }, ]; function workerDef(id) { @@ -64,8 +66,13 @@ } return t ? Math.round(m / t * 100) : 0; } - function railSub(s) { + function railSub(s, id) { if (!s || !s.enabled) return 'Not configured'; + if (id === 'youtube') { + if (s.running && s.current_item && s.current_item.name) return s.current_item.name; + if (s.queued) return s.queued + ' queued'; + return (s.dates_cached || 0) + ' dates cached'; + } if (s.idle) return 'All matched'; if (s.running && !s.paused && s.current_item && s.current_item.name) return s.current_item.name; return (s.stats ? (s.stats.pending || 0) : 0) + ' pending'; @@ -119,11 +126,11 @@ ''; var icon = LOGOS[w.id] ? '' - : ''; + : '' + (w.glyph || '★') + ''; return ''; }).join(''); WORKERS.forEach(function (w) { @@ -132,6 +139,25 @@ }); } + function renderYoutubePanel(panel) { + var s = state.statuses.youtube || {}; + var info = statusInfo(s); + var prog = (s.progress && s.progress.channels) || {}; + var current = (s.current_item && s.current_item.name) + ? 'channel: ' + esc(s.current_item.name) + '' : ''; + panel.innerHTML = + '
' + + '
' + + 'YouTube Dates' + info.label + '' + current + '
' + + '
' + + '
Fetches real upload dates for the YouTube channels you follow, so the channel page can group videos into year-seasons. Runs in the background when you follow or open a channel — bulk via a no-key proxy, falling back to per-video; cached so it\'s a one-time pass per channel.
' + + '
' + + '
' + (prog.matched || 0) + 'channels enriched
' + + '
' + (s.dates_cached || 0) + 'dates cached
' + + '
' + (s.queued || 0) + 'queued
' + + '
'; + } + function renderPanel() { var panel = byId('vem-panel'); if (!panel) return; @@ -139,6 +165,7 @@ var w = WORKERS.find(function (x) { return x.id === state.selected; }) || WORKERS[0]; panel.style.setProperty('--em-accent', w.color); panel.style.setProperty('--em-accent-rgb', w.rgb); + if (state.selected === 'youtube') { renderYoutubePanel(panel); return; } panel.innerHTML = '
' + '
Coverage' + @@ -276,6 +303,7 @@ state.selected = id; state.breakdown = null; state.unmatched = null; state.kind = defaultKind(id); state.page = 0; state.search = ''; renderRail(); renderPanel(); + if (id === 'youtube') return; // date worker has no breakdown/unmatched Promise.all([loadBreakdown(id), loadUnmatched()]).then(function () { renderPanel(); }); } function switchKind(kind) { @@ -416,7 +444,11 @@ state.pollTimer = setInterval(function () { if (!state.open) return; getJSON('/api/video/enrichment/' + state.selected + '/status').then(function (d) { - if (d) { state.statuses[state.selected] = d; renderHeader(); renderRail(); } + if (d) { + state.statuses[state.selected] = d; + if (state.selected === 'youtube') renderPanel(); else renderHeader(); + renderRail(); + } }); }, 3000); } diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index c479acd3..230c5dde 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -2977,3 +2977,12 @@ body[data-side="video"] #soulsync-toggle { display: none; } .vyt-skel-line { height: 11px; border-radius: 5px; width: 78px; } .vyt-skel-line--sm { height: 9px; width: 52px; } @media (prefers-reduced-motion: reduce) { .vyt-skel, .vsr-yt-loading::after { animation: none; } } + +/* YouTube Dates worker — simple panel in the Manage Workers modal (no match queue) */ +.vem-yt-about { margin: 4px 2px 20px; font-size: 13px; line-height: 1.6; color: rgba(255,255,255,0.6); } +.vem-yt-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.vem-yt-stat { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.07); border-radius: 12px; + padding: 18px 14px; text-align: center; } +.vem-yt-num { display: block; font-size: 28px; font-weight: 900; color: #fff; line-height: 1; } +.vem-yt-lbl { display: block; margin-top: 7px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; + color: rgba(255,255,255,0.45); }