feat: show live SHA-256 verification progress and surface mismatches in UI for aria2 downloads

This commit is contained in:
Dariusz L
2026-11-01 16:06:59 +01:00
parent 22f454b67f
commit d0d706f1cf
7 changed files with 442 additions and 48 deletions
+6
View File
@@ -230,6 +230,12 @@ context._run_huggingface_xet_transfer = _bind(
)
context.download_file_with_aria2 = _bind(aria2_backend.download_file_with_aria2)
context.verify_download_sha256_with_progress = _bind(
aria2_backend.verify_download_sha256_with_progress
)
context._verify_download_sha256_with_progress = (
context.verify_download_sha256_with_progress
)
context.start_background_sha256_verification = _bind(
aria2_backend.start_background_sha256_verification
)
+242 -30
View File
@@ -858,6 +858,7 @@ def download_file_with_aria2(
metadata: Optional[Dict[str, Any]] = None,
category: str = "",
*,
expected_sha256: Optional[str] = None,
dependencies: Any = None,
) -> Dict[str, Any]:
"""Download a file with an aria2c JSON-RPC process."""
@@ -872,7 +873,9 @@ def download_file_with_aria2(
}
start_time = facade.time.time()
filename = facade.get_filename_from_path(dest_path)
expected_sha256 = facade._extract_expected_sha256(metadata)
expected_sha256 = (
expected_sha256 or facade._extract_expected_sha256(metadata)
)
max_url_recovery = getattr(
facade,
"MAX_ARIA2_URL_RECOVERY_ATTEMPTS",
@@ -1144,6 +1147,126 @@ def download_file_with_aria2(
if facade.os.path.exists(completed_path)
else downloaded
)
clean_expected = str(expected_sha256 or "").lower().strip()
if clean_expected and facade.os.path.isfile(completed_path):
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id].update(
{
"status": "verifying",
"progress": 0,
"downloaded": 0,
"total_size": size,
"speed": 0,
"path": completed_path,
"directory": facade.os.path.dirname(completed_path),
"expected_sha256": clean_expected,
}
)
log.info(
f"aria2 transfer complete for {filename}; verifying SHA256 integrity..."
)
verify_fn = getattr(
facade,
"verify_download_sha256_with_progress",
None,
) or verify_download_sha256_with_progress
is_match, actual_sha256, is_cancelled = verify_fn(
completed_path,
clean_expected,
download_id=download_id,
size=size,
dependencies=facade,
)
if is_cancelled or download_id in facade.cancelled_downloads:
facade._delete_partial_download_files(completed_path)
facade.cancelled_downloads.discard(download_id)
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id]["status"] = "cancelled"
facade.download_progress[download_id]["speed"] = 0
result["error"] = "Download cancelled"
return result
if not is_match:
log.error(
f"✗ SHA256 mismatch detected for {filename}! "
f"Expected {clean_expected}, got {actual_sha256}"
)
bad_path = f"{completed_path}.badsha"
try:
facade.os.replace(completed_path, bad_path)
except OSError as exc:
log.warning(
f"Could not preserve SHA256-mismatched download at {bad_path}: {exc}"
)
bad_path = ""
error_msg = f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
if bad_path:
error_msg += f"; file kept at {bad_path}"
sidecar_model_path = (
bad_path
if facade.os.path.exists(completed_path)
else completed_path
)
try:
payload = facade.build_model_resolver_metadata(
sidecar_model_path,
{"sha256": actual_sha256, "size": size},
category,
)
payload.update(
{
"expected_sha256": clean_expected,
"sha256_verified": False,
}
)
sidecar_meta_path = (
facade.get_model_resolver_sidecar_path(
sidecar_model_path
)
)
facade.write_json_atomic(
sidecar_meta_path, payload, indent=2
)
except Exception as exc:
log.warning(
f"Could not save metadata for {bad_path}: {exc}"
)
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id].update(
{
"status": "error",
"error": error_msg,
"speed": 0,
"sha256": actual_sha256,
"actual_sha256": actual_sha256,
"expected_sha256": clean_expected,
"sha256_verified": False,
"path": bad_path or completed_path,
}
)
result.update(
{
"success": False,
"error": error_msg,
"path": bad_path or completed_path,
"size": size,
"sha256": actual_sha256,
"expected_sha256": clean_expected,
"sha256_verified": False,
}
)
return result
metadata_path = facade.write_model_resolver_metadata(
completed_path,
metadata or {},
@@ -1151,6 +1274,13 @@ def download_file_with_aria2(
url,
create_preview=True,
)
if metadata_path and facade.os.path.isfile(metadata_path) and clean_expected:
meta = facade.read_json_safe(metadata_path, {})
if isinstance(meta, dict):
meta["sha256_verified"] = True
meta["sha256"] = clean_expected
facade.write_json_atomic(metadata_path, meta, indent=2)
with facade.download_lock:
facade.download_progress[download_id].update(
{
@@ -1161,6 +1291,9 @@ def download_file_with_aria2(
"speed": 0,
"path": completed_path,
"directory": facade.os.path.dirname(completed_path),
"sha256_verified": True if clean_expected else None,
"sha256": clean_expected or None,
"expected_sha256": clean_expected or None,
}
)
if metadata_path:
@@ -1173,6 +1306,8 @@ def download_file_with_aria2(
"path": completed_path,
"size": size,
"metadata_path": metadata_path,
"sha256_verified": True if clean_expected else None,
"sha256": clean_expected or None,
}
)
guard_fn = getattr(facade, "get_connectivity_guard", None)
@@ -1187,15 +1322,6 @@ def download_file_with_aria2(
)
facade.invalidate_model_caches()
if expected_sha256:
start_background_sha256_verification(
completed_path,
expected_sha256,
download_id=download_id,
metadata_path=metadata_path,
dependencies=facade,
)
return result
if state == "error":
@@ -1315,6 +1441,86 @@ def download_file_with_aria2(
facade._schedule_aria2_idle_stop()
def verify_download_sha256_with_progress(
completed_path: str,
expected_sha256: str,
*,
download_id: str = "",
size: int = 0,
dependencies: Any = None,
) -> tuple[bool, str, bool]:
"""Verify downloaded model SHA256 while streaming progress to download_progress.
Returns (is_match, actual_sha256, is_cancelled).
"""
facade = require_download_dependencies(dependencies, "aria2 backend")
clean_expected = str(expected_sha256 or "").lower().strip()
if not clean_expected or not facade.os.path.isfile(completed_path):
return True, "", False
filename = facade.get_filename_from_path(completed_path)
file_size = (
size
or (
facade.os.path.getsize(completed_path)
if facade.os.path.isfile(completed_path)
else 0
)
)
hasher = facade.hashlib.sha256()
start_t = facade.time.time()
last_ui_update = start_t
last_cli_log = start_t
verified_bytes = 0
last_verified_bytes = 0
with open(completed_path, "rb") as file_handle:
while True:
if download_id and download_id in facade.cancelled_downloads:
return False, "", True
chunk = file_handle.read(4 * 1024 * 1024)
if not chunk:
break
hasher.update(chunk)
verified_bytes += len(chunk)
now = facade.time.time()
if now - last_ui_update >= 0.25:
time_delta = now - last_ui_update
bytes_delta = verified_bytes - last_verified_bytes
verify_speed = int(bytes_delta / time_delta) if time_delta > 0 else 0
last_ui_update = now
last_verified_bytes = verified_bytes
pct = int((verified_bytes / file_size) * 100) if file_size > 0 else 0
if download_id:
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id].update(
{
"status": "verifying",
"progress": pct,
"downloaded": verified_bytes,
"total_size": file_size,
"speed": verify_speed,
}
)
if now - last_cli_log >= facade.CLI_LOG_INTERVAL:
last_cli_log = now
pct = int((verified_bytes / file_size) * 100) if file_size > 0 else 0
log.info(
f"Verifying SHA256 for {filename}: "
f"{facade.format_bytes(verified_bytes)} / {facade.format_bytes(file_size)} "
f"({pct}%)"
)
actual_sha256 = hasher.hexdigest().lower()
return (actual_sha256 == clean_expected), actual_sha256, False
def start_background_sha256_verification(
completed_path: str,
expected_sha256: str,
@@ -1333,26 +1539,29 @@ def start_background_sha256_verification(
filename = facade.get_filename_from_path(completed_path)
log.debug(f"Starting background SHA256 verification for {filename}...")
try:
hasher = facade.hashlib.sha256()
with open(completed_path, "rb") as file_handle:
while True:
chunk = file_handle.read(4 * 1024 * 1024)
if not chunk:
break
hasher.update(chunk)
actual_sha256 = hasher.hexdigest().lower()
is_match, actual_sha256, is_cancelled = verify_download_sha256_with_progress(
completed_path,
expected_sha256,
download_id=download_id,
dependencies=facade,
)
if is_cancelled:
return
if actual_sha256 == clean_expected:
target_meta_path = (
metadata_path
or facade.get_model_resolver_sidecar_path(completed_path)
)
if is_match:
log.info(f"✓ Background SHA256 verified for: {filename}")
if download_id:
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id]["status"] = "completed"
facade.download_progress[download_id]["progress"] = 100
facade.download_progress[download_id]["speed"] = 0
facade.download_progress[download_id]["sha256_verified"] = True
facade.download_progress[download_id]["sha256"] = actual_sha256
target_meta_path = (
metadata_path
or facade.get_model_resolver_sidecar_path(completed_path)
)
if target_meta_path and facade.os.path.isfile(target_meta_path):
meta = facade.read_json_safe(target_meta_path, {})
if isinstance(meta, dict):
@@ -1364,18 +1573,21 @@ def start_background_sha256_verification(
f"✗ SHA256 mismatch detected for {filename}! "
f"Expected {clean_expected}, got {actual_sha256}"
)
bad_path = f"{completed_path}.badsha"
try:
facade.os.replace(completed_path, bad_path)
except OSError:
bad_path = ""
error_msg = f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
if bad_path:
error_msg += f"; file kept at {bad_path}"
if download_id:
with facade.download_lock:
if download_id in facade.download_progress:
facade.download_progress[download_id]["status"] = "error"
facade.download_progress[download_id]["sha256_verified"] = False
facade.download_progress[download_id]["actual_sha256"] = actual_sha256
facade.download_progress[download_id]["error"] = (
f"SHA256 mismatch: expected {clean_expected}, got {actual_sha256}"
)
target_meta_path = (
metadata_path
or facade.get_model_resolver_sidecar_path(completed_path)
)
facade.download_progress[download_id]["error"] = error_msg
if target_meta_path and facade.os.path.isfile(target_meta_path):
meta = facade.read_json_safe(target_meta_path, {})
if isinstance(meta, dict):
+133
View File
@@ -605,6 +605,139 @@ class DownloaderBoundaryTests(unittest.TestCase):
self.assertEqual(result["error"], "Download cancelled")
self.assertEqual(mock_rpc.call_count, 0)
def test_aria2_download_sha256_verification_success(self):
import hashlib
with tempfile.TemporaryDirectory() as temp_dir:
dest_path = os.path.join(temp_dir, "model_verified.safetensors")
content = b"valid model file content for aria2 verification"
with open(dest_path, "wb") as f:
f.write(content)
expected_sha = hashlib.sha256(content).hexdigest()
download_id = "test-aria2-verify-success"
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
mock_rpc = MagicMock(return_value="gid-verify-1")
mock_status = MagicMock(return_value={
"status": "complete",
"completedLength": str(len(content)),
"totalLength": str(len(content)),
})
with patch.object(downloader, "_ensure_aria2_daemon"), \
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
patch.object(downloader, "_aria2_rpc", mock_rpc), \
patch.object(downloader, "_aria2_tell_status", mock_status), \
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
patch.object(downloader, "_schedule_aria2_idle_stop"), \
patch.object(downloader, "invalidate_model_caches"):
result = download_file_with_aria2(
"https://example.com/model.safetensors",
dest_path,
download_id,
expected_sha256=expected_sha,
dependencies=downloader,
)
self.assertTrue(result["success"])
self.assertEqual(dest_path, result["path"])
self.assertTrue(os.path.isfile(dest_path))
with downloader.download_lock:
progress = downloader.download_progress.get(download_id, {})
self.assertEqual("completed", progress.get("status"))
self.assertTrue(progress.get("sha256_verified"))
self.assertEqual(expected_sha, progress.get("sha256"))
def test_aria2_download_sha256_verification_mismatch(self):
with tempfile.TemporaryDirectory() as temp_dir:
dest_path = os.path.join(temp_dir, "model_corrupt.safetensors")
content = b"corrupted model file content"
with open(dest_path, "wb") as f:
f.write(content)
expected_sha = "f" * 64
download_id = "test-aria2-verify-mismatch"
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
mock_rpc = MagicMock(return_value="gid-verify-2")
mock_status = MagicMock(return_value={
"status": "complete",
"completedLength": str(len(content)),
"totalLength": str(len(content)),
})
with patch.object(downloader, "_ensure_aria2_daemon"), \
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
patch.object(downloader, "_aria2_rpc", mock_rpc), \
patch.object(downloader, "_aria2_tell_status", mock_status), \
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
patch.object(downloader, "_schedule_aria2_idle_stop"), \
patch.object(downloader, "invalidate_model_caches"):
result = download_file_with_aria2(
"https://example.com/model.safetensors",
dest_path,
download_id,
expected_sha256=expected_sha,
dependencies=downloader,
)
self.assertFalse(result["success"])
self.assertIn("sha256 mismatch", result["error"].lower())
self.assertFalse(os.path.exists(dest_path))
bad_path = f"{dest_path}.badsha"
self.assertTrue(os.path.isfile(bad_path))
with downloader.download_lock:
progress = downloader.download_progress.get(download_id, {})
self.assertEqual("error", progress.get("status"))
self.assertFalse(progress.get("sha256_verified"))
self.assertIn("mismatch", progress.get("error", "").lower())
def test_aria2_download_sha256_verification_cancelled(self):
with tempfile.TemporaryDirectory() as temp_dir:
dest_path = os.path.join(temp_dir, "model_cancelled.safetensors")
content = b"content being verified"
with open(dest_path, "wb") as f:
f.write(content)
download_id = "test-aria2-verify-cancel"
mock_resolve = MagicMock(return_value=("https://example.com/model.safetensors", {}))
mock_rpc = MagicMock(return_value="gid-verify-3")
mock_status = MagicMock(return_value={
"status": "complete",
"completedLength": str(len(content)),
"totalLength": str(len(content)),
})
def mock_verify(*args, **kwargs):
return False, "", True
with patch.object(downloader, "_ensure_aria2_daemon"), \
patch.object(downloader, "_resolve_download_url_for_aria2", mock_resolve), \
patch.object(downloader, "_aria2_rpc", mock_rpc), \
patch.object(downloader, "_aria2_tell_status", mock_status), \
patch.object(downloader, "verify_download_sha256_with_progress", mock_verify), \
patch.object(downloader, "write_model_resolver_metadata", return_value=""), \
patch.object(downloader, "_schedule_aria2_idle_stop"), \
patch.object(downloader, "invalidate_model_caches"):
result = download_file_with_aria2(
"https://example.com/model.safetensors",
dest_path,
download_id,
expected_sha256="expected_sha",
dependencies=downloader,
)
self.assertFalse(result["success"])
self.assertEqual("Download cancelled", result["error"])
self.assertFalse(os.path.exists(dest_path))
with downloader.download_lock:
progress = downloader.download_progress.get(download_id, {})
self.assertEqual("cancelled", progress.get("status"))
def test_parse_retry_after(self):
self.assertEqual(parse_retry_after("120"), 120)
self.assertEqual(parse_retry_after("0"), 1)
+11
View File
@@ -784,6 +784,17 @@
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
}
.mr-download-queue-status.mr-download-queue-status-verifying {
color: var(--mr-color-code-blue, #38bdf8);
background: rgba(56, 189, 248, 0.16);
border-color: rgba(56, 189, 248, 0.34);
}
.mr-download-queue-progress .mr-progress-fill.mr-progress-fill-verifying {
background: var(--mr-color-code-blue, #38bdf8);
box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
}
.mr-download-queue-stat.mr-download-queue-stat-error {
max-width: 220px;
overflow: hidden;
@@ -4,13 +4,14 @@ export const downloadProgressMethods = {
*/
getDownloadProgressPollDelay(progress = {}, info = {}) {
if (progress.status === 'paused') return 1500;
if (progress.status === 'verifying') return 300;
const backend = String(
progress.download_backend || info.downloadBackend || info.download_backend || ''
).toLowerCase();
return backend === 'huggingface_xet' ? 200 : 1000;
},
getDownloadProgressStatusLabel(status = '', percent = 0, isFinalizing = false) {
getDownloadProgressStatusLabel(status = '', percent = 0, isFinalizing = false, errorMessage = '') {
if (status === 'downloading') {
if (isFinalizing) return 'Finalizing';
const percentLabel = this.formatDownloadPercent?.(percent) ?? String(Math.round(percent));
@@ -18,7 +19,13 @@ export const downloadProgressMethods = {
}
if (status === 'starting') return 'Starting';
if (status === 'paused') return 'Paused';
if (status === 'error' || status === 'refresh_error') return 'Interrupted';
if (status === 'verifying') return 'Verifying SHA-256';
if (status === 'error' || status === 'refresh_error') {
if (String(errorMessage || '').toLowerCase().includes('sha256')) {
return 'SHA-256 mismatch';
}
return 'Interrupted';
}
if (status === 'completed_checking') return 'Checking';
return String(status || '').replace(/_/g, ' ') || 'Download';
},
@@ -28,6 +35,7 @@ export const downloadProgressMethods = {
return status === 'starting'
|| status === 'downloading'
|| status === 'paused'
|| status === 'verifying'
|| (isActive && !terminalStatuses.has(status));
},
};
+28 -9
View File
@@ -1231,13 +1231,28 @@ export const queueMethods = {
const backend = String(progress.download_backend || info.downloadBackend || '').toLowerCase();
const isAria2 = backend === 'aria2';
const isPaused = status === 'paused';
const isVerifying = status === 'verifying';
const isError = status === 'error' || status === 'refresh_error';
const errorMessage = progress.error || info.statusSnapshot?.message || '';
const isShaMismatch = isError && String(errorMessage).toLowerCase().includes('sha256');
const statusLabel = status === 'downloading'
? (presentation.isFinalizing ? 'Finalizing' : '')
: (status === 'starting' ? 'Starting' : (isPaused ? 'Paused' : (isError ? 'Interrupted' : status.replace(/_/g, ' '))));
const sizeTextBase = presentation.isFinalizing && logicalTotal
? `Finalizing file: ${logicalDownloaded} / ${logicalTotal}`
: (presentation.totalSize ? `${downloaded} / ${total}` : downloaded);
: (isVerifying
? 'Verifying SHA-256'
: (status === 'starting'
? 'Starting'
: (isPaused
? 'Paused'
: (isShaMismatch
? 'SHA-256 mismatch'
: (isError
? 'Interrupted'
: status.replace(/_/g, ' '))))));
const sizeTextBase = isVerifying
? `Verifying integrity: ${downloaded} / ${total}`
: (presentation.isFinalizing && logicalTotal
? `Finalizing file: ${logicalDownloaded} / ${logicalTotal}`
: (presentation.totalSize ? `${downloaded} / ${total}` : downloaded));
const sizeText = presentation.totalSize ? `${sizeTextBase} (${percentLabel}%)` : sizeTextBase;
const speed = Number(progress.speed) || 0;
const speedText = speed > 0 ? `${this.formatBytes(speed)}/s` : '';
@@ -1268,9 +1283,11 @@ export const queueMethods = {
html += `<div class="mr-download-queue-heading">`;
html += `<span class="mr-download-queue-filename" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>`;
if (statusLabel) {
const errorMessage = progress.error || info.statusSnapshot?.message || '';
const errorTooltip = isError && errorMessage ? ` data-tooltip="${this.escapeHtml(errorMessage)}"` : '';
html += `<span class="mr-download-queue-status${isError ? ' mr-download-queue-status-error' : ''}"${errorTooltip}>${this.escapeHtml(statusLabel)}</span>`;
const statusClass = isError
? ' mr-download-queue-status-error'
: (isVerifying ? ' mr-download-queue-status-verifying' : '');
html += `<span class="mr-download-queue-status${statusClass}"${errorTooltip}>${this.escapeHtml(statusLabel)}</span>`;
}
html += `</div>`;
html += `<div class="mr-download-queue-actions" role="group" aria-label="Download actions">`;
@@ -1281,7 +1298,7 @@ export const queueMethods = {
isPaused ? 'mr-download-queue-resume' : 'mr-download-queue-pause',
isPaused ? 'Resume download' : 'Pause download',
isPaused ? 'play' : 'pause',
!isAria2
!isAria2 || isVerifying
);
}
html += actionButton('mr-download-queue-cancel', 'Cancel download', 'x');
@@ -1293,12 +1310,14 @@ export const queueMethods = {
html += `</div>`;
html += `</div>`;
html += `<div class="mr-download-queue-progress">`;
html += `<div class="mr-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-label="Download progress"><div class="mr-progress-fill${isError ? ' mr-progress-fill-error' : ''}" style="width: ${percent}%;"></div></div>`;
html += `<div class="mr-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${percent}" aria-label="Download progress"><div class="mr-progress-fill${isError ? ' mr-progress-fill-error' : (isVerifying ? ' mr-progress-fill-verifying' : '')}" style="width: ${percent}%;"></div></div>`;
html += `<div class="mr-download-queue-stats">`;
html += `<span class="mr-download-queue-size">${this.escapeHtml(sizeText)}</span>`;
html += `<span class="mr-download-queue-stat-group">`;
if (speedText && !isError) {
html += `<span class="mr-download-queue-stat">${renderIcon('download', 'mr-download-queue-stat-icon')}<span>${this.escapeHtml(speedText)}</span></span>`;
const speedIcon = isVerifying ? 'hash' : 'download';
const speedTooltip = isVerifying ? 'Hashing speed' : 'Download speed';
html += `<span class="mr-download-queue-stat" data-tooltip="${speedTooltip}">${renderIcon(speedIcon, 'mr-download-queue-stat-icon')}<span>${this.escapeHtml(speedText)}</span></span>`;
}
if (etaText && !isError) {
html += `<span class="mr-download-queue-stat"><span>${this.escapeHtml(etaText)}</span></span>`;
@@ -1291,9 +1291,11 @@ export const resolveDownloadMethods = {
: '';
const label = displayProgress.isFinalizing
? 'Finalizing download...'
: (status === 'paused'
? `Paused ${percentLabel}`.trim()
: (percentLabel ? `Downloading ${percentLabel}` : 'Starting download...'));
: (status === 'verifying'
? (percentLabel ? `Verifying SHA ${percentLabel}` : 'Verifying SHA-256...')
: (status === 'paused'
? `Paused ${percentLabel}`.trim()
: (percentLabel ? `Downloading ${percentLabel}` : 'Starting download...')));
if (downloadBtn.classList.contains('search-download-btn')) {
this.ensureSearchDownloadButtonIcon(downloadBtn);
downloadBtn.setAttribute('data-tooltip', label);
@@ -1301,9 +1303,11 @@ export const resolveDownloadMethods = {
} else {
downloadBtn.textContent = displayProgress.isFinalizing
? 'Finalizing...'
: (status === 'paused'
? 'Paused'
: (Number.isFinite(percent) && percent > 0 ? `${Math.round(percent)}%` : 'Starting...'));
: (status === 'verifying'
? (Number.isFinite(percent) && percent > 0 ? `SHA ${Math.round(percent)}%` : 'Verifying...')
: (status === 'paused'
? 'Paused'
: (Number.isFinite(percent) && percent > 0 ? `${Math.round(percent)}%` : 'Starting...')));
}
} else if (status === 'cancelling') {
downloadBtn.disabled = true;
@@ -2210,7 +2214,7 @@ export const resolveDownloadMethods = {
const { missing, category } = info;
const downloadFolderContext = this.getDownloadFolderContext(progress, info);
if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused') {
if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused' || progress.status === 'verifying') {
this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn });
if (typeof this.requestQueuePanelUpdate === 'function') {
this.requestQueuePanelUpdate();
@@ -2282,6 +2286,7 @@ export const resolveDownloadMethods = {
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.refreshLocalMatchesUiForMissing?.(missing);
this.showNotification(progress.error || 'Download failed', 'error');
} else if (progress.status === 'cancelled') {
this.finalizeCancelledDownloadFrontend?.(downloadId, info, progress);