Add queue-based gradual discovery with shuffled call ordering and progress UI
Each yt-dlp call is now an independent task (one search query, one trending fetch, one graph channel fetch). Tasks are shuffled together so we don't fire 10 searches in a row, then enqueued with 30-90s random gaps between them — a full sweep of ~17 tasks completes in roughly 10-25 minutes instead of hammering YouTube with 21 calls back-to-back. Fast signals (community, category clusters) still run synchronously at schedule time since they're pure SQL. Progress is tracked per-user (total/done/running) and exposed on GET /api/discovery/status. The Discovery page polls every 10s while running and shows a progress bar + "Finding channels… X / Y" in the header. The auto-discovery daemon skips scheduling if a manual sweep is already running. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -217,7 +217,8 @@ export default function DiscoveryPage() {
|
||||
const { data: discStatus } = useQuery({
|
||||
queryKey: ["discovery-status"],
|
||||
queryFn: () => getDiscoveryStatus().then(r => r.data),
|
||||
staleTime: 60_000,
|
||||
staleTime: 10_000,
|
||||
refetchInterval: (query) => query.state.data?.progress?.running ? 10_000 : 60_000,
|
||||
});
|
||||
|
||||
const refreshMut = useMutation({
|
||||
@@ -247,37 +248,52 @@ export default function DiscoveryPage() {
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="font-display font-bold text-2xl text-zinc-100">Discover</h1>
|
||||
{discStatus && (
|
||||
<p className="text-xs text-zinc-500 mt-0.5">
|
||||
{discStatus.pending_count > 0
|
||||
? `${discStatus.pending_count} channel${discStatus.pending_count !== 1 ? "s" : ""} queued`
|
||||
: "Queue empty"}
|
||||
{discStatus.progress?.running
|
||||
? `Finding channels… ${discStatus.progress.done} / ${discStatus.progress.total}`
|
||||
: discStatus.pending_count > 0
|
||||
? `${discStatus.pending_count} channel${discStatus.pending_count !== 1 ? "s" : ""} queued`
|
||||
: "Queue empty"}
|
||||
{discStatus.last_run
|
||||
? ` · last refreshed ${new Date(discStatus.last_run + "Z").toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`
|
||||
: " · never refreshed"}
|
||||
</p>
|
||||
)}
|
||||
{discStatus?.progress?.running && (
|
||||
<div className="mt-1.5 h-1 w-48 rounded-full bg-zinc-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.round((discStatus.progress.done / discStatus.progress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => refreshMut.mutate()}
|
||||
disabled={refreshMut.isPending || refreshMut.isSuccess}
|
||||
disabled={refreshMut.isPending || discStatus?.progress?.running}
|
||||
className="shrink-0 flex items-center gap-2 text-sm font-medium px-4 py-2 bg-zinc-800 text-zinc-300 rounded-lg hover:bg-zinc-700 transition-colors disabled:opacity-60"
|
||||
>
|
||||
{refreshMut.isPending && (
|
||||
{discStatus?.progress?.running ? (
|
||||
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||
</svg>
|
||||
)}
|
||||
{refreshMut.isSuccess ? "Queued ✓" : refreshMut.isPending ? "Queueing…" : "Find more"}
|
||||
) : refreshMut.isPending ? (
|
||||
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||||
</svg>
|
||||
) : null}
|
||||
{discStatus?.progress?.running ? "Running…" : refreshMut.isSuccess ? "Queued ✓" : refreshMut.isPending ? "Queueing…" : "Find more"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{refreshMut.isSuccess && (
|
||||
{(refreshMut.isSuccess || discStatus?.progress?.running) && (
|
||||
<div className="px-4 py-3 rounded-xl bg-zinc-800/60 border border-zinc-700/50 text-sm text-zinc-400">
|
||||
Discovery is running in the background — it searches YouTube using your tags and interests and takes a few minutes. New channels will appear when it finishes. It also runs automatically every day.
|
||||
Discovery is running in the background — searches and channel fetches are spaced out over ~20 minutes to avoid hitting limits. New channels appear as each batch completes. Runs automatically every day.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user