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:
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user