diff --git a/__init__.py b/__init__.py index b14464b..e937ef8 100644 --- a/__init__.py +++ b/__init__.py @@ -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 "" diff --git a/core/downloader.py b/core/downloader.py index 01a7d79..94ace57 100644 --- a/core/downloader.py +++ b/core/downloader.py @@ -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) diff --git a/core/resolver.py b/core/resolver.py index 4689dfe..da013c7 100644 --- a/core/resolver.py +++ b/core/resolver.py @@ -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: diff --git a/core/sources/civarchive.py b/core/sources/civarchive.py index 33a9036..b9e24a8 100644 --- a/core/sources/civarchive.py +++ b/core/sources/civarchive.py @@ -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: diff --git a/core/sources/civitai.py b/core/sources/civitai.py index c130f22..232cf18 100644 --- a/core/sources/civitai.py +++ b/core/sources/civitai.py @@ -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", diff --git a/core/sources/huggingface.py b/core/sources/huggingface.py index 31d94f5..3e790b9 100644 --- a/core/sources/huggingface.py +++ b/core/sources/huggingface.py @@ -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( diff --git a/core/type_utils.py b/core/type_utils.py index 804016c..8c843f6 100644 --- a/core/type_utils.py +++ b/core/type_utils.py @@ -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 diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index 5602fbf..b56b8c4 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -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') {