refactor: centralize model-extension handling and search-cache clearing

- Add clear_all_search_caches() in core/sources/__init__.py and replace 4
  individual cache-clearing call sites in __init__.py with the new helper.
- Add shared parse_civitai_model_path() in core/type_utils.py and use it
  from both CivitAI and CivArchive URL parsers to remove duplicated regex logic.
- Rename parse_huggingface_url() to get_huggingface_repo_and_file_from_url()
  in core/resolver.py for clarity.
- Add stripModelExtension() and hasModelExtension() utilities (with a single
  MODEL_EXTENSIONS_REGEX) in web/resolver/utils/render_format_methods.js
  and replace repeated inline regex patterns across search_panel.js,
  model_info_methods.js, tabs_loaded_methods.js, and queue_methods.js.
This commit is contained in:
Dariusz L
2026-07-02 03:57:43 +02:00
parent 2d2e8b4621
commit 86bd6c04fc
11 changed files with 78 additions and 42 deletions
+3 -8
View File
@@ -258,6 +258,7 @@ class ModelResolverExtension:
search_lora_manager_archive_for_file,
clear_search_cache as clear_lora_manager_archive_search_cache,
)
from .core.sources import clear_all_search_caches
download_available = True
except ImportError as e:
@@ -3152,10 +3153,7 @@ class ModelResolverExtension:
@json_api_endpoint("Clear search cache")
async def clear_search_cache_route(request):
"""Clear backend search caches after token/settings changes."""
clear_huggingface_search_cache()
clear_civitai_search_cache()
clear_civarchive_search_cache()
clear_lora_manager_archive_search_cache()
clear_all_search_caches()
reload_popular_databases()
reload_model_list()
invalidate_model_files_cache()
@@ -3236,10 +3234,7 @@ class ModelResolverExtension:
async def model_list_update_route(request):
"""Download latest ComfyUI-Manager model-list.json."""
result = await asyncio.to_thread(update_model_list_from_remote)
clear_huggingface_search_cache()
clear_civitai_search_cache()
clear_civarchive_search_cache()
clear_lora_manager_archive_search_cache()
clear_all_search_caches()
self.search_result_timestamps.clear()
return web.json_response(result)
+1 -1
View File
@@ -696,7 +696,7 @@ def extract_workflow_urls(workflow_json: Dict[str, Any]) -> Dict[str, Dict[str,
return url_map
def parse_huggingface_url(url: str) -> Tuple[Optional[str], Optional[str]]:
def get_huggingface_repo_and_file_from_url(url: str) -> Tuple[Optional[str], Optional[str]]:
"""
Extract HuggingFace repo and path from URL.
+15 -1
View File
@@ -12,8 +12,14 @@ from .huggingface import (
search_huggingface,
search_huggingface_for_file,
get_huggingface_download_url,
clear_search_cache as clear_huggingface_search_cache,
)
from .civitai import (
search_civitai,
search_civitai_for_file,
get_civitai_download_url,
clear_search_cache as clear_civitai_search_cache,
)
from .civitai import search_civitai, search_civitai_for_file, get_civitai_download_url
from .civarchive import (
clear_search_cache as clear_civarchive_search_cache,
is_civarchive_available,
@@ -30,6 +36,13 @@ from .lora_manager_archive import (
search_lora_manager_archive_for_file,
)
def clear_all_search_caches() -> None:
"""Clear search caches for all external sources."""
clear_huggingface_search_cache()
clear_civitai_search_cache()
clear_civarchive_search_cache()
clear_lora_manager_archive_search_cache()
__all__ = [
"search_popular_models",
"get_popular_model_url",
@@ -52,4 +65,5 @@ __all__ = [
"is_lora_manager_archive_available",
"search_lora_manager_archive",
"search_lora_manager_archive_for_file",
"clear_all_search_caches",
]
+2 -10
View File
@@ -34,6 +34,7 @@ from ..type_utils import (
parse_size_header as _parse_size_header,
parse_content_range_size as _parse_content_range_size,
extract_response_file_size as _response_file_size,
parse_civitai_model_path,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system.log_funcs import create_module_logger
@@ -509,16 +510,7 @@ def parse_civarchive_url(url: str) -> Optional[Dict[str, Any]]:
if sha_match:
return {"sha256": sha_match.group(1).lower()}
model_match = re.search(r"/models/(\d+)", parsed.path)
if model_match:
result: Dict[str, Any] = {"model_id": int(model_match.group(1))}
query = parse_qs(parsed.query)
version_id = query.get("modelVersionId", [None])[0]
if version_id:
result["version_id"] = _coerce_int(version_id)
return result
return None
return parse_civitai_model_path(parsed.path, parsed.query)
def _extract_sha256(value: str) -> Optional[str]:
+2 -9
View File
@@ -28,6 +28,7 @@ from ..type_utils import (
get_version_sort_key,
check_credential_http,
DEFAULT_BROWSER_USER_AGENT,
parse_civitai_model_path,
)
from ..progress import report_progress, get_progress_reporter
from ..path_utils import calculate_file_sha256, get_filename_from_path
@@ -684,15 +685,7 @@ def parse_civitai_url(url: str) -> Optional[Dict[str, Any]]:
if match:
return {"version_id": int(match.group(1))}
match = re.search(r"/models/(\d+)", parsed.path)
if match:
result = {"model_id": int(match.group(1))}
query = parse_qs(parsed.query)
if "modelVersionId" in query:
result["version_id"] = int(query["modelVersionId"][0])
return result
return None
return parse_civitai_model_path(parsed.path, parsed.query)
def get_civitai_download_url(version_id: int, api_key: Optional[str] = None) -> str:
+23
View File
@@ -503,3 +503,26 @@ def extract_response_file_size(response: Any) -> Optional[int]:
return size
return None
def parse_civitai_model_path(path: str, query_string: str) -> Optional[Dict[str, int]]:
"""
Common helper to parse `/models/{model_id}?modelVersionId={version_id}` URLs
for CivitAI and CivArchive.
"""
import re
from urllib.parse import parse_qs
model_match = re.search(r"/models/(\d+)", path)
if model_match:
result = {"model_id": int(model_match.group(1))}
query = parse_qs(query_string)
version_id_list = query.get("modelVersionId")
if version_id_list:
try:
result["version_id"] = int(version_id_list[0])
except (ValueError, TypeError):
pass
return result
return None
+1 -1
View File
@@ -2296,7 +2296,7 @@ export const queueMethods = {
text = text.split('/').pop() || text;
}
if (stem) {
text = text.replace(/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i, '');
text = this.stripModelExtension(text);
}
return text;
},
+2 -2
View File
@@ -150,7 +150,7 @@ export const searchPanelMethods = {
.split(/[\\/]+/)
.map(part => part.trim())
.filter(Boolean);
const directoryParts = parts.filter(part => !/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i.test(part));
const directoryParts = parts.filter(part => !this.hasModelExtension(part));
for (const part of directoryParts) {
const exact = this.resolveBaseModelAliasExact(part);
@@ -311,7 +311,7 @@ export const searchPanelMethods = {
if (/^urn:/i.test(value.trim())) {
return;
}
if (/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)\b/i.test(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
if (this.hasModelExtension(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
values.push(value);
}
return;
@@ -335,5 +335,26 @@ export const renderFormatMethods = {
normalizePathToBackward(value) {
return String(value || '').trim().replace(/\//g, '\\');
},
stripModelExtension(value) {
return stripModelExtension(value);
},
hasModelExtension(value) {
return hasModelExtension(value);
}
};
export const MODEL_EXTENSIONS_REGEX = /\.(safetensors|ckpt|pt2?|bin|pth|pkl|sft|onnx|gguf)(?:$|[?#])/i;
export function stripModelExtension(value) {
if (!value) return '';
return String(value).replace(MODEL_EXTENSIONS_REGEX, '');
}
export function hasModelExtension(value) {
if (!value) return false;
return MODEL_EXTENSIONS_REGEX.test(value);
}
+3 -3
View File
@@ -1056,7 +1056,7 @@ export const modelInfoMethods = {
}
} catch (e) {
// Fall back to direct search
const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const searchName = this.stripModelExtension(name);
const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`;
window.open(searchUrl, '_blank');
}
@@ -1154,7 +1154,7 @@ export const modelInfoMethods = {
},
createInfoDialog(loraName, modelData) {
const loraDisplayName = loraName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const loraDisplayName = this.stripModelExtension(loraName);
const dialog = document.createElement('div');
dialog.className = 'mr-info-dialog-backdrop';
@@ -1917,7 +1917,7 @@ export const modelInfoMethods = {
buildLocalInfoDialogData(loraName, modelData = {}) {
const filename = loraName || modelData?.filename || modelData?.name || '';
const modelName = String(filename || '').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const modelName = this.stripModelExtension(filename);
const resolvedPath = this.getModelInfoResolvedPath(modelData);
const categoryTypeMap = {
checkpoints: 'checkpoint',
+5 -7
View File
@@ -168,6 +168,8 @@ export const tabsLoadedMethods = {
const activeCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.active.length, 0);
const inactiveCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.inactive.length, 0);
const stripModelExtension = (value) => this.stripModelExtension(value || 'Unknown');
const buildCategoryStrings = (filter) => {
const result = {};
for (const [category, modelsObj] of Object.entries(byCategory)) {
@@ -175,10 +177,7 @@ export const tabsLoadedMethods = {
const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive];
const parts = models.map(model => {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
let name = fullName;
if (fullName.match(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i)) {
name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
}
const name = stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined
? model.strength.toFixed(2)
: '1.00';
@@ -194,7 +193,6 @@ export const tabsLoadedMethods = {
const allString = buildCategoryStrings('all');
const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon');
const stripModelExtension = (value) => String(value || 'Unknown').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const modelToken = (model, category) => {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
const name = stripModelExtension(fullName);
@@ -253,7 +251,7 @@ export const tabsLoadedMethods = {
for (const model of modelsObj.active) {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const name = this.stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(model)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
}
@@ -272,7 +270,7 @@ export const tabsLoadedMethods = {
for (const model of modelsObj.inactive) {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const name = this.stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(model)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
}