Show popular fetch phases in nav indicator and Downloads page

- Phase 1 (crawling) now creates the task immediately so Downloads shows it
- Phase label updates to 'Enriching view counts' when phase 2 starts
- Nav bar DownloadIndicator also polls /channels/tasks and shows spinning
  indicator + progress % for background tasks (not just file downloads)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 23:32:39 +02:00
parent be84660e2d
commit 3a557a1d24
3 changed files with 55 additions and 20 deletions

View File

@@ -680,7 +680,20 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str, channel_name:
from ..database import SessionLocal from ..database import SessionLocal
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
task_id = f"popular-{channel_id}"
label = f"Popular fetch — {channel_name}" if channel_name else "Popular fetch"
# Phase 1 — flat-playlist: crawl all channel videos quickly # Phase 1 — flat-playlist: crawl all channel videos quickly
with _tasks_lock:
_tasks[task_id] = {
"id": task_id,
"label": label,
"phase": "Crawling channel…",
"total": 0,
"done": 0,
"started_at": datetime.utcnow().isoformat(),
}
if youtube_channel_id.startswith("@"): if youtube_channel_id.startswith("@"):
url = f"https://www.youtube.com/{youtube_channel_id}/videos" url = f"https://www.youtube.com/{youtube_channel_id}/videos"
else: else:
@@ -750,17 +763,15 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str, channel_name:
db.close() db.close()
if not video_ids: if not video_ids:
with _tasks_lock:
_tasks.pop(task_id, None)
return return
task_id = f"popular-{channel_id}"
with _tasks_lock: with _tasks_lock:
_tasks[task_id] = { if task_id in _tasks:
"id": task_id, _tasks[task_id]["phase"] = "Enriching view counts…"
"label": f"Popular fetch — {channel_name}" if channel_name else "Popular fetch", _tasks[task_id]["total"] = len(video_ids)
"total": len(video_ids), _tasks[task_id]["done"] = 0
"done": 0,
"started_at": datetime.utcnow().isoformat(),
}
results = {} results = {}
try: try:

View File

@@ -2,7 +2,7 @@ import { Outlet, NavLink, Link, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { getDownloads, getChannels } from "../api"; import { getDownloads, getChannels, getActiveTasks } from "../api";
function BottomNav({ newCount }) { function BottomNav({ newCount }) {
const tabs = [ const tabs = [
@@ -73,7 +73,7 @@ function BottomNav({ newCount }) {
} }
function DownloadIndicator() { function DownloadIndicator() {
const { data } = useQuery({ const { data: downloads } = useQuery({
queryKey: ["downloads"], queryKey: ["downloads"],
queryFn: () => getDownloads().then((r) => r.data), queryFn: () => getDownloads().then((r) => r.data),
refetchInterval: (query) => { refetchInterval: (query) => {
@@ -84,27 +84,48 @@ function DownloadIndicator() {
}, },
}); });
const active = (data ?? []).filter( const { data: tasks = [] } = useQuery({
queryKey: ["active-tasks"],
queryFn: () => getActiveTasks().then((r) => r.data),
refetchInterval: (query) => (query.state.data?.length > 0 ? 2000 : 10_000),
});
const activeDownloads = (downloads ?? []).filter(
(d) => d.status === "pending" || d.status === "downloading" (d) => d.status === "pending" || d.status === "downloading"
); );
if (!active.length) return null;
const top = active[0]; if (!activeDownloads.length && !tasks.length) return null;
const pct = top.progress_percent ?? 0;
const totalActive = activeDownloads.length + tasks.length;
// Show download progress if there are active downloads, otherwise show task phase
let label;
if (activeDownloads.length) {
const pct = activeDownloads[0].progress_percent ?? 0;
label = <span className="font-mono tabular-nums text-[11px]">{pct.toFixed(0)}%</span>;
} else {
const task = tasks[0];
const pct = task.total > 0 ? Math.round((task.done / task.total) * 100) : null;
label = (
<span className="text-[11px] max-w-[80px] truncate hidden sm:inline">
{pct !== null ? `${pct}%` : task.phase || "…"}
</span>
);
}
return ( return (
<Link <Link
to="/downloads" to="/downloads"
className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors text-xs text-zinc-300 shrink-0" className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-zinc-800 hover:bg-zinc-700 transition-colors text-xs text-zinc-300 shrink-0"
title={`${active.length} download${active.length > 1 ? "s" : ""} in progress`} title={`${totalActive} task${totalActive > 1 ? "s" : ""} in progress`}
> >
<svg className="w-3 h-3 animate-spin text-zinc-400 shrink-0" fill="none" viewBox="0 0 24 24"> <svg className="w-3 h-3 animate-spin text-zinc-400 shrink-0" fill="none" viewBox="0 0 24 24">
<circle className="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" /> <circle className="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
<path className="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" /> <path className="opacity-80" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
</svg> </svg>
<span className="font-mono tabular-nums text-[11px]">{pct.toFixed(0)}%</span> {label}
{active.length > 1 && ( {totalActive > 1 && (
<span className="hidden sm:inline text-zinc-500">+{active.length - 1}</span> <span className="hidden sm:inline text-zinc-500">+{totalActive - 1}</span>
)} )}
</Link> </Link>
); );

View File

@@ -141,9 +141,12 @@ export default function DownloadsPage() {
const pct = task.total > 0 ? (task.done / task.total) * 100 : 0; const pct = task.total > 0 ? (task.done / task.total) * 100 : 0;
return ( return (
<div key={task.id} className="bg-zinc-900 rounded-xl p-4"> <div key={task.id} className="bg-zinc-900 rounded-xl p-4">
<p className="text-sm font-medium text-zinc-100 mb-0.5">{task.label}</p>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="text-sm font-medium text-zinc-100">{task.label}</p> <span className="text-xs text-zinc-400">{task.phase || "Running…"}</span>
{task.total > 0 && (
<span className="text-xs text-zinc-400 tabular-nums">{task.done}/{task.total}</span> <span className="text-xs text-zinc-400 tabular-nums">{task.done}/{task.total}</span>
)}
</div> </div>
<ProgressBar pct={pct} /> <ProgressBar pct={pct} />
</div> </div>