diff --git a/backend/app/services/downloader.py b/backend/app/services/downloader.py index 2186a4e..90c14f1 100644 --- a/backend/app/services/downloader.py +++ b/backend/app/services/downloader.py @@ -39,17 +39,38 @@ def cleanup_task(task_id: str): def _make_hook(task_id: str): - """yt-dlp progress hook with real-time tracking and cancel support.""" + """yt-dlp progress hook: handles DASH multi-phase + HLS fragments + cancel.""" + state = {"phase": 0} # counts "finished" events (video phase, audio phase…) + PHASE_WEIGHTS = [0.80, 0.19] # phase-0 → 0-80%, phase-1 → 80-99% + def hook(d): flag = _cancel_flags.get(task_id) if flag and flag.is_set(): raise yt_dlp.utils.DownloadCancelled("Cancelled by user") + if d["status"] == "downloading": total = d.get("total_bytes") or d.get("total_bytes_estimate") or 0 - done = d.get("downloaded_bytes", 0) - _download_progress[task_id] = int(done * 100 / total) if total else 0 + done = d.get("downloaded_bytes", 0) + + if total > 0: + phase_pct = done / total # 0.0–1.0 + else: + # HLS / unknown size: use fragment index + fc = d.get("fragment_count") or 0 + fi = d.get("fragment_index") or 0 + phase_pct = (fi / fc) if fc > 0 else 0.5 # 0.5 = "working" + + ph = min(state["phase"], len(PHASE_WEIGHTS) - 1) + base = sum(PHASE_WEIGHTS[:ph]) * 100 + span = PHASE_WEIGHTS[ph] * 100 + pct = int(base + phase_pct * span) + _download_progress[task_id] = max(1, pct) # at least 1 to show activity + elif d["status"] == "finished": - _download_progress[task_id] = 99 # merging, not 100 yet + state["phase"] += 1 + done_pct = int(sum(PHASE_WEIGHTS[:state["phase"]]) * 100) + _download_progress[task_id] = min(done_pct, 99) + return hook VIDEO_BASE_PATH = os.getenv("VIDEO_BASE_PATH", "/home/xdl/xdl_videos") diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 18bc946..9c32595 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -33,12 +33,12 @@
- + + :stroke-dasharray="v.progress > 1 ? `${(v.progress / 100) * 113} 113` : '30 113'" /> - {{ v.progress }}% + {{ v.progress > 1 ? v.progress + '%' : '…' }}
@@ -529,6 +529,8 @@ onUnmounted(() => { if (pollTimer) clearInterval(pollTimer) }) gap: 0.2rem; background: rgba(0,0,0,0.72); border-radius: 6px; } .ring { width: 36px; height: 36px; transform: rotate(-90deg); } +.ring-spin { animation: spin 1.2s linear infinite; } +@keyframes spin { to { transform: rotate(270deg); } } .ring-bg { fill: none; stroke: #333; stroke-width: 4; } .ring-fill { fill: none; stroke: #1da1f2; stroke-width: 4; stroke-linecap: round; transition: stroke-dasharray 0.4s; } .ring-pct { font-size: 0.65rem; color: #7fdbff; font-weight: 600; margin-top: -2px; } diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index cabe5bb..9afdadc 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -37,7 +37,7 @@
-

{{ statusText }}

+

{{ statusText.replace('0%', '…') }}

💾 Save to device @@ -193,7 +193,14 @@ h1 { font-size: 2rem; margin-bottom: 0.5rem; } .cancel-btn:hover { background: rgba(231,76,60,0.1); } .task-status { margin-top: 1.5rem; } .progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin-bottom: 0.5rem; } -.progress-fill { height: 100%; background: #1da1f2; transition: width 0.3s; } +.progress-fill { height: 100%; background: #1da1f2; transition: width 0.5s; } +.progress-bar:has(.progress-fill[style="width: 0%"]), +.progress-bar:has(.progress-fill[style="width: 1%"]) { + background: linear-gradient(90deg, #1a1a2e 25%, #1da1f2 50%, #1a1a2e 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} +@keyframes shimmer { to { background-position: -200% 0; } } .download-link { display: inline-block; margin-top: 0.5rem; padding: 0.8rem 2rem; background: #27ae60; color: #fff; border-radius: 8px; text-decoration: none; font-weight: 600;