diff --git a/__init__.py b/__init__.py index 15eca05..47ce8e1 100644 --- a/__init__.py +++ b/__init__.py @@ -416,6 +416,57 @@ class ModelResolverExtension: "progress_id": job_id, }) + def run_in_background_thread( + tracker, + progress_id, + task_func, + on_success, + on_cancel=None, + on_error=None, + error_log_msg="Background task failed", + ): + def wrapper(): + try: + result = task_func() + if tracker.is_cancelled(progress_id): + if on_cancel: + on_cancel(result) + else: + tracker.update( + progress_id, + status="cancelled", + stage="cancelled", + message="Task cancelled", + ) + return + on_success(result) + except (HashCalculationCancelled, asyncio.CancelledError): + if on_cancel: + on_cancel() + else: + tracker.update( + progress_id, + status="cancelled", + stage="cancelled", + message="Task cancelled", + ) + except Exception as exc: + self.logger.exception(f"{error_log_msg}: {exc}") + if on_error: + on_error(exc) + else: + tracker.update( + progress_id, + status="error", + stage="error", + message=str(exc) or "Task failed", + percent=100, + error=str(exc) or "Task failed", + ) + + import threading + threading.Thread(target=wrapper, daemon=True).start() + async def get_override_settings_from_request(request): settings = await asyncio.to_thread(load_resolver_settings) if request.method == "POST": @@ -1035,57 +1086,29 @@ class ModelResolverExtension: if self.hash_tracker.is_cancelled(progress_id): raise HashCalculationCancelled() - is_safetensors = normalized_path.lower().endswith(".safetensors") - if is_safetensors: - self.logger.info( - f"Checking safetensors header for SHA256: {normalized_path}" - ) - else: - self.logger.info( - "Skipping safetensors header lookup for non-safetensors file; " - f"calculating full SHA256: {normalized_path}" - ) - - header_sha256 = extract_safetensors_header_sha256(normalized_path) - if header_sha256: - self.logger.info( - f"Found SHA256 in safetensors header: {normalized_path}" - ) - self.hash_tracker.update( - progress_id, - status="running", - stage="header", - message="Found SHA256 in safetensors header", - percent=98, - bytes_read=0, - total_bytes=total_bytes, - sha256=header_sha256, - hash=header_sha256, - sha256_source="safetensors_header", - ) - return header_sha256, "safetensors_header" - - if is_safetensors: - self.logger.info( - "No SHA256 found in safetensors header; calculating full " - f"file SHA256: {normalized_path}" - ) - - self.hash_tracker.update( - progress_id, - status="running", - stage="hashing", - message="Calculating SHA256...", - percent=0, - bytes_read=0, - total_bytes=total_bytes, - ) - + hash_source = ["file"] + stage_transitioned = [False] _bytes_read = [0] _last_update = [0.0] def on_progress(bytes_read, total_bytes): _bytes_read[0] = bytes_read + if not stage_transitioned[0]: + stage_transitioned[0] = True + self.logger.info( + "No SHA256 found in safetensors header; calculating full " + f"file SHA256: {normalized_path}" + ) + self.hash_tracker.update( + progress_id, + status="running", + stage="hashing", + message="Calculating SHA256...", + percent=0, + bytes_read=0, + total_bytes=total_bytes, + ) + now = time.time() if now - _last_update[0] >= 0.15 or bytes_read >= total_bytes: percent = 98 if total_bytes <= 0 else min( @@ -1121,14 +1144,36 @@ class ModelResolverExtension: return True return False + def on_hash_source(source_name): + hash_source[0] = source_name + if source_name == "safetensors_header": + self.logger.info( + f"Found SHA256 in safetensors header: {normalized_path}" + ) + self.hash_tracker.update( + progress_id, + status="running", + stage="header", + message="Found SHA256 in safetensors header", + percent=98, + bytes_read=0, + total_bytes=total_bytes, + sha256_source="safetensors_header", + ) + sha256 = _calculate_file_sha256_core( normalized_path, chunk_size=1024 * 1024 * 4, on_progress=on_progress, is_cancelled=is_cancelled, - use_safetensors_header=False, + use_safetensors_header=True, + on_hash_source=on_hash_source, ) - return sha256, "file" + + if is_cancelled(): + raise HashCalculationCancelled() + + return sha256, hash_source[0] @routes.post("/model_resolver/calculate-file-hash") @json_api_endpoint("calculate-file-hash") @@ -1200,57 +1245,58 @@ class ModelResolverExtension: ) def run_hash_task(): - try: - sha256, sha256_source = calculate_sha256_with_progress( - normalized_path, - progress_id=progress_id, - ) - if self.hash_tracker.is_cancelled(progress_id): - raise HashCalculationCancelled() - self.hash_tracker.update( - progress_id, - status="running", - stage="metadata", - message="Saving metadata...", - percent=99, - ) - resolved_metadata_path, metadata_updated = write_calculated_hash_metadata( - normalized_path, - sha256, - sha256_source, - ) - self.hash_tracker.update( - progress_id, - status="done", - stage="done", - message="Hash calculated", - percent=100, - sha256=sha256, - hash=sha256, - sha256_source=sha256_source, - file_path=normalized_path, - metadata_path=resolved_metadata_path, - metadata_updated=metadata_updated, - ) - except HashCalculationCancelled: - self.hash_tracker.update( - progress_id, - status="cancelled", - stage="cancelled", - message="Hash calculation cancelled", - ) - except Exception as exc: - self.logger.exception(f"Hash calculation failed for {normalized_path}: {exc}") - self.hash_tracker.update( - progress_id, - status="error", - stage="error", - message=str(exc) or "Hash calculation failed", - percent=100, - error=str(exc) or "Hash calculation failed", - ) + sha256, sha256_source = calculate_sha256_with_progress( + normalized_path, + progress_id=progress_id, + ) + if self.hash_tracker.is_cancelled(progress_id): + raise HashCalculationCancelled() + self.hash_tracker.update( + progress_id, + status="running", + stage="metadata", + message="Saving metadata...", + percent=99, + ) + resolved_metadata_path, metadata_updated = write_calculated_hash_metadata( + normalized_path, + sha256, + sha256_source, + ) + return sha256, sha256_source, resolved_metadata_path, metadata_updated - threading.Thread(target=run_hash_task, daemon=True).start() + def on_success(res): + sha256, sha256_source, resolved_metadata_path, metadata_updated = res + self.hash_tracker.update( + progress_id, + status="done", + stage="done", + message="Hash calculated", + percent=100, + sha256=sha256, + hash=sha256, + sha256_source=sha256_source, + file_path=normalized_path, + metadata_path=resolved_metadata_path, + metadata_updated=metadata_updated, + ) + + def on_cancel(res=None): + self.hash_tracker.update( + progress_id, + status="cancelled", + stage="cancelled", + message="Hash calculation cancelled", + ) + + run_in_background_thread( + self.hash_tracker, + progress_id, + run_hash_task, + on_success, + on_cancel, + error_log_msg=f"Hash calculation failed for {normalized_path}", + ) return web.json_response( { "success": True, @@ -1372,64 +1418,73 @@ class ModelResolverExtension: return self.metadata_builder_progress.is_cancelled(progress_id) def run_metadata_build_task(): - try: - result = build_missing_local_metadata( - force_rescan=force_rescan, - worker_count=worker_count, - progress_callback=update_metadata_build_progress, - is_cancelled=is_metadata_build_cancelled, - ) - if result.get("cancelled") or is_metadata_build_cancelled(): - self.metadata_builder_progress.update( - progress_id, - status="cancelled", - stage="cancelled", - message="Metadata build cancelled", - percent=100, - active_models=[], - active_worker_count=0, - current_model="", - current_path="", - bytes_read=0, - total_bytes=0, - result=result, - **result, - ) - return + result = build_missing_local_metadata( + force_rescan=force_rescan, + worker_count=worker_count, + progress_callback=update_metadata_build_progress, + is_cancelled=is_metadata_build_cancelled, + ) + return result - self.metadata_builder_progress.update( - progress_id, - status="done", - stage="done", - message="Local metadata build completed.", - percent=100, - active_models=[], - active_worker_count=0, - current_model="", - current_path="", - bytes_read=0, - total_bytes=0, - result=result, - **result, - ) - except Exception as exc: - self.logger.exception(f"Metadata build failed: {exc}") - self.metadata_builder_progress.update( - progress_id, - status="error", - stage="error", - message=str(exc) or "Metadata build failed", - percent=100, - active_models=[], - active_worker_count=0, - current_model="", - current_path="", - bytes_read=0, - total_bytes=0, - error=str(exc) or "Metadata build failed", - ) + def on_success(result): + self.metadata_builder_progress.update( + progress_id, + status="done", + stage="done", + message="Local metadata build completed.", + percent=100, + active_models=[], + active_worker_count=0, + current_model="", + current_path="", + bytes_read=0, + total_bytes=0, + result=result, + **result, + ) - threading.Thread(target=run_metadata_build_task, daemon=True).start() + def on_cancel(result=None): + self.metadata_builder_progress.update( + progress_id, + status="cancelled", + stage="cancelled", + message="Metadata build cancelled", + percent=100, + active_models=[], + active_worker_count=0, + current_model="", + current_path="", + bytes_read=0, + total_bytes=0, + result=result or {}, + **(result or {}), + ) + + def on_error(exc): + self.metadata_builder_progress.update( + progress_id, + status="error", + stage="error", + message=str(exc) or "Metadata build failed", + percent=100, + active_models=[], + active_worker_count=0, + current_model="", + current_path="", + bytes_read=0, + total_bytes=0, + error=str(exc) or "Metadata build failed", + ) + + run_in_background_thread( + self.metadata_builder_progress, + progress_id, + run_metadata_build_task, + on_success, + on_cancel, + on_error, + error_log_msg="Metadata build failed", + ) return web.json_response( { "success": True, diff --git a/core/matcher.py b/core/matcher.py index 1a2b84d..0cee29c 100644 --- a/core/matcher.py +++ b/core/matcher.py @@ -7,7 +7,7 @@ Implements fuzzy string matching to find similar model names. import os import re import heapq -from typing import List, Dict, Tuple, Optional +from typing import List, Dict, Tuple, Optional, Any from difflib import SequenceMatcher from .log_system import create_module_logger @@ -287,7 +287,7 @@ def base_model_score(candidate: str, preferred: Optional[str]) -> float: # ==================== CENTRALIZED MATCHING & CONFIDENCE HELPERS ==================== -from .type_utils import MODEL_EXTENSIONS, PRECISION_FORMAT_SUFFIXES +from .type_utils import MODEL_EXTENSIONS, PRECISION_FORMAT_SUFFIXES, get_version_sort_key MODEL_FILE_EXTENSIONS = MODEL_EXTENSIONS @@ -437,3 +437,68 @@ def build_filename_search_queries(filename: str) -> List[str]: queries.append(query_val) return queries + + +def match_model_by_title_generic( + model_id: int, + title_query: str, + model_name: str, + versions: list, + base_model_context: Optional[str], + get_base_model_fn, + select_file_fn, + build_result_fn, + hydrate_version_fn=None, + log_prefix: str = "ModelResolver", +) -> Optional[Dict[str, Any]]: + """Generic helper to resolve extensionless workflow values by model title across CivitAI and CivArchive.""" + from typing import Callable, Any + + title_confidence = calculate_model_title_confidence(title_query, model_name) + if title_confidence < MODEL_TITLE_MATCH_THRESHOLD: + log.debug( + f"{log_prefix} title candidate rejected: model_id={model_id}, query={title_query}, model_name={model_name}, confidence={title_confidence}" + ) + return None + + if not versions: + return None + + # Sort versions + versions = sorted(versions, key=get_version_sort_key, reverse=True) + rejected_by_base_model = False + + for version in versions: + hydrated_version = hydrate_version_fn(version) if hydrate_version_fn else version + + # Check base model context + if base_model_context: + base_model = get_base_model_fn(hydrated_version) + if not base_model_matches(base_model, base_model_context): + rejected_by_base_model = True + continue + + # Select file + file_info = select_file_fn(hydrated_version) + if not file_info: + continue + + # Build result + result = build_result_fn(hydrated_version, file_info, title_confidence) + if not result: + continue + + result["confidence"] = title_confidence + result["title_confidence"] = title_confidence + + log.info( + f"{log_prefix} model-title match: query={title_query}, model_id={model_id}, model_name={model_name}, version_id={result.get('version_id')}, filename={result.get('filename')}, confidence={title_confidence}, base={result.get('base_model')}" + ) + return result + + if rejected_by_base_model: + log.debug( + f"{log_prefix} title candidate rejected by base model: model_id={model_id}, base={base_model_context}" + ) + + return None diff --git a/core/sources/civarchive.py b/core/sources/civarchive.py index d2be0d6..a0a0754 100644 --- a/core/sources/civarchive.py +++ b/core/sources/civarchive.py @@ -1215,14 +1215,9 @@ def _find_model_title_match_in_model_details( base_model_context: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """For extensionless workflow values, resolve by CivArchive model page title.""" - model_name = model_details.get("name", "") - title_confidence = calculate_model_title_confidence(title_query, model_name) - if title_confidence < MODEL_TITLE_MATCH_THRESHOLD: - log.debug( - f"CivArchive title candidate rejected: model_id={model_id}, query={title_query}, model_name={model_name}, confidence={title_confidence}" - ) - return None + from ..matcher import match_model_by_title_generic + model_name = model_details.get("name", "") versions = [ version for version in (model_details.get("versions") or []) @@ -1234,46 +1229,35 @@ def _find_model_title_match_in_model_details( if not any(_coerce_int(version.get("id")) == selected_id for version in versions): versions.append(selected_version) - if not versions: - return None + def hydrate_version(v): + return _hydrate_civarchive_version_with_files(model_id, v) - versions = sorted(versions, key=get_version_sort_key, reverse=True) - rejected_by_base_model = False + def get_base_model(v): + return v.get("base_model") - for version in versions: - hydrated_version = _hydrate_civarchive_version_with_files(model_id, version) - if base_model_context and not _base_model_matches( - hydrated_version.get("base_model"), base_model_context - ): - rejected_by_base_model = True - continue + def select_file(v): + return _select_primary_model_file(v.get("files") or []) - file_info = _select_primary_model_file(hydrated_version.get("files") or []) - if not file_info: - continue - - result = _build_result_from_normalized_version( + def build_result(v, file_info, confidence): + return _build_result_from_normalized_version( model_details=model_details, - version=hydrated_version, + version=v, file_info=file_info, match_type="model_title", ) - if not result: - continue - result["confidence"] = title_confidence - result["title_confidence"] = title_confidence - log.info( - f"CivArchive model-title match: query={title_query}, model_id={model_id}, model_name={model_name}, version_id={result.get('version_id')}, filename={result.get('filename')}, confidence={title_confidence}, base={result.get('base_model')}" - ) - return result - - if rejected_by_base_model: - log.debug( - f"CivArchive title candidate rejected by base model: model_id={model_id}, base={base_model_context}" - ) - - return None + return match_model_by_title_generic( + model_id=model_id, + title_query=title_query, + model_name=model_name, + versions=versions, + base_model_context=base_model_context, + get_base_model_fn=get_base_model, + select_file_fn=select_file, + build_result_fn=build_result, + hydrate_version_fn=hydrate_version, + log_prefix="CivArchive", + ) diff --git a/core/sources/civitai.py b/core/sources/civitai.py index 5e6ce85..4bb57fc 100644 --- a/core/sources/civitai.py +++ b/core/sources/civitai.py @@ -190,59 +190,46 @@ def _find_model_title_match_in_model( base_model_context: Optional[str] = None, ) -> Optional[Dict[str, Any]]: """For extensionless workflow values, resolve by CivitAI model page title.""" - model_name = model_data.get("name", "") - title_confidence = calculate_model_title_confidence(title_query, model_name) - if title_confidence < MODEL_TITLE_MATCH_THRESHOLD: - log.debug( - f"CivitAI title candidate rejected: model_id={model_id}, query={title_query}, model_name={model_name}, confidence={title_confidence}" - ) - return None + from ..matcher import match_model_by_title_generic + model_name = model_data.get("name", "") versions = [ version for version in (model_data.get("modelVersions") or []) if isinstance(version, dict) ] - if not versions: - return None - if base_model_context: - versions = [ - version - for version in versions - if _base_model_matches(version.get("baseModel"), base_model_context) - ] - if not versions: - log.debug( - f"CivitAI title candidate rejected by base model: model_id={model_id}, base={base_model_context}" - ) - return None + def get_base_model(v): + return v.get("baseModel") - versions = sorted(versions, key=get_version_sort_key, reverse=True) + def select_file(v): + return select_primary_model_file(v.get("files") or []) - for version in versions: - file_info = select_primary_model_file(version.get("files") or []) - if not file_info: - continue - - result = _build_civitai_result_from_version( + def build_result(v, file_info, confidence): + res = _build_civitai_result_from_version( model_id=model_id, model_name=model_name, model_type=model_data.get("type", ""), - version=version, + version=v, file_info=file_info, tags=model_data.get("tags", []), match_type="model_title", ) - result["confidence"] = title_confidence - result["title_confidence"] = title_confidence - result["version_name"] = version.get("name", "") - log.info( - f"CivitAI model-title match: query={title_query}, model_id={model_id}, model_name={model_name}, version_id={result.get('version_id')}, filename={result.get('filename')}, confidence={title_confidence}, base={result.get('base_model')}" - ) - return result + if res: + res["version_name"] = v.get("name", "") + return res - return None + return match_model_by_title_generic( + model_id=model_id, + title_query=title_query, + model_name=model_name, + versions=versions, + base_model_context=base_model_context, + get_base_model_fn=get_base_model, + select_file_fn=select_file, + build_result_fn=build_result, + log_prefix="CivitAI", + ) def _filename_base_partial_match(target_base: str, candidate_base: str) -> bool: diff --git a/metadata/base-models.meta.json b/metadata/base-models.meta.json index 0d22852..aa928e5 100644 --- a/metadata/base-models.meta.json +++ b/metadata/base-models.meta.json @@ -1,10 +1,7 @@ { - "updated_at": "2026-07-12T19:07:42+00:00", - "last_checked_at": "2026-07-12T19:07:42+00:00", + "updated_at": "2026-07-15T11:23:31+00:00", + "last_checked_at": "2026-07-15T11:23:31+00:00", "local_count": 96, - "new_models_added": 2, - "new_models_added_list": [ - "Tripo", - "Hunyuan3D" - ] + "new_models_added": 0, + "new_models_added_list": [] } diff --git a/metadata/model-list.meta.json b/metadata/model-list.meta.json index c6f2dbb..2489dc7 100644 --- a/metadata/model-list.meta.json +++ b/metadata/model-list.meta.json @@ -5,5 +5,5 @@ "sha": "5d9308f50a08cf3cfb47e6a25cc525425714d3ad", "size": 274526, "model_count": 538, - "updated_at": "2026-07-12T19:07:40+00:00" + "updated_at": "2026-07-15T11:23:29+00:00" } diff --git a/tests/test_title_matching_hashing.py b/tests/test_title_matching_hashing.py new file mode 100644 index 0000000..b48f876 --- /dev/null +++ b/tests/test_title_matching_hashing.py @@ -0,0 +1,123 @@ +import sys +import os +import unittest +from unittest.mock import MagicMock, patch, AsyncMock + +parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + +from core.sources.civitai import _find_model_title_match_in_model +from core.sources.civarchive import _find_model_title_match_in_model_details +from core.path_utils import calculate_file_sha256 + +class TestTitleMatchingAndHashing(unittest.IsolatedAsyncioTestCase): + + def test_civitai_find_model_title_match_success(self): + model_data = { + "name": "SDXL Base Model", + "type": "checkpoint", + "tags": ["anime", "stylized"], + "modelVersions": [ + { + "id": 123, + "name": "v1.0", + "baseModel": "SDXL 1.0", + "files": [ + { + "id": 456, + "name": "sdxl_base.safetensors", + "sizeKB": 2048, + "primary": True, + "hashes": {"sha256": "abcdef123456"}, + } + ] + } + ] + } + + # SDXL Base Model vs sdxl base model should be a high confidence match + result = _find_model_title_match_in_model( + model_id=1, + model_data=model_data, + title_query="sdxl base model", + base_model_context="SDXL 1.0" + ) + self.assertIsNotNone(result) + self.assertEqual(result["model_id"], 1) + self.assertEqual(result["version_id"], 123) + self.assertEqual(result["filename"], "sdxl_base.safetensors") + self.assertEqual(result["base_model"], "SDXL 1.0") + self.assertIn("anime", result["tags"]) + + def test_civitai_find_model_title_match_rejected_confidence(self): + model_data = { + "name": "Totally Different Model Name", + "modelVersions": [{"id": 123, "name": "v1.0"}] + } + result = _find_model_title_match_in_model( + model_id=1, + model_data=model_data, + title_query="sdxl base model" + ) + self.assertIsNone(result) + + def test_civarchive_find_model_title_match_success(self): + model_details = { + "model_id": 10, + "name": "SD 1.5 Pruned", + "type": "checkpoint", + "tags": ["base"], + "versions": [ + { + "id": 999, + "name": "v1.5 pruned", + "base_model": "SD 1.5", + "files": [ + { + "name": "v1-5-pruned.safetensors", + "size": 10240, + "download_url": "https://civarchive.com/api/download/models/999", + "primary": True, + } + ] + } + ] + } + + result = _find_model_title_match_in_model_details( + model_id=10, + model_details=model_details, + title_query="SD 1.5 Pruned", + base_model_context="SD 1.5" + ) + self.assertIsNotNone(result) + self.assertEqual(result["model_id"], 10) + self.assertEqual(result["version_id"], 999) + self.assertEqual(result["filename"], "v1-5-pruned.safetensors") + self.assertEqual(result["base_model"], "SD 1.5") + + async def test_calculate_file_sha256_with_mock_progress(self): + import tempfile + on_progress = MagicMock() + on_source = MagicMock() + + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, "dummy.safetensors") + with open(file_path, "wb") as f: + f.write(b"a" * 1024 * 512) # 512 KB + + # Non-safetensors or empty header safetensors + sha256 = calculate_file_sha256( + file_path, + chunk_size=65536, + on_progress=on_progress, + use_safetensors_header=True, + on_hash_source=on_source + ) + self.assertIsNotNone(sha256) + on_progress.assert_called() + on_source.assert_called_with("file") + +if __name__ == "__main__": + unittest.main() diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index 9b334c0..172e828 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -1159,23 +1159,24 @@ export const optionsMethods = { const shortSha = (value) => value ? String(value).slice(0, 8) : ''; - const setModelListBusy = (busy) => { - if (modelListCheckBtn) modelListCheckBtn.disabled = busy; - if (modelListUpdateBtn) modelListUpdateBtn.disabled = busy; + const setControlsBusyState = (buttons, busy, cancelBtn = null) => { + const btnList = Array.isArray(buttons) ? buttons : [buttons]; + btnList.forEach(btn => { + if (btn) btn.disabled = busy; + }); + if (cancelBtn) { + cancelBtn.hidden = !busy; + cancelBtn.disabled = false; + } }; - const setBaseModelsBusy = (busy) => { - if (baseModelsCheckBtn) baseModelsCheckBtn.disabled = busy; - if (baseModelsUpdateBtn) baseModelsUpdateBtn.disabled = busy; - }; + const setModelListBusy = (busy) => setControlsBusyState([modelListCheckBtn, modelListUpdateBtn], busy); - const setHfIndexBusy = (busy) => { - if (hfIndexRefreshBtn) hfIndexRefreshBtn.disabled = busy; - }; + const setBaseModelsBusy = (busy) => setControlsBusyState([baseModelsCheckBtn, baseModelsUpdateBtn], busy); - const setMetadataSizeAuditBusy = (busy) => { - if (metadataSizeAuditBtn) metadataSizeAuditBtn.disabled = busy; - }; + const setHfIndexBusy = (busy) => setControlsBusyState(hfIndexRefreshBtn, busy); + + const setMetadataSizeAuditBusy = (busy) => setControlsBusyState(metadataSizeAuditBtn, busy); const setStatusMode = (element, mode = '') => { if (!element) return; @@ -1188,12 +1189,14 @@ export const optionsMethods = { return Number.isFinite(number) ? number.toLocaleString() : '0'; }; - const setMetadataSizeAuditStatus = (text, mode = '') => { - if (!metadataSizeAuditStatus) return; - metadataSizeAuditStatus.textContent = text; - setStatusMode(metadataSizeAuditStatus, mode); + const setElementStatusText = (element, text, mode = '') => { + if (!element) return; + element.textContent = text; + setStatusMode(element, mode); }; + const setMetadataSizeAuditStatus = (text, mode = '') => setElementStatusText(metadataSizeAuditStatus, text, mode); + const setMetadataSizeSummaryValue = (element, value, mode = '') => { if (!element) return; element.textContent = formatAuditNumber(value); @@ -1394,19 +1397,9 @@ export const optionsMethods = { } }; - const setMetadataBuildBusy = (busy) => { - if (metadataBuildStartBtn) metadataBuildStartBtn.disabled = busy; - if (metadataBuildCancelBtn) { - metadataBuildCancelBtn.hidden = !busy; - metadataBuildCancelBtn.disabled = false; - } - }; + const setMetadataBuildBusy = (busy) => setControlsBusyState(metadataBuildStartBtn, busy, metadataBuildCancelBtn); - const setMetadataBuildStatus = (text, mode = '') => { - if (!metadataBuildStatus) return; - metadataBuildStatus.textContent = text; - setStatusMode(metadataBuildStatus, mode); - }; + const setMetadataBuildStatus = (text, mode = '') => setElementStatusText(metadataBuildStatus, text, mode); const setMetadataBuildSummaryValue = (element, value, mode = '') => { if (!element) return;