YouTube enricher: coverage-aware retry + show in Manage Workers modal
- 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.
This commit is contained in:
parent
9524a40615
commit
581735ac7d
3 changed files with 55 additions and 8 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
'<span class="em-rail-cov"><span class="em-rail-cov-fill" style="width:' + pct + '%"></span></span>';
|
||||
var icon = LOGOS[w.id]
|
||||
? '<img class="vem-logo vem-logo--' + w.id + '" src="' + LOGOS[w.id] + '" alt="">'
|
||||
: '<span class="vem-glyph" style="color:' + w.color + '">★</span>';
|
||||
: '<span class="vem-glyph" style="color:' + w.color + '">' + (w.glyph || '★') + '</span>';
|
||||
return '<button class="em-worker-row" data-em-select="' + w.id + '" style="--row-accent: ' + w.rgb + '">' +
|
||||
'<span class="em-worker-icon">' + icon + '</span>' +
|
||||
'<span class="em-worker-meta"><span class="em-worker-name">' + esc(w.name) + '</span>' +
|
||||
'<span class="em-worker-sub">' + esc(railSub(s)) + '</span>' + cov + '</span>' +
|
||||
'<span class="em-worker-sub">' + esc(railSub(s, w.id)) + '</span>' + cov + '</span>' +
|
||||
'<span class="em-dot em-dot--' + info.cls + '" title="' + info.label + '"></span></button>';
|
||||
}).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)
|
||||
? '<span class="em-ph-current">channel: ' + esc(s.current_item.name) + '</span>' : '';
|
||||
panel.innerHTML =
|
||||
'<div class="em-panel-header">' +
|
||||
'<div class="em-ph-main"><span class="em-dot em-dot--' + info.cls + '"></span>' +
|
||||
'<strong>YouTube Dates</strong><span class="em-ph-status">' + info.label + '</span>' + current + '</div>' +
|
||||
'<button class="em-pause-btn" data-em-pause>' + (s.paused ? '▶ Resume' : '⏸ Pause') + '</button></div>' +
|
||||
'<div class="vem-yt-about">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.</div>' +
|
||||
'<div class="vem-yt-stats">' +
|
||||
'<div class="vem-yt-stat"><span class="vem-yt-num">' + (prog.matched || 0) + '</span><span class="vem-yt-lbl">channels enriched</span></div>' +
|
||||
'<div class="vem-yt-stat"><span class="vem-yt-num">' + (s.dates_cached || 0) + '</span><span class="vem-yt-lbl">dates cached</span></div>' +
|
||||
'<div class="vem-yt-stat"><span class="vem-yt-num">' + (s.queued || 0) + '</span><span class="vem-yt-lbl">queued</span></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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 =
|
||||
'<div class="em-panel-header" id="vem-panel-header"></div>' +
|
||||
'<div class="em-section-label em-section-label--row"><span>Coverage</span>' +
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue