refactor: centralize source API requests, polling, and path parsing

- Add request_source_response and request_source_json helpers with 429 retry logic
- Introduce shared split_path_segments and parse_provider_model_url utilities
- Migrate CivitAI, CivArchive, HuggingFace, and ModelList sources to centralized helpers
- Add generic pollBackgroundTask utility for frontend progress polling
- Add tests for request_source_json, split_path_segments, parse_provider_model_url
- Add huggingface-hub to requirements
This commit is contained in:
Dariusz L
2026-07-14 12:03:35 +02:00
parent a5b54bb7de
commit 8cf688eba6
14 changed files with 376 additions and 204 deletions
+86
View File
@@ -199,3 +199,89 @@ def request_public_url(
current_headers = next_headers
raise UnsafeUrlError("The download URL has too many redirects")
def request_source_response(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, Any]] = None,
timeout: int = 20,
max_attempts: int = 2,
log_name: str = "Source API",
) -> Optional[requests.Response]:
"""Perform a GET/POST request with retry logic for rate limit status (HTTP 429)."""
import time
from .log_system import create_module_logger
log = create_module_logger(__name__)
request_params = {k: v for k, v in (params or {}).items() if v is not None}
response = None
for attempt in range(max_attempts):
try:
if method.upper() == "POST":
response = requests.post(url, json=request_params, headers=headers, timeout=timeout)
else:
response = requests.get(url, params=request_params, headers=headers, timeout=timeout)
except Exception as e:
log.warning(f"{log_name} request failed: url={url}, error={e}")
return None
if response.status_code != 429 or attempt == max_attempts - 1:
break
retry_after = response.headers.get("Retry-After")
try:
delay = float(retry_after) if retry_after else 1.2
except (TypeError, ValueError):
delay = 1.2
time.sleep(max(0.5, min(delay, 3.0)))
return response
def request_source_json(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, Any]] = None,
timeout: int = 20,
max_attempts: int = 2,
log_name: str = "Source API",
raise_on_error: bool = False,
) -> Optional[Dict[str, Any]]:
"""Perform a GET/POST request and return JSON with retry logic for HTTP 429."""
response = request_source_response(
url,
method=method,
headers=headers,
params=params,
timeout=timeout,
max_attempts=max_attempts,
log_name=log_name,
)
if response is None:
if raise_on_error:
raise ValueError(f"{log_name} request failed: no response")
return None
if response.status_code != 200:
from .log_system import create_module_logger
log = create_module_logger(__name__)
log.debug(f"{log_name} returned HTTP {response.status_code}: url={url}")
if raise_on_error:
response.raise_for_status()
return None
try:
return response.json()
except Exception as e:
from .log_system import create_module_logger
log = create_module_logger(__name__)
log.warning(f"{log_name} JSON parse failed: url={url}, error={e}")
if raise_on_error:
raise
return None
+2 -5
View File
@@ -64,11 +64,8 @@ def _base_model_token_variants(value: Any) -> List[str]:
def _split_relative_folder_segments(relative_path: Any) -> List[str]:
parts = [
part.strip()
for part in str(relative_path or "").replace("\\", "/").split("/")
if part.strip()
]
from .path_utils import split_path_segments
parts = split_path_segments(relative_path)
return parts[:-1] if len(parts) > 1 else []
+19
View File
@@ -1079,3 +1079,22 @@ def find_metadata_sidecar_path(model_path: str) -> str:
return path
return ""
def split_path_segments(path_value: Any, filter_dots: bool = True) -> list[str]:
"""Split path into standard segments, replacing backslashes and filtering empty segments."""
text = str(path_value or "").replace("\\", "/").strip()
if not text:
return []
parts = []
for raw_part in text.split("/"):
part = raw_part.strip()
if not part:
continue
if filter_dots and part in {".", ".."}:
continue
parts.append(part)
return parts
+5 -19
View File
@@ -85,28 +85,14 @@ def sanitize_folder_name(value: Any, fallback: str = "") -> str:
def normalize_relative_subfolder(path_value: Any) -> str:
text = str(path_value or "").replace("\\", "/").strip()
if not text:
return ""
parts = []
for raw_part in text.split("/"):
part = sanitize_folder_name(raw_part)
if part and part not in {".", ".."}:
parts.append(part)
return "/".join(parts)
from .path_utils import split_path_segments
parts = [sanitize_folder_name(part) for part in split_path_segments(path_value)]
return "/".join([part for part in parts if part])
def normalize_download_path_template(template: Any) -> str:
text = str(template or "").replace("\\", "/").strip()
if not text:
return ""
parts = []
for raw_part in text.split("/"):
part = raw_part.strip()
if not part or part in {".", ".."}:
continue
parts.append(part)
return "/".join(parts)
from .path_utils import split_path_segments
return "/".join(split_path_segments(template))
def _read_settings_file() -> Dict[str, Any]:
+16 -48
View File
@@ -43,6 +43,7 @@ from ..type_utils import (
extract_file_size,
normalize_model_file_info,
build_search_result,
normalize_sha256,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
@@ -125,43 +126,16 @@ def _request_json(
params: Optional[Dict[str, Any]] = None,
timeout: int = 20,
) -> Optional[Dict[str, Any]]:
from ..network_utils import request_source_json
url = f"{CIVARCHIVE_API_URL}{path}"
response = None
request_params = {k: v for k, v in (params or {}).items() if v is not None}
for attempt in range(2):
try:
response = requests.get(
url,
params=request_params,
headers=REQUEST_HEADERS,
timeout=timeout,
)
except Exception as e:
log.warning(f"CivArchive API request failed: path={path}, error={e}")
return None
return request_source_json(
url,
params=params,
headers=REQUEST_HEADERS,
timeout=timeout,
log_name="CivArchive API"
)
if response.status_code != 429 or attempt == 1:
break
retry_after = response.headers.get("Retry-After")
try:
delay = float(retry_after) if retry_after else 1.2
except (TypeError, ValueError):
delay = 1.2
time.sleep(max(0.5, min(delay, 3.0)))
if response is None or response.status_code != 200:
status = response.status_code if response is not None else "no-response"
log.debug(
f"CivArchive API returned {status}: path={path}, params={params}"
)
return None
try:
return response.json()
except Exception as e:
log.warning(f"CivArchive API JSON parse failed: path={path}, error={e}")
return None
def _normalize_embedded_html_text(html_text: str) -> str:
@@ -478,30 +452,24 @@ def _search_page(
def parse_civarchive_url(url: str) -> Optional[Dict[str, Any]]:
"""Parse CivArchive URLs into sha256 or model/version identifiers."""
from ..type_utils import parse_provider_model_url
if not url:
return None
parsed = urlparse(urljoin(CIVARCHIVE_BASE_URL, url))
if parsed.hostname and not host_matches_domain(parsed.hostname, "civarchive.com"):
return None
sha_match = re.search(r"/sha256/([a-fA-F0-9]{64})", parsed.path)
if sha_match:
return {"sha256": sha_match.group(1).lower()}
return parse_civitai_model_path(parsed.path, parsed.query)
full_url = urljoin(CIVARCHIVE_BASE_URL, url)
return parse_provider_model_url(full_url, ["civarchive.com"])
def _extract_sha256(value: str) -> Optional[str]:
if not value:
return None
direct = re.fullmatch(r"[a-fA-F0-9]{64}", value.strip())
if direct:
return direct.group(0).lower()
normalized = normalize_sha256(value)
if normalized:
return normalized
parsed = parse_civarchive_url(value)
return parsed.get("sha256") if parsed else None
def _extract_model_context(
payload: Dict[str, Any],
) -> Tuple[Dict[str, Any], Dict[str, Any], List[Dict[str, Any]]]:
+30 -43
View File
@@ -9,6 +9,7 @@ import re
import json
import hashlib
import requests
from ..network_utils import request_source_response, request_source_json
from typing import Dict, Any, Optional, List, Callable
from urllib.parse import urlparse, parse_qs, quote, urlencode
@@ -445,10 +446,9 @@ def _search_civitai_trpc_candidates(
f"CivitAI tRPC search start: filename={filename}, model_type={model_type}, type_filter={type_filter or 'none'}, session_token={'yes' if session_token else 'no'}, url={url}"
)
try:
response = requests.get(url, headers=headers, timeout=timeout)
except Exception as e:
log.warning(f"CivitAI tRPC request failed for filename={filename}: {e}")
response = request_source_response(url, headers=headers, timeout=timeout, log_name="CivitAI tRPC")
if response is None:
log.warning(f"CivitAI tRPC request failed for filename={filename}: no response")
return []
text_preview = response.text[:800].replace("\n", " ").replace("\r", " ")
@@ -497,12 +497,13 @@ def _search_civitai_red_candidates(
headers["Cookie"] = build_civitai_session_cookie(session_token)
log.info(f"CivitAI.red search start: filename={filename}, url={search_url}")
response = requests.get(search_url, headers=headers, timeout=timeout)
if response.status_code != 200:
if response.status_code in {401, 403}:
response = request_source_response(search_url, headers=headers, timeout=timeout, log_name="CivitAI.red")
if response is None or response.status_code != 200:
if response is not None and response.status_code in {401, 403}:
return []
status = response.status_code if response is not None else "no-response"
log.warning(
f"CivitAI.red search returned {response.status_code} for filename={filename}"
f"CivitAI.red search returned {status} for filename={filename}"
)
return []
@@ -582,15 +583,15 @@ def _search_civitai_public_api_candidates(
f"CivitAI public API search start: filename={filename}, model_type={model_type}, api_key={'yes' if api_key else 'no'}, session_token={'yes' if session_token else 'no'}"
)
try:
response = requests.get(
f"{CIVITAI_API_URL}/models",
params=params,
headers=headers,
timeout=timeout,
)
except Exception as e:
log.warning(f"CivitAI public API request failed for filename={filename}: {e}")
response = request_source_response(
f"{CIVITAI_API_URL}/models",
params=params,
headers=headers,
timeout=timeout,
log_name="CivitAI public API"
)
if response is None:
log.warning(f"CivitAI public API request failed for filename={filename}: no response")
return []
text_preview = response.text[:800].replace("\n", " ").replace("\r", " ")
@@ -686,19 +687,15 @@ def _find_civitai_file_in_model(
best_resolved_result = None
best_resolved_confidence = 0.0
headers = {}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
response = requests.get(
f"{CIVITAI_API_URL}/models/{model_id}", headers=headers, timeout=15
data = request_source_json(
f"{CIVITAI_API_URL}/models/{model_id}", headers=headers, timeout=15, log_name="CivitAI model lookup"
)
if response.status_code != 200:
log.warning(f"CivitAI model lookup returned {response.status_code} for model_id={model_id}")
if data is None:
return None
data = response.json()
versions = data.get("modelVersions", [])
if allow_model_title_match:
@@ -796,18 +793,8 @@ def parse_civitai_url(url: str) -> Optional[Dict[str, Any]]:
"""
Parse a CivitAI URL to extract model/version info.
"""
if not isinstance(url, str) or not url.strip():
return None
parsed = urlparse(url)
if not _is_civitai_host(parsed.hostname):
return None
if "/api/download/models/" in parsed.path:
match = re.search(r"/api/download/models/(\d+)", parsed.path)
if match:
return {"version_id": int(match.group(1))}
return parse_civitai_model_path(parsed.path, parsed.query)
from ..type_utils import parse_provider_model_url
return parse_provider_model_url(url, ["civitai.com", "civitai.red"])
def get_civitai_download_url(version_id: int, api_key: Optional[str] = None) -> str:
@@ -1085,12 +1072,11 @@ def search_civitai(
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
response = requests.get(
f"{CIVITAI_API_URL}/models", params=params, headers=headers, timeout=15
data = request_source_json(
f"{CIVITAI_API_URL}/models", params=params, headers=headers, timeout=15, log_name="CivitAI search models"
)
if response.status_code == 200:
data = response.json()
if data:
for model in data.get("items", []):
model_id = model.get("id")
@@ -1174,10 +1160,11 @@ def get_civitai_model_details(
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
response = requests.get(
response = request_source_response(
f"{CIVITAI_API_URL}/models/{model_id}",
headers=headers,
timeout=20,
log_name="CivitAI model details"
)
if response.status_code != 200:
log.warning(f"CivitAI model details returned {response.status_code}: model_id={model_id}")
@@ -1332,7 +1319,7 @@ def resolve_urn(
url = f"{CIVITAI_API_URL}/models/{model_id}"
params = {"modelVersionId": version_id}
response = requests.get(url, headers=headers, params=params, timeout=15)
response = request_source_response(url, headers=headers, params=params, timeout=15, log_name="CivitAI URN resolve")
if response.status_code != 200:
log.warning(f"CivitAI URN resolve failed: {response.status_code}")
@@ -1443,7 +1430,7 @@ def get_model_info_by_hash(
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
response = requests.get(api_url, headers=headers, timeout=15)
response = request_source_response(api_url, headers=headers, timeout=15, log_name="CivitAI by-hash")
if response.status_code == 200:
data = response.json()
+14 -11
View File
@@ -18,7 +18,7 @@ from ..log_system import create_module_logger
log = create_module_logger(__name__)
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe
from ..network_utils import host_matches_domain
from ..network_utils import host_matches_domain, request_source_response, request_source_json
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size
from ..matcher import build_filename_search_queries, clean_filename_for_search
@@ -188,11 +188,12 @@ def _build_author_index_from_models(
def _fetch_author_index(author: str, headers: Dict[str, str], limit: int = 200):
try:
response = requests.get(
response = request_source_response(
f"{HF_API_URL}/models",
params={"author": author, "limit": limit, "full": "true"},
headers=headers,
timeout=15,
log_name="HuggingFace author index"
)
if response.status_code != 200:
log.debug(
@@ -389,11 +390,12 @@ def _get_repo_tree(
files_url = f"{HF_API_URL}/models/{repo_id}/tree/{quote(branch, safe='')}"
try:
response = requests.get(
response = request_source_response(
files_url,
params={"recursive": "1"},
headers=headers,
timeout=15,
log_name="HuggingFace tree"
)
if response.status_code != 200:
log.debug(
@@ -528,11 +530,12 @@ def _get_repos_by_author(
author: str, headers: Dict[str, str], limit: int = 200
) -> List[str]:
try:
response = requests.get(
response = request_source_response(
f"{HF_API_URL}/models",
params={"author": author, "limit": limit},
headers=headers,
timeout=15,
log_name="HuggingFace repos by author"
)
if response.status_code != 200:
log.debug(
@@ -569,8 +572,8 @@ def _search_brave_for_huggingface_candidates(
seen = set()
try:
response = requests.get(
BRAVE_SEARCH_API_URL, headers=headers, params=params, timeout=15
response = request_source_response(
BRAVE_SEARCH_API_URL, headers=headers, params=params, timeout=15, log_name="Brave search"
)
if response.status_code != 200:
log.warning(
@@ -689,7 +692,7 @@ def search_huggingface_for_file(
query_count=total_queries,
)
search_url = f"{HF_API_URL}/models?search={quote(search_query)}&limit=20"
response = requests.get(search_url, headers=headers, timeout=10)
response = request_source_response(search_url, headers=headers, timeout=10, log_name="HuggingFace models search")
if response.status_code != 200:
log.warning(
f"HuggingFace API status={response.status_code} query={search_query}"
@@ -969,12 +972,12 @@ def search_huggingface(
params = {"search": query, "limit": limit, "full": "true"}
response = requests.get(
f"{HF_API_URL}/models", params=params, headers=headers, timeout=15
data = request_source_json(
f"{HF_API_URL}/models", params=params, headers=headers, timeout=15, log_name="HuggingFace general search"
)
if response.status_code == 200:
models = response.json()
if data:
models = data
for model in models:
repo_id = model.get("id", "")
+9 -3
View File
@@ -27,6 +27,7 @@ MODEL_LIST_SOURCE_URL = (
MODEL_LIST_GITHUB_API_URL = (
"https://api.github.com/repos/Comfy-Org/ComfyUI-Manager/contents/model-list.json?ref=main"
)
from ..network_utils import request_source_json
HTTP_TIMEOUT = 30
# Cache for loaded data
@@ -42,9 +43,14 @@ def _fetch_json_url(url: str) -> Dict[str, Any]:
"Accept": "application/json",
"User-Agent": "comfyui-model-resolver",
}
response = requests.get(url, headers=headers, timeout=HTTP_TIMEOUT)
response.raise_for_status()
return response.json()
return request_source_json(
url,
headers=headers,
timeout=HTTP_TIMEOUT,
log_name="ModelList",
raise_on_error=True,
)
+35
View File
@@ -1072,3 +1072,38 @@ def build_search_result(
}
result.update(extra)
return result
def parse_provider_model_url(url: str, allowed_domains: list[str]) -> Optional[Dict[str, Any]]:
"""Parse a provider model URL (e.g. CivitAI or mirror site) to extract model/version info."""
from urllib.parse import urlparse
import re
if not isinstance(url, str) or not url.strip():
return None
parsed = urlparse(url)
hostname = parsed.hostname
if hostname:
hostname = hostname.lower().strip(".")
matched = False
for domain in allowed_domains:
if hostname == domain or hostname.endswith("." + domain):
matched = True
break
if not matched:
return None
# Standard CivArchive sha256 mirrors pattern
sha_match = re.search(r"/sha256/([a-fA-F0-9]{64})", parsed.path)
if sha_match:
return {"sha256": sha_match.group(1).lower()}
# Standard CivitAI download pattern
if "/api/download/models/" in parsed.path:
match = re.search(r"/api/download/models/(\d+)", parsed.path)
if match:
return {"version_id": int(match.group(1))}
return parse_civitai_model_path(parsed.path, parsed.query)
+1
View File
@@ -1,3 +1,4 @@
requests
aiohttp
rapidfuzz
huggingface-hub
+61
View File
@@ -1128,3 +1128,64 @@ class UnifiedHelpersTests(unittest.TestCase):
# Verify new data is written
self.assertEqual(read_json_safe(data_file), new_data)
self.assertEqual(read_json_safe(meta_file), new_meta)
@patch("requests.get")
def test_request_source_json_success(self, mock_get):
from core.network_utils import request_source_json
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_get.return_value = mock_response
res = request_source_json("https://example.com/api")
self.assertEqual(res, {"success": True})
@patch("requests.get")
@patch("time.sleep")
def test_request_source_json_429_retry(self, mock_sleep, mock_get):
from core.network_utils import request_source_json
mock_response_429 = MagicMock()
mock_response_429.status_code = 429
mock_response_429.headers = {"Retry-After": "1.5"}
mock_response_200 = MagicMock()
mock_response_200.status_code = 200
mock_response_200.json.return_value = {"ok": True}
mock_get.side_effect = [mock_response_429, mock_response_200]
res = request_source_json("https://example.com/api", max_attempts=2)
self.assertEqual(res, {"ok": True})
mock_sleep.assert_called_once_with(1.5)
def test_split_path_segments(self):
from core.path_utils import split_path_segments
self.assertEqual(split_path_segments("a/b/c"), ["a", "b", "c"])
self.assertEqual(split_path_segments("a\\b\\c"), ["a", "b", "c"])
self.assertEqual(split_path_segments("a/./b/../c"), ["a", "b", "c"])
self.assertEqual(split_path_segments("a/./b/../c", filter_dots=False), ["a", ".", "b", "..", "c"])
self.assertEqual(split_path_segments(""), [])
self.assertEqual(split_path_segments(None), [])
def test_parse_provider_model_url(self):
from core.type_utils import parse_provider_model_url
allowed = ["civitai.com", "civitai.red"]
# Valid url download format
res = parse_provider_model_url("https://civitai.com/api/download/models/12345", allowed)
self.assertEqual(res, {"version_id": 12345})
# Valid model page format
res = parse_provider_model_url("https://civitai.red/models/9876?modelVersionId=54321", allowed)
self.assertEqual(res, {"model_id": 9876, "version_id": 54321})
# Non-matching hostname
res = parse_provider_model_url("https://example.com/api/download/models/12345", allowed)
self.assertIsNone(res)
# Mirror sha256 path pattern
res = parse_provider_model_url("https://civarchive.com/sha256/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", ["civarchive.com"])
self.assertEqual(res, {"sha256": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"})
+40
View File
@@ -119,3 +119,43 @@ export function sanitizeDescriptionHtml(html) {
Array.from(root.childNodes).forEach(child => sanitizeNode(child));
return root.innerHTML;
}
/**
* Generic background task progress poller with token cancellation checks.
*/
export async function pollBackgroundTask({
endpoint,
tokenCheck,
onProgress,
isTerminal,
onTerminal,
onError,
intervalMs = 250,
fetchJson,
filterIgnoredStatus
}) {
while (tokenCheck()) {
try {
const data = await fetchJson(endpoint, { silent: true }, 'Poll background task');
if (filterIgnoredStatus && filterIgnoredStatus(data)) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
continue;
}
if (tokenCheck()) {
onProgress(data);
if (isTerminal(data)) {
onTerminal(data);
return;
}
}
} catch (error) {
if (tokenCheck()) {
onError(error);
return;
}
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
+36 -51
View File
@@ -2,7 +2,7 @@ import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { escapeHtml, escapeJsString } from "./html_utils.js";
import { escapeHtml, escapeJsString, pollBackgroundTask } from "./html_utils.js";
export const renderFormatMethods = {
encodeContextMenuModel(context = null) {
@@ -248,69 +248,54 @@ export const renderFormatMethods = {
},
async pollAnalysisProgress(analysisId, token) {
while (this._analysisProgressToken === token) {
try {
const progress = await this.fetchJson(`/model_resolver/analyze-progress/${analysisId}`, {
silent: true
}, 'Poll analysis progress');
if (
progress &&
this.contentElement &&
this._analysisProgressToken === token &&
this.activeTab === 'missing'
) {
await pollBackgroundTask({
endpoint: `/model_resolver/analyze-progress/${analysisId}`,
tokenCheck: () => this._analysisProgressToken === token,
onProgress: (progress) => {
if (this.contentElement && this.activeTab === 'missing') {
this.contentElement.innerHTML = this.renderAnalysisProgress(progress);
if (progress.status === 'completed' || progress.status === 'error') {
if (this._analysisProgressToken === token) {
this._analysisProgressToken = null;
}
return;
}
}
} catch (error) {
},
isTerminal: (progress) => progress.status === 'completed' || progress.status === 'error',
onTerminal: () => {
if (this._analysisProgressToken === token) {
this._analysisProgressToken = null;
}
},
onError: (error) => {
console.warn('Model Resolver: analysis progress polling failed', error);
}
await new Promise(resolve => setTimeout(resolve, 250));
}
},
intervalMs: 250,
fetchJson: this.fetchJson.bind(this)
});
},
async pollLoadedModelsProgress(loadedId, token) {
while (this._loadedModelsProgressToken === token) {
try {
const progress = await this.fetchJson(`/model_resolver/loaded-progress/${encodeURIComponent(loadedId)}`, {
silent: true
}, 'Poll loaded models progress');
if (progress?.status === 'unknown') {
await new Promise(resolve => setTimeout(resolve, 250));
continue;
}
if (
progress &&
this.contentElement &&
this._loadedModelsProgressToken === token &&
this.activeTab === 'loaded'
) {
await pollBackgroundTask({
endpoint: `/model_resolver/loaded-progress/${encodeURIComponent(loadedId)}`,
tokenCheck: () => this._loadedModelsProgressToken === token,
filterIgnoredStatus: (progress) => progress?.status === 'unknown',
onProgress: (progress) => {
if (this.contentElement && this.activeTab === 'loaded') {
this.contentElement.innerHTML = this.renderLoadedModelsProgress(progress);
if (progress.status === 'completed' || progress.status === 'error') {
if (this._loadedModelsProgressToken === token) {
this._loadedModelsProgressToken = null;
}
return;
}
}
} catch (error) {
},
isTerminal: (progress) => progress.status === 'completed' || progress.status === 'error',
onTerminal: () => {
if (this._loadedModelsProgressToken === token) {
this._loadedModelsProgressToken = null;
}
},
onError: (error) => {
console.warn('Model Resolver: loaded models progress polling failed', error);
}
await new Promise(resolve => setTimeout(resolve, 250));
}
},
intervalMs: 250,
fetchJson: this.fetchJson.bind(this)
});
},
/**
* Get display information for the best local match of a missing model
* @param {object} missing - The missing model object
* @returns {object} Match display metadata
*/
getLocalMatchDisplayInfo(missing) {
const bestMatch = this.getBestLocalMatch?.(missing, 70) || null;
+22 -24
View File
@@ -4,6 +4,7 @@ import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { LOG_LEVEL as DEFAULT_FRONTEND_LOG_LEVEL } from "../../log_system/config.js";
import { logger as frontendLogger } from "../../log_system/logger.js";
import { pollBackgroundTask } from "../utils/html_utils.js";
const SETTINGS_MAP = [
{ serverKey: 'civitai_key', localKey: 'ModelResolver.civitaiApiKey', type: 'string', default: '' },
{ serverKey: 'civitai_session_token', localKey: 'ModelResolver.civitaiSessionToken', type: 'string', default: '' },
@@ -2067,34 +2068,31 @@ export const optionsMethods = {
const pollMetadataBuildProgress = async () => {
const progressId = this.metadataBuildProgressId;
if (!progressId) return;
try {
const data = await this.fetchJson(
`/model_resolver/metadata-build/progress/${encodeURIComponent(progressId)}`,
{ silent: true },
'Metadata build progress'
);
appendMetadataBuildHistory(data);
updateMetadataBuildSummary(data);
updateMetadataBuildProgressView(data);
renderMetadataBuildResults(data);
if (isMetadataBuildTerminal(data)) {
stopMetadataBuildPolling();
stopMetadataBuildPolling();
await pollBackgroundTask({
endpoint: `/model_resolver/metadata-build/progress/${encodeURIComponent(progressId)}`,
tokenCheck: () => this.metadataBuildProgressId === progressId,
onProgress: (data) => {
appendMetadataBuildHistory(data);
updateMetadataBuildSummary(data);
updateMetadataBuildProgressView(data);
renderMetadataBuildResults(data);
},
isTerminal: (data) => isMetadataBuildTerminal(data),
onTerminal: (data) => {
finishMetadataBuild(data);
if (this.metadataBuildProgressId === progressId) {
this.metadataBuildProgressId = '';
}
return;
}
stopMetadataBuildPolling();
this.metadataBuildPollTimer = window.setTimeout(() => {
pollMetadataBuildProgress();
}, 700);
} catch (error) {
stopMetadataBuildPolling();
this.metadataBuildProgressId = '';
setMetadataBuildBusy(false);
setMetadataBuildStatus(error.message || 'Metadata build progress failed.', 'is-invalid');
}
},
onError: (error) => {
this.metadataBuildProgressId = '';
setMetadataBuildBusy(false);
setMetadataBuildStatus(error.message || 'Metadata build progress failed.', 'is-invalid');
},
intervalMs: 700,
fetchJson: this.fetchJson.bind(this)
});
};
const runMetadataBuild = async () => {