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 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
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("@"):
url = f"https://www.youtube.com/{youtube_channel_id}/videos"
else:
@@ -750,17 +763,15 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str, channel_name:
db.close()
if not video_ids:
with _tasks_lock:
_tasks.pop(task_id, None)
return
task_id = f"popular-{channel_id}"
with _tasks_lock:
_tasks[task_id] = {
"id": task_id,
"label": f"Popular fetch — {channel_name}" if channel_name else "Popular fetch",
"total": len(video_ids),
"done": 0,
"started_at": datetime.utcnow().isoformat(),
}
if task_id in _tasks:
_tasks[task_id]["phase"] = "Enriching view counts…"
_tasks[task_id]["total"] = len(video_ids)
_tasks[task_id]["done"] = 0
results = {}
try:

View File

@@ -2,7 +2,7 @@ import { Outlet, NavLink, Link, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../hooks/useAuth";
import SearchBar from "./SearchBar";
import { getDownloads, getChannels } from "../api";
import { getDownloads, getChannels, getActiveTasks } from "../api";
function BottomNav({ newCount }) {
const tabs = [
@@ -73,7 +73,7 @@ function BottomNav({ newCount }) {
}
function DownloadIndicator() {
const { data } = useQuery({
const { data: downloads } = useQuery({
queryKey: ["downloads"],
queryFn: () => getDownloads().then((r) => r.data),
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"
);
if (!active.length) return null;
const top = active[0];
const pct = top.progress_percent ?? 0;
if (!activeDownloads.length && !tasks.length) return null;
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 (
<Link
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"
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">
<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" />
</svg>
<span className="font-mono tabular-nums text-[11px]">{pct.toFixed(0)}%</span>
{active.length > 1 && (
<span className="hidden sm:inline text-zinc-500">+{active.length - 1}</span>
{label}
{totalActive > 1 && (
<span className="hidden sm:inline text-zinc-500">+{totalActive - 1}</span>
)}
</Link>
);

View File

@@ -141,9 +141,12 @@ export default function DownloadsPage() {
const pct = task.total > 0 ? (task.done / task.total) * 100 : 0;
return (
<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">
<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>
)}
</div>
<ProgressBar pct={pct} />
</div>