annotate active downloads in local matches
This commit is contained in:
+109
-4
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user