refactor: centralize filename and size utilities across sources

- Introduce get_filename_from_path in path_utils and replace repeated
  os.path.basename calls across core/downloader.py, core/resolver.py,
  core/sources/civarchive.py, core/sources/civitai.py, and
  core/sources/huggingface.py
- Move duplicated inline HTTP response size helpers (_response_file_size,
  _parse_size_header, _parse_content_range_size) into core/type_utils.py
  and update civarchive.py and huggingface.py to import them
- Remove inline normalize_sha256_value from __init__.py and reuse the
  existing normalize_sha256 helper
- Refactor options_methods.js localStorage read/write logic into a
  SETTINGS_MAP-driven loop to reduce repetition
This commit is contained in:
Dariusz L
2026-07-02 03:37:44 +02:00
parent 59e61ba077
commit 2d2e8b4621
8 changed files with 159 additions and 225 deletions
+2 -13
View File
@@ -1320,28 +1320,17 @@ class ModelResolverExtension:
"metadata_checked": bool(civitai_checked),
"civitai_checked": bool(civitai_checked),
}
def normalize_sha256_value(value):
text = str(value or "").strip().lower()
if text.startswith("sha256:"):
text = text.split(":", 1)[1].strip()
if len(text) == 64 and all(
ch in "0123456789abcdef" for ch in text
):
return text
return ""
def extract_result_sha256(result):
if not isinstance(result, dict):
return ""
for key in ("sha256", "hash"):
normalized = normalize_sha256_value(result.get(key))
normalized = normalize_sha256(result.get(key))
if normalized:
return normalized
hashes = result.get("hashes")
if isinstance(hashes, dict):
for key in ("SHA256", "sha256", "Sha256"):
normalized = normalize_sha256_value(hashes.get(key))
normalized = normalize_sha256(hashes.get(key))
if normalized:
return normalized
return ""
+9 -9
View File
@@ -23,7 +23,7 @@ from .log_system.log_funcs import create_module_logger
log = create_module_logger(__name__)
from .resolver import normalize_sha256
from .path_utils import is_path_within, get_path_identity, write_json_atomic, read_json_safe, get_comfy_root_path, calculate_file_sha256 as _calculate_file_sha256
from .path_utils import is_path_within, get_path_identity, write_json_atomic, read_json_safe, get_comfy_root_path, calculate_file_sha256 as _calculate_file_sha256, get_filename_from_path
from .type_utils import as_dict, as_list, first_non_empty, format_size_bytes as format_bytes
try:
@@ -310,7 +310,7 @@ def _find_metadata_file_info(
if isinstance(value, dict):
return value
filename_lower = os.path.basename(str(filename or "")).lower()
filename_lower = get_filename_from_path(str(filename or "")).lower()
file_lists = [
source.get("files"),
selected_version.get("files"),
@@ -404,7 +404,7 @@ def build_lora_manager_metadata(
source.get("selected_version") or details.get("selected_version")
)
basename = os.path.basename(dest_path)
basename = get_filename_from_path(dest_path)
file_name = os.path.splitext(basename)[0]
filename = _first_present(
source.get("filename"),
@@ -679,7 +679,7 @@ def get_download_directory(category: str, preferred_base_directory: str = "") ->
comfy_root = get_comfy_root_path(folder_paths)
def _basename(path_value: str) -> str:
return os.path.basename(os.path.normpath(path_value)).lower()
return get_filename_from_path(os.path.normpath(path_value)).lower()
def _prefer_redirected(candidate_paths: List[str]) -> Optional[str]:
if not candidate_paths:
@@ -1170,7 +1170,7 @@ def download_file_with_aria2(
"size": 0,
}
start_time = time.time()
filename = os.path.basename(dest_path)
filename = get_filename_from_path(dest_path)
with download_lock:
download_progress[download_id] = {
@@ -1431,7 +1431,7 @@ def download_file(
"progress": 0,
"total_size": 0,
"downloaded": 0,
"filename": os.path.basename(dest_path),
"filename": get_filename_from_path(dest_path),
"path": dest_path,
"directory": os.path.dirname(dest_path),
"url": url,
@@ -1446,7 +1446,7 @@ def download_file(
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
# Verbose logging - what model and from where
filename = os.path.basename(dest_path)
filename = get_filename_from_path(dest_path)
source = (
"HuggingFace"
if "huggingface.co" in url
@@ -1627,7 +1627,7 @@ def download_file(
result["error"] = error_msg
# CLI error log
log.error(f"✗ Download failed: {os.path.basename(dest_path)}")
log.error(f"✗ Download failed: {get_filename_from_path(dest_path)}")
log.error(f"Error: {error_msg}")
# Clean up partial file
@@ -1645,7 +1645,7 @@ def download_file(
result["error"] = error_msg
# CLI error log
log.error(f"✗ Download failed: {os.path.basename(dest_path)}")
log.error(f"✗ Download failed: {get_filename_from_path(dest_path)}")
log.error(f"Error: {error_msg}")
log.error(f"Download error: {e}", exc_info=True)
+10 -9
View File
@@ -42,6 +42,7 @@ from .path_utils import (
is_path_within,
prefer_local_base_directory,
dedupe_local_base_directories,
get_filename_from_path,
)
@@ -54,13 +55,13 @@ def get_workflow_url_info_for_filename(
if filename in workflow_urls:
return workflow_urls[filename]
filename_stem = strip_known_model_extension(os.path.basename(filename)).lower()
filename_stem = strip_known_model_extension(get_filename_from_path(filename)).lower()
if not filename_stem:
return None
for workflow_filename, url_info in workflow_urls.items():
workflow_stem = strip_known_model_extension(
os.path.basename(workflow_filename)
get_filename_from_path(workflow_filename)
).lower()
if workflow_stem == filename_stem:
return url_info
@@ -294,7 +295,7 @@ def _metadata_sidecar_candidates(model_path: str) -> List[str]:
return []
directory = os.path.dirname(model_path)
filename = os.path.basename(model_path)
filename = get_filename_from_path(model_path)
stem, _ = os.path.splitext(filename)
candidates = [
@@ -340,9 +341,9 @@ def _collect_hashes_from_container(value: Any) -> List[str]:
def _metadata_file_matches_model(file_info: Dict[str, Any], model: Dict[str, Any]) -> bool:
model_filename = str(model.get("filename") or os.path.basename(model.get("path", ""))).lower()
model_filename = str(model.get("filename") or get_filename_from_path(model.get("path", ""))).lower()
model_relative = str(model.get("relative_path") or "").replace("\\", "/").lower()
model_stem = strip_known_model_extension(os.path.basename(model_filename)).lower()
model_stem = strip_known_model_extension(get_filename_from_path(model_filename)).lower()
candidates = [
file_info.get("name"),
@@ -352,7 +353,7 @@ def _metadata_file_matches_model(file_info: Dict[str, Any], model: Dict[str, Any
]
for candidate in candidates:
text = str(candidate or "").replace("\\", "/").lower()
basename = os.path.basename(text)
basename = get_filename_from_path(text)
stem = strip_known_model_extension(basename).lower()
if text and model_relative and text == model_relative:
return True
@@ -463,7 +464,7 @@ def search_local_matches_by_hash(
matches.append(
{
"model": model_with_metadata,
"filename": model.get("filename") or os.path.basename(model_path),
"filename": model.get("filename") or get_filename_from_path(model_path),
"similarity": 1.0,
"confidence": 100.0,
"match_type": "hash",
@@ -499,7 +500,7 @@ def get_local_model_hash_metadata(
**(model if isinstance(model, dict) else {}),
"path": normalized_path,
}
model_info.setdefault("filename", os.path.basename(normalized_path))
model_info.setdefault("filename", get_filename_from_path(normalized_path))
model_info.setdefault("relative_path", model_info.get("filename", ""))
first_metadata_path = ""
@@ -846,7 +847,7 @@ def analyze_and_find_matches(
# Enrich missing models with workflow URLs
for missing in missing_models:
original_path = missing.get("original_path", "")
filename = os.path.basename(original_path)
filename = get_filename_from_path(original_path)
url_info = get_workflow_url_info_for_filename(workflow_urls, filename)
if url_info:
+9 -42
View File
@@ -23,6 +23,7 @@ from ..matcher import (
base_model_score as _base_model_score,
calculate_candidate_rank,
)
from ..path_utils import get_filename_from_path
from ..type_utils import (
to_int,
CIVARCHIVE_API_TYPE_MAP,
@@ -30,6 +31,9 @@ from ..type_utils import (
parse_size_to_bytes,
get_version_sort_key,
DEFAULT_BROWSER_USER_AGENT,
parse_size_header as _parse_size_header,
parse_content_range_size as _parse_content_range_size,
extract_response_file_size as _response_file_size,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system.log_funcs import create_module_logger
@@ -582,8 +586,8 @@ def _download_url_looks_like_model_file(
if normalized.startswith(CIVITAI_DOWNLOAD_URL_PREFIXES):
return True
basename = os.path.basename(path).lower()
expected = os.path.basename(str(expected_filename or "")).lower()
basename = get_filename_from_path(path).lower()
expected = get_filename_from_path(str(expected_filename or "")).lower()
if expected and basename == expected:
return True
return _has_known_model_extension(basename)
@@ -628,25 +632,6 @@ def _prepare_size_probe_url(url: Any) -> Optional[str]:
return normalized
def _response_file_size(response: requests.Response) -> Optional[int]:
size = _parse_content_range_size(response.headers.get("Content-Range"))
if size:
return size
for key in (
"x-linked-size",
"x-file-size",
"x-goog-stored-content-length",
"content-length",
):
size = _parse_size_header(response.headers.get(key))
if size:
content_type = (response.headers.get("content-type") or "").lower()
if key == "content-length" and "text/html" in content_type:
continue
return size
return None
def _fetch_remote_file_size_bytes(url: Any, timeout: int = 15) -> Optional[int]:
@@ -1436,24 +1421,6 @@ def _find_model_title_match_in_model_details(
return None
def _parse_size_header(value: Any) -> Optional[int]:
if value is None or value == "":
return None
try:
size = int(str(value).strip())
return size if size > 0 else None
except (TypeError, ValueError):
size = parse_size_to_bytes(value)
return size if size and size > 0 else None
def _parse_content_range_size(value: Any) -> Optional[int]:
if not value:
return None
match = re.search(r"/(\d+)\s*$", str(value))
if not match:
return None
return _parse_size_header(match.group(1))
def _build_result_from_model_details(
@@ -1941,7 +1908,7 @@ def _build_result_from_search_candidate(
parsed: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
candidate_name = _candidate_display_name(candidate)
query_basename = os.path.basename(query or "").strip()
query_basename = get_filename_from_path(query or "").strip()
filename = (
candidate.get("filename")
or candidate.get("fileName")
@@ -2187,7 +2154,7 @@ def _is_hash_verified_exact_match(result: Optional[Dict[str, Any]], confidence:
def _build_search_queries(filename: str) -> List[str]:
basename = os.path.basename(filename or "").strip()
basename = get_filename_from_path(filename or "").strip()
stem = os.path.splitext(basename)[0].strip()
simplified = re.sub(
r"[-_]?(fp16|fp32|fp8|fp4|bf16|e4m3fn|scaled|pruned|emaonly|mixed|q4|q8)$",
@@ -2318,7 +2285,7 @@ def search_civarchive_for_file(
best_rank = -9999.0
seen = set()
allow_model_title_match = not exact_only and not _has_known_model_extension(
os.path.basename(normalized_filename)
get_filename_from_path(normalized_filename)
)
try:
+4 -4
View File
@@ -30,7 +30,7 @@ from ..type_utils import (
DEFAULT_BROWSER_USER_AGENT,
)
from ..progress import report_progress, get_progress_reporter
from ..path_utils import calculate_file_sha256
from ..path_utils import calculate_file_sha256, get_filename_from_path
from ..log_system.log_funcs import create_module_logger
log = create_module_logger(__name__)
@@ -469,7 +469,7 @@ def _find_civitai_file_in_model(
filename_lower = filename.lower()
filename_base = os.path.splitext(filename_lower)[0]
allow_model_title_match = not exact_only and not _has_known_model_extension(
os.path.basename(filename)
get_filename_from_path(filename)
)
def build_result_from_resolved_version(
@@ -1564,7 +1564,7 @@ def _get_metadata_file_path(model_path: str) -> str:
# Get directory and filename
directory = os.path.dirname(model_path)
filename = os.path.basename(model_path)
filename = get_filename_from_path(model_path)
# Try different variations of the metadata file name
base_name = filename.rsplit(".", 1)[0] if "." in filename else filename
@@ -1948,7 +1948,7 @@ def get_model_info_for_file(
log.info(f"Files in {directory}: {metadata_files}")
if local_only:
filename = os.path.basename(file_path)
filename = get_filename_from_path(file_path)
stem = os.path.splitext(filename)[0]
result = {
"source": "local",
+1 -37
View File
@@ -18,7 +18,7 @@ from ..log_system.log_funcs import create_module_logger
log = create_module_logger(__name__)
from ..path_utils import write_json_atomic, METADATA_DIR
from ..type_utils import check_credential_http
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, parse_content_range_size as _parse_content_range_size, extract_response_file_size as _response_file_size
HF_API_URL = "https://huggingface.co/api"
HF_AUTHOR_FALLBACKS = ["Comfy-Org"]
@@ -334,23 +334,6 @@ def get_huggingface_download_url(repo: str, filename: str, branch: str = "main")
return f"https://huggingface.co/{repo}/resolve/{branch}/{quote(filename)}"
def _parse_size_header(value: Any) -> Optional[int]:
if value is None or value == "":
return None
try:
size = int(str(value).strip())
return size if size > 0 else None
except (TypeError, ValueError):
return None
def _parse_content_range_size(value: Any) -> Optional[int]:
if not value:
return None
match = re.search(r"/(\d+)\s*$", str(value))
if not match:
return None
return _parse_size_header(match.group(1))
def _extract_file_info_size(file_info: Dict[str, Any]) -> Optional[int]:
@@ -383,25 +366,6 @@ def _normalize_huggingface_size_probe_url(url: str) -> Optional[str]:
return value
def _response_file_size(response: requests.Response) -> Optional[int]:
size = _parse_content_range_size(response.headers.get("Content-Range"))
if size:
return size
for key in (
"x-linked-size",
"x-file-size",
"x-goog-stored-content-length",
"content-length",
):
size = _parse_size_header(response.headers.get(key))
if size:
content_type = (response.headers.get("content-type") or "").lower()
if key == "content-length" and "text/html" in content_type:
continue
return size
return None
def _fetch_remote_file_size_bytes(
+43
View File
@@ -456,7 +456,50 @@ DEFAULT_BROWSER_USER_AGENT = (
)
def parse_size_header(value: Any) -> Optional[int]:
"""Safely parse a size value (like a header) to bytes with fallback."""
if value is None or value == "":
return None
try:
size = int(str(value).strip())
return size if size > 0 else None
except (TypeError, ValueError):
size = parse_size_to_bytes(value)
return size if size and size > 0 else None
def parse_content_range_size(value: Any) -> Optional[int]:
"""Extract total size from a Content-Range header value."""
if not value:
return None
match = re.search(r"/(\d+)\s*$", str(value))
if not match:
return None
return parse_size_header(match.group(1))
def extract_response_file_size(response: Any) -> Optional[int]:
"""Extract file size from various HTTP response headers."""
if response is None:
return None
# Check Content-Range first
size = parse_content_range_size(response.headers.get("Content-Range"))
if size:
return size
# Try other common size headers
for key in (
"x-linked-size",
"x-file-size",
"x-goog-stored-content-length",
"content-length",
):
size = parse_size_header(response.headers.get(key))
if size:
content_type = (response.headers.get("content-type") or "").lower()
if key == "content-length" and "text/html" in content_type:
continue
return size
return None
+81 -111
View File
@@ -4,6 +4,32 @@ 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";
const SETTINGS_MAP = [
{ serverKey: 'civitai_key', localKey: 'ModelResolver.civitaiApiKey', type: 'string', default: '' },
{ serverKey: 'civitai_session_token', localKey: 'ModelResolver.civitaiSessionToken', type: 'string', default: '' },
{ serverKey: 'civitai_use_trpc_search', localKey: 'ModelResolver.civitaiUseTrpcSearch', type: 'boolean', default: true },
{ serverKey: 'civitai_use_html_fallback', localKey: 'ModelResolver.civitaiUseHtmlFallback', type: 'boolean', default: true },
{ serverKey: 'hf_token', localKey: 'ModelResolver.huggingFaceToken', type: 'string', default: '' },
{ serverKey: 'brave_search_api_key', localKey: 'ModelResolver.braveSearchApiKey', type: 'string', default: '' },
{ serverKey: 'hf_use_api_search', localKey: 'ModelResolver.hfUseApiSearch', type: 'boolean', default: true },
{ serverKey: 'hf_use_comfy_org_fallback', localKey: 'ModelResolver.hfUseComfyOrgFallback', type: 'boolean', default: true },
{ serverKey: 'hf_use_brave_fallback', localKey: 'ModelResolver.hfUseBraveFallback', type: 'boolean', default: true },
{ serverKey: 'auto_fill_base_model', localKey: 'ModelResolver.autoFillBaseModel', type: 'boolean', default: true },
{ serverKey: 'auto_fill_subfolder', localKey: 'ModelResolver.autoFillSubfolder', type: 'boolean', default: true },
{ serverKey: 'auto_refresh_comfy_models_after_apply', localKey: 'ModelResolver.autoRefreshComfyModelsAfterApply', type: 'boolean', default: true },
{ serverKey: 'download_backend', localKey: 'ModelResolver.downloadBackend', type: 'backend', default: 'python' },
{ serverKey: 'aria2c_path', localKey: 'ModelResolver.aria2cPath', type: 'string', default: '' },
{ serverKey: 'aria2_auto_stop_daemon', localKey: 'ModelResolver.aria2AutoStopDaemon', type: 'boolean', default: true },
{ serverKey: 'download_path_mode', localKey: 'ModelResolver.downloadPathMode', type: 'pathMode', default: 'suggested' },
{ serverKey: 'download_path_templates', localKey: 'ModelResolver.downloadPathTemplates', type: 'json', default: {} },
{ serverKey: 'base_model_path_mappings', localKey: 'ModelResolver.baseModelPathMappings', type: 'json', default: {} },
{ serverKey: 'frontend_logs_enabled', localKey: 'ModelResolver.frontendLogsEnabled', type: 'frontendLogsEnabled', default: true },
{ serverKey: 'backend_logs_enabled', localKey: 'ModelResolver.backendLogsEnabled', type: 'backendLogsEnabled', default: true },
{ serverKey: 'frontend_log_level', localKey: 'ModelResolver.frontendLogLevel', type: 'string', default: 'DEBUG' },
{ serverKey: 'backend_log_level', localKey: 'ModelResolver.backendLogLevel', type: 'string', default: 'DEBUG' },
{ serverKey: 'civitai_candidate_limit', localKey: 'ModelResolver.civitaiCandidateLimit', type: 'candidateLimit', default: 5 },
];
export const optionsMethods = {
displayOptions() {
if (!this.contentElement) return;
@@ -2106,35 +2132,22 @@ export const optionsMethods = {
};
// 1. Write to localStorage (fast local cache)
localStorage.setItem('ModelResolver.civitaiApiKey', newSettings.civitai_key);
localStorage.setItem('ModelResolver.civitaiSessionToken', newSettings.civitai_session_token);
localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', newSettings.civitai_use_trpc_search ? 'true' : 'false');
localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', newSettings.civitai_use_html_fallback ? 'true' : 'false');
localStorage.setItem('ModelResolver.huggingFaceToken', newSettings.hf_token);
localStorage.setItem('ModelResolver.braveSearchApiKey', newSettings.brave_search_api_key);
localStorage.setItem('ModelResolver.hfUseApiSearch', newSettings.hf_use_api_search ? 'true' : 'false');
localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', newSettings.hf_use_comfy_org_fallback ? 'true' : 'false');
localStorage.setItem('ModelResolver.hfUseBraveFallback', newSettings.hf_use_brave_fallback ? 'true' : 'false');
localStorage.setItem('ModelResolver.autoFillBaseModel', newSettings.auto_fill_base_model ? 'true' : 'false');
localStorage.setItem('ModelResolver.autoFillSubfolder', newSettings.auto_fill_subfolder ? 'true' : 'false');
localStorage.setItem('ModelResolver.autoRefreshComfyModelsAfterApply', newSettings.auto_refresh_comfy_models_after_apply ? 'true' : 'false');
localStorage.setItem('ModelResolver.downloadBackend', newSettings.download_backend);
localStorage.setItem('ModelResolver.aria2cPath', newSettings.aria2c_path);
localStorage.setItem('ModelResolver.aria2AutoStopDaemon', newSettings.aria2_auto_stop_daemon ? 'true' : 'false');
localStorage.setItem('ModelResolver.downloadPathMode', newSettings.download_path_mode);
localStorage.setItem('ModelResolver.downloadPathTemplates', JSON.stringify(newSettings.download_path_templates));
localStorage.setItem('ModelResolver.baseModelPathMappings', JSON.stringify(newSettings.base_model_path_mappings));
for (const opt of SETTINGS_MAP) {
const val = newSettings[opt.serverKey];
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
localStorage.setItem(opt.localKey, val ? 'true' : 'false');
} else if (opt.type === 'json') {
localStorage.setItem(opt.localKey, JSON.stringify(val));
} else {
localStorage.setItem(opt.localKey, String(val ?? ''));
}
}
defaultRootSelectInputs.forEach((select) => {
const storageKey = select.dataset.storageKey;
if (storageKey) {
localStorage.setItem(storageKey, select.value || '');
}
});
localStorage.setItem('ModelResolver.frontendLogsEnabled', newSettings.frontend_logs_enabled ? 'true' : 'false');
localStorage.setItem('ModelResolver.backendLogsEnabled', newSettings.backend_logs_enabled ? 'true' : 'false');
localStorage.setItem('ModelResolver.frontendLogLevel', newSettings.frontend_log_level);
localStorage.setItem('ModelResolver.backendLogLevel', newSettings.backend_log_level);
localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${civitaiCandidateLimit}`);
Object.entries(sourceEnabled).forEach(([key, val]) => {
localStorage.setItem(key, val ? 'true' : 'false');
});
@@ -2170,45 +2183,35 @@ export const optionsMethods = {
},
getStoredTokens() {
const civitaiCandidateLimitRaw = parseInt(localStorage.getItem('ModelResolver.civitaiCandidateLimit') || '5', 10);
const civitai_candidate_limit = Number.isFinite(civitaiCandidateLimitRaw)
? Math.min(20, Math.max(1, civitaiCandidateLimitRaw))
: 5;
const search_source_enabled = this.getSearchSourceEnabledMap();
const storedFrontendLogsEnabled = localStorage.getItem('ModelResolver.frontendLogsEnabled');
const storedBackendLogsEnabled = localStorage.getItem('ModelResolver.backendLogsEnabled');
const storedFrontendLogLevel = localStorage.getItem('ModelResolver.frontendLogLevel');
const storedBackendLogLevel = localStorage.getItem('ModelResolver.backendLogLevel');
const tokens = {};
for (const opt of SETTINGS_MAP) {
const raw = localStorage.getItem(opt.localKey);
if (opt.type === 'boolean') {
tokens[opt.serverKey] = raw === null ? opt.default : raw !== 'false';
} else if (opt.type === 'json') {
try {
tokens[opt.serverKey] = raw ? JSON.parse(raw) : opt.default;
} catch (e) {
tokens[opt.serverKey] = opt.default;
}
} else if (opt.type === 'backend') {
tokens[opt.serverKey] = this.normalizeDownloadBackend(raw || opt.default);
} else if (opt.type === 'pathMode') {
tokens[opt.serverKey] = this.normalizeDownloadPathMode(raw || opt.default);
} else if (opt.type === 'frontendLogsEnabled') {
tokens[opt.serverKey] = raw === null ? (frontendLogger.enabled !== false) : raw !== 'false';
} else if (opt.type === 'backendLogsEnabled') {
tokens[opt.serverKey] = raw === null ? true : raw !== 'false';
} else if (opt.type === 'candidateLimit') {
const limit = parseInt(raw || String(opt.default), 10);
tokens[opt.serverKey] = Number.isFinite(limit) ? Math.min(20, Math.max(1, limit)) : opt.default;
} else {
tokens[opt.serverKey] = raw || opt.default;
}
}
tokens.search_source_enabled = this.getSearchSourceEnabledMap();
return {
civitai_key: localStorage.getItem('ModelResolver.civitaiApiKey') || '',
civitai_session_token: localStorage.getItem('ModelResolver.civitaiSessionToken') || '',
hf_token: localStorage.getItem('ModelResolver.huggingFaceToken') || '',
brave_search_api_key: localStorage.getItem('ModelResolver.braveSearchApiKey') || '',
civitai_use_trpc_search: localStorage.getItem('ModelResolver.civitaiUseTrpcSearch') !== 'false',
civitai_use_html_fallback: localStorage.getItem('ModelResolver.civitaiUseHtmlFallback') !== 'false',
hf_use_api_search: localStorage.getItem('ModelResolver.hfUseApiSearch') !== 'false',
hf_use_comfy_org_fallback: localStorage.getItem('ModelResolver.hfUseComfyOrgFallback') !== 'false',
hf_use_brave_fallback: localStorage.getItem('ModelResolver.hfUseBraveFallback') !== 'false',
auto_fill_base_model: localStorage.getItem('ModelResolver.autoFillBaseModel') !== 'false',
auto_fill_subfolder: localStorage.getItem('ModelResolver.autoFillSubfolder') !== 'false',
auto_refresh_comfy_models_after_apply: localStorage.getItem('ModelResolver.autoRefreshComfyModelsAfterApply') !== 'false',
frontend_logs_enabled: storedFrontendLogsEnabled === null
? frontendLogger.enabled !== false
: storedFrontendLogsEnabled !== 'false',
backend_logs_enabled: storedBackendLogsEnabled === null
? true
: storedBackendLogsEnabled !== 'false',
frontend_log_level: storedFrontendLogLevel || DEFAULT_FRONTEND_LOG_LEVEL,
backend_log_level: storedBackendLogLevel || 'DEBUG',
civitai_candidate_limit,
search_source_enabled,
download_backend: this.normalizeDownloadBackend(localStorage.getItem('ModelResolver.downloadBackend') || 'python'),
aria2c_path: localStorage.getItem('ModelResolver.aria2cPath') || '',
aria2_auto_stop_daemon: localStorage.getItem('ModelResolver.aria2AutoStopDaemon') !== 'false',
download_path_mode: this.getDownloadPathMode(),
download_path_templates: this.getDownloadPathTemplates(),
base_model_path_mappings: this.getBaseModelPathMappings(),
...tokens,
...this.getDefaultRootSettings()
};
},
@@ -2232,62 +2235,29 @@ export const optionsMethods = {
return;
}
// Helper: write to localStorage only when the value from server is
// non-empty, so we don't overwrite a key the user already has locally.
const sync = (localKey, serverValue) => {
if (serverValue !== undefined && serverValue !== null && serverValue !== '') {
localStorage.setItem(localKey, serverValue);
for (const opt of SETTINGS_MAP) {
const serverValue = data[opt.serverKey];
if (serverValue === undefined) continue;
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
localStorage.setItem(opt.localKey, serverValue ? 'true' : 'false');
} else if (opt.type === 'json') {
localStorage.setItem(opt.localKey, JSON.stringify(serverValue || {}));
} else if (opt.type === 'backend') {
localStorage.setItem(opt.localKey, this.normalizeDownloadBackend(serverValue));
} else if (opt.type === 'pathMode') {
localStorage.setItem(opt.localKey, this.normalizeDownloadPathMode(serverValue));
} else {
if (serverValue !== null && serverValue !== '') {
localStorage.setItem(opt.localKey, String(serverValue));
}
}
};
sync('ModelResolver.civitaiApiKey', data.civitai_key);
sync('ModelResolver.civitaiSessionToken', data.civitai_session_token);
sync('ModelResolver.huggingFaceToken', data.hf_token);
sync('ModelResolver.braveSearchApiKey', data.brave_search_api_key);
if (data.civitai_use_trpc_search !== undefined)
localStorage.setItem('ModelResolver.civitaiUseTrpcSearch', data.civitai_use_trpc_search ? 'true' : 'false');
if (data.civitai_use_html_fallback !== undefined)
localStorage.setItem('ModelResolver.civitaiUseHtmlFallback', data.civitai_use_html_fallback ? 'true' : 'false');
if (data.hf_use_api_search !== undefined)
localStorage.setItem('ModelResolver.hfUseApiSearch', data.hf_use_api_search ? 'true' : 'false');
if (data.hf_use_comfy_org_fallback !== undefined)
localStorage.setItem('ModelResolver.hfUseComfyOrgFallback', data.hf_use_comfy_org_fallback ? 'true' : 'false');
if (data.hf_use_brave_fallback !== undefined)
localStorage.setItem('ModelResolver.hfUseBraveFallback', data.hf_use_brave_fallback ? 'true' : 'false');
if (data.auto_fill_base_model !== undefined)
localStorage.setItem('ModelResolver.autoFillBaseModel', data.auto_fill_base_model ? 'true' : 'false');
if (data.auto_fill_subfolder !== undefined)
localStorage.setItem('ModelResolver.autoFillSubfolder', data.auto_fill_subfolder ? 'true' : 'false');
if (data.auto_refresh_comfy_models_after_apply !== undefined)
localStorage.setItem('ModelResolver.autoRefreshComfyModelsAfterApply', data.auto_refresh_comfy_models_after_apply ? 'true' : 'false');
if (data.download_backend !== undefined)
localStorage.setItem('ModelResolver.downloadBackend', this.normalizeDownloadBackend(data.download_backend));
if (data.aria2c_path !== undefined)
localStorage.setItem('ModelResolver.aria2cPath', String(data.aria2c_path || ''));
if (data.aria2_auto_stop_daemon !== undefined)
localStorage.setItem('ModelResolver.aria2AutoStopDaemon', data.aria2_auto_stop_daemon ? 'true' : 'false');
if (data.download_path_mode !== undefined)
localStorage.setItem('ModelResolver.downloadPathMode', this.normalizeDownloadPathMode(data.download_path_mode));
if (data.download_path_templates !== undefined)
localStorage.setItem('ModelResolver.downloadPathTemplates', JSON.stringify(data.download_path_templates || {}));
if (data.base_model_path_mappings !== undefined)
localStorage.setItem('ModelResolver.baseModelPathMappings', JSON.stringify(data.base_model_path_mappings || {}));
}
this.getDefaultRootCategoryDefinitions().forEach((item) => {
if (data[item.settingKey] !== undefined) {
localStorage.setItem(item.storageKey, String(data[item.settingKey] || ''));
}
});
if (data.civitai_candidate_limit !== undefined)
localStorage.setItem('ModelResolver.civitaiCandidateLimit', `${data.civitai_candidate_limit}`);
if (data.frontend_logs_enabled !== undefined)
localStorage.setItem('ModelResolver.frontendLogsEnabled', data.frontend_logs_enabled ? 'true' : 'false');
if (data.backend_logs_enabled !== undefined)
localStorage.setItem('ModelResolver.backendLogsEnabled', data.backend_logs_enabled ? 'true' : 'false');
if (data.frontend_log_level !== undefined)
localStorage.setItem('ModelResolver.frontendLogLevel', String(data.frontend_log_level || 'DEBUG').toUpperCase());
if (data.backend_log_level !== undefined)
localStorage.setItem('ModelResolver.backendLogLevel', String(data.backend_log_level || 'DEBUG').toUpperCase());
// Source-enabled flags stored as a nested object
if (data.search_source_enabled && typeof data.search_source_enabled === 'object') {