annotate active downloads in local matches

This commit is contained in:
Dariusz L
2026-07-05 05:16:20 +02:00
parent 629bc791a6
commit f691af6387
4 changed files with 212 additions and 5 deletions
+109 -4
View File
@@ -53,6 +53,7 @@ from .path_utils import (
_LOCAL_HASH_MATCH_CACHE_LOCK = threading.Lock()
_LOCAL_HASH_MATCH_CACHE: Optional[Dict[str, List[Dict[str, Any]]]] = None
_ACTIVE_DOWNLOAD_STATUSES = {"starting", "downloading", "paused", "cancelling"}
def invalidate_local_hash_match_cache() -> None:
@@ -69,6 +70,105 @@ def _clone_hash_match(match: Dict[str, Any]) -> Dict[str, Any]:
return cloned
def _normalize_download_match_path(path: Any) -> str:
text = str(path or "").strip()
if not text:
return ""
try:
return os.path.normcase(os.path.abspath(os.path.normpath(text)))
except (OSError, ValueError):
return os.path.normcase(os.path.normpath(text))
def _get_active_downloads_by_path() -> Dict[str, Dict[str, Any]]:
try:
from .downloader import get_all_progress
progress_items = get_all_progress()
except Exception:
return {}
active: Dict[str, Dict[str, Any]] = {}
for download_id, progress in progress_items.items():
if not isinstance(progress, dict):
continue
status = str(progress.get("status") or "").strip().lower()
if status not in _ACTIVE_DOWNLOAD_STATUSES:
continue
path = progress.get("path") or ""
if not path and progress.get("directory") and progress.get("filename"):
path = os.path.join(str(progress["directory"]), str(progress["filename"]))
path_key = _normalize_download_match_path(path)
if not path_key:
continue
active[path_key] = {
"download_id": download_id,
"download_status": status,
"download_progress": progress.get("progress", 0),
"downloaded": progress.get("downloaded", 0),
"total_size": progress.get("total_size", 0),
}
return active
def annotate_local_matches_with_download_state(
matches: List[Dict[str, Any]],
active_downloads_by_path: Optional[Dict[str, Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
active_downloads = (
active_downloads_by_path
if active_downloads_by_path is not None
else _get_active_downloads_by_path()
)
if not active_downloads:
return matches
enriched_matches: List[Dict[str, Any]] = []
for match in matches:
if not isinstance(match, dict):
enriched_matches.append(match)
continue
model = match.get("model") if isinstance(match.get("model"), dict) else {}
candidate_paths = [
model.get("path"),
model.get("resolved_path"),
match.get("path"),
match.get("resolved_path"),
]
download_info = None
for candidate_path in candidate_paths:
path_key = _normalize_download_match_path(candidate_path)
if path_key and path_key in active_downloads:
download_info = active_downloads[path_key]
break
if not download_info:
enriched_matches.append(match)
continue
enriched_match = dict(match)
enriched_model = dict(model)
download_fields = {
**download_info,
"is_downloading": True,
"downloading": True,
}
enriched_match.update(download_fields)
if enriched_model:
enriched_model.update(download_fields)
enriched_match["model"] = enriched_model
enriched_matches.append(enriched_match)
return enriched_matches
def _is_local_hash_match_candidate(model: Dict[str, Any]) -> bool:
model_path = str(model.get("path") or "").strip()
if not model_path:
@@ -389,7 +489,7 @@ def search_local_matches(
deduplicated_matches[idx] = match
seen_absolute_paths[dedupe_key] = match
return deduplicated_matches
return annotate_local_matches_with_download_state(deduplicated_matches)
def _collect_hashes_from_container(value: Any) -> List[str]:
@@ -477,9 +577,10 @@ def search_local_matches_by_hash(
else 1
)
annotated_matches = annotate_local_matches_with_download_state(matches)
if max_matches > 0:
return matches[:max_matches]
return matches
return annotated_matches[:max_matches]
return annotated_matches
def get_local_model_hash_metadata(
@@ -867,6 +968,7 @@ def analyze_and_find_matches(
)
local_match_cache: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}
active_downloads_by_path = _get_active_downloads_by_path()
def get_match_target(model_ref: Dict[str, Any]) -> Optional[str]:
target_for_matching = (
@@ -961,7 +1063,10 @@ def analyze_and_find_matches(
threshold=similarity_threshold,
max_results=max_matches_per_model,
)
deduplicated_matches = deduplicate_matches(matches)
deduplicated_matches = annotate_local_matches_with_download_state(
deduplicate_matches(matches),
active_downloads_by_path,
)
local_match_cache[cache_key] = deduplicated_matches
return deduplicated_matches
+12
View File
@@ -1923,11 +1923,23 @@
cursor: help;
}
.mr-match-status-downloading {
color: #67e8f9;
background: rgba(6, 182, 212, 0.12);
box-shadow: inset 0 0 0 1px rgba(6, 182, 212, 0.24);
cursor: help;
}
.mr-match-status-bad-folder:focus-visible {
outline: 2px solid rgba(248, 113, 113, 0.58);
outline-offset: 2px;
}
.mr-match-status-downloading:focus-visible {
outline: 2px solid rgba(6, 182, 212, 0.58);
outline-offset: 2px;
}
.mr-local-alternatives-toggle {
display: flex;
align-items: center;
@@ -1775,6 +1775,7 @@ export const resolveDownloadMethods = {
if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused') {
this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn });
this.refreshLocalMatchesUiForMissing?.(missing);
if (typeof this.requestQueuePanelUpdate === 'function') {
this.requestQueuePanelUpdate();
} else {
@@ -1800,6 +1801,7 @@ export const resolveDownloadMethods = {
delete this.activeDownloads[downloadId];
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.refreshLocalMatchesUiForMissing?.(missing);
this.showNotification(alreadyExists
? `Already downloaded: ${progress.filename}`
: `Downloaded: ${progress.filename}`, 'success', {
@@ -1831,6 +1833,7 @@ export const resolveDownloadMethods = {
delete this.activeDownloads[downloadId];
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.refreshLocalMatchesUiForMissing?.(missing);
} else if (progress.status === 'cancelled') {
const cancelledSnapshot = this.rememberDownloadUiState(downloadId, info, progress, {
@@ -1842,6 +1845,7 @@ export const resolveDownloadMethods = {
delete this.activeDownloads[downloadId];
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.refreshLocalMatchesUiForMissing?.(missing);
this.showNotification('Download cancelled', 'info');
} else {
+87 -1
View File
@@ -2407,8 +2407,94 @@ export const searchPanelMethods = {
return `<span class="mr-match-status mr-match-status-bad-folder" tabindex="0" data-tooltip="${this.escapeHtml(warning.tooltip)}">Bad folder</span>`;
},
getActiveDownloadInfoForLocalMatch(match = {}) {
const model = match.model || {};
const normalizePath = (value = '') => this.normalizeLocalMatchPathIdentity?.(value) || String(value || '').trim().replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase();
const joinPath = (...parts) => normalizePath(parts.filter(Boolean).join('/'));
const activeStatuses = new Set(['starting', 'downloading', 'paused', 'cancelling']);
const matchAbsolutePaths = [
model.path,
model.resolved_path,
match.path,
match.resolved_path
].map(normalizePath).filter(Boolean);
const matchRelativePaths = [
model.relative_path,
match.relative_path
].map(normalizePath).filter(Boolean);
const matchFilename = normalizePath(model.filename || match.filename || '');
for (const [downloadId, info] of Object.entries(this.activeDownloads || {})) {
const progress = info?.lastProgress || info?.statusSnapshot?.progress || {};
const status = String(progress.status || info?.lastStatus || info?.statusSnapshot?.status || 'starting').trim().toLowerCase();
if (!activeStatuses.has(status)) continue;
const filename = progress.filename || info?.filename || '';
const absoluteCandidates = [
progress.path,
info?.downloadPath,
info?.statusSnapshot?.downloadPath,
joinPath(progress.directory || '', filename),
joinPath(info?.downloadDirectory || '', filename),
joinPath(info?.statusSnapshot?.downloadDirectory || '', filename)
].map(normalizePath).filter(Boolean);
const relativeCandidates = [
info?.subfolder && filename ? joinPath(info.subfolder, filename) : '',
progress.relative_path,
info?.relativePath
].map(normalizePath).filter(Boolean);
const absoluteMatch = absoluteCandidates.some(activePath => (
matchAbsolutePaths.includes(activePath)
|| matchRelativePaths.some(relativePath => activePath.endsWith(`/${relativePath}`))
));
const relativeMatch = relativeCandidates.some(activeRelative => (
matchRelativePaths.includes(activeRelative)
|| matchAbsolutePaths.some(matchPath => matchPath.endsWith(`/${activeRelative}`))
));
if (!absoluteMatch && !relativeMatch) continue;
return {
download_id: downloadId,
download_status: status,
download_progress: progress.progress,
downloaded: progress.downloaded,
total_size: progress.total_size,
filename: filename || matchFilename,
is_downloading: true,
downloading: true
};
}
return null;
},
renderLocalMatchDownloadingBadge(match = {}) {
const model = match.model || {};
const activeDownloadInfo = this.getActiveDownloadInfoForLocalMatch?.(match) || null;
const isDownloading = Boolean(
match.is_downloading
|| match.downloading
|| model.is_downloading
|| model.downloading
|| activeDownloadInfo
);
if (!isDownloading) return '';
const status = String(activeDownloadInfo?.download_status || match.download_status || model.download_status || '').trim().toLowerCase();
const progress = Number(activeDownloadInfo?.download_progress ?? match.download_progress ?? model.download_progress);
const progressLabel = Number.isFinite(progress) && progress > 0 && progress < 100
? ` (${Math.round(progress)}%)`
: '';
const statusLabel = status === 'paused'
? 'paused'
: (status === 'starting' ? 'starting' : 'still downloading');
const tooltip = `The download for this local file is ${statusLabel}${progressLabel}. It may be incomplete until the download finishes.`;
return `<span class="mr-match-status mr-match-status-downloading" tabindex="0" data-tooltip="${this.escapeHtml(tooltip)}">Downloading</span>`;
},
renderLocalMatchStatusGroup(missing = {}, match = {}, hashLabelMap = null) {
return `<span class="mr-match-status-group">${this.renderLocalMatchStatus(match, hashLabelMap)}${this.renderLocalMatchBadFolderBadge(missing, match)}</span>`;
return `<span class="mr-match-status-group">${this.renderLocalMatchStatus(match, hashLabelMap)}${this.renderLocalMatchDownloadingBadge(match)}${this.renderLocalMatchBadFolderBadge(missing, match)}</span>`;
},
areLocalMatchAlternativesCollapsed() {