fix: DASH multi-phase progress + HLS fragment progress + indeterminate animation
This commit is contained in:
@@ -39,17 +39,38 @@ def cleanup_task(task_id: str):
|
|||||||
|
|
||||||
|
|
||||||
def _make_hook(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):
|
def hook(d):
|
||||||
flag = _cancel_flags.get(task_id)
|
flag = _cancel_flags.get(task_id)
|
||||||
if flag and flag.is_set():
|
if flag and flag.is_set():
|
||||||
raise yt_dlp.utils.DownloadCancelled("Cancelled by user")
|
raise yt_dlp.utils.DownloadCancelled("Cancelled by user")
|
||||||
|
|
||||||
if d["status"] == "downloading":
|
if d["status"] == "downloading":
|
||||||
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 0
|
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 0
|
||||||
done = d.get("downloaded_bytes", 0)
|
done = d.get("downloaded_bytes", 0)
|
||||||
_download_progress[task_id] = int(done * 100 / total) if total else 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":
|
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
|
return hook
|
||||||
|
|
||||||
VIDEO_BASE_PATH = os.getenv("VIDEO_BASE_PATH", "/home/xdl/xdl_videos")
|
VIDEO_BASE_PATH = os.getenv("VIDEO_BASE_PATH", "/home/xdl/xdl_videos")
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
|
|
||||||
<!-- Downloading: circular progress ring + cancel X -->
|
<!-- Downloading: circular progress ring + cancel X -->
|
||||||
<div v-if="v.status === 'downloading'" class="thumb-overlay ring-overlay">
|
<div v-if="v.status === 'downloading'" class="thumb-overlay ring-overlay">
|
||||||
<svg class="ring" viewBox="0 0 44 44">
|
<svg class="ring" :class="{ 'ring-spin': v.progress <= 1 }" viewBox="0 0 44 44">
|
||||||
<circle class="ring-bg" cx="22" cy="22" r="18" />
|
<circle class="ring-bg" cx="22" cy="22" r="18" />
|
||||||
<circle class="ring-fill" cx="22" cy="22" r="18"
|
<circle class="ring-fill" cx="22" cy="22" r="18"
|
||||||
:stroke-dasharray="`${(v.progress / 100) * 113} 113`" />
|
:stroke-dasharray="v.progress > 1 ? `${(v.progress / 100) * 113} 113` : '30 113'" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ring-pct">{{ v.progress }}%</span>
|
<span class="ring-pct">{{ v.progress > 1 ? v.progress + '%' : '…' }}</span>
|
||||||
<button class="ring-cancel" @click.stop="cancelDownload(v)" title="Cancel">✕</button>
|
<button class="ring-cancel" @click.stop="cancelDownload(v)" title="Cancel">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -529,6 +529,8 @@ onUnmounted(() => { if (pollTimer) clearInterval(pollTimer) })
|
|||||||
gap: 0.2rem; background: rgba(0,0,0,0.72); border-radius: 6px;
|
gap: 0.2rem; background: rgba(0,0,0,0.72); border-radius: 6px;
|
||||||
}
|
}
|
||||||
.ring { width: 36px; height: 36px; transform: rotate(-90deg); }
|
.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-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-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; }
|
.ring-pct { font-size: 0.65rem; color: #7fdbff; font-weight: 600; margin-top: -2px; }
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ statusText }}</p>
|
<p>{{ statusText.replace('0%', '…') }}</p>
|
||||||
<a v-if="downloadReady" :href="downloadUrl" class="download-link" download>💾 Save to device</a>
|
<a v-if="downloadReady" :href="downloadUrl" class="download-link" download>💾 Save to device</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,14 @@ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|||||||
.cancel-btn:hover { background: rgba(231,76,60,0.1); }
|
.cancel-btn:hover { background: rgba(231,76,60,0.1); }
|
||||||
.task-status { margin-top: 1.5rem; }
|
.task-status { margin-top: 1.5rem; }
|
||||||
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin-bottom: 0.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 {
|
.download-link {
|
||||||
display: inline-block; margin-top: 0.5rem; padding: 0.8rem 2rem;
|
display: inline-block; margin-top: 0.5rem; padding: 0.8rem 2rem;
|
||||||
background: #27ae60; color: #fff; border-radius: 8px; text-decoration: none; font-weight: 600;
|
background: #27ae60; color: #fff; border-radius: 8px; text-decoration: none; font-weight: 600;
|
||||||
|
|||||||
Reference in New Issue
Block a user