From 024dfff0218fd22b29b142e8b737188267eacb81 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 9 Feb 2026 09:53:40 +0800 Subject: [PATCH] feat: add metadata refresh skip paths setting, #790 --- locales/de.json | 9 +++ locales/en.json | 9 +++ locales/es.json | 9 +++ locales/fr.json | 9 +++ locales/he.json | 9 +++ locales/ja.json | 9 +++ locales/ko.json | 9 +++ locales/ru.json | 9 +++ locales/zh-CN.json | 9 +++ locales/zh-TW.json | 9 +++ py/services/settings_manager.py | 53 +++++++++++++ .../bulk_metadata_refresh_use_case.py | 19 ++++- static/js/managers/SettingsManager.js | 76 +++++++++++++++++++ static/js/state/index.js | 1 + .../components/modals/settings_modal.html | 19 +++++ 15 files changed, 257 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index f0e39a6a..f5b13d49 100644 --- a/locales/de.json +++ b/locales/de.json @@ -292,6 +292,15 @@ "saveFailed": "Fehler beim Speichern der Ausschlüsse: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "Metadaten-Aktualisierung: Übersprungene Pfade", + "placeholder": "Beispiel: temp, archived/old, test_models", + "help": "Modelle in diesen Verzeichnispfaden bei der Massenaktualisierung der Metadaten (\"Alle Metadaten abrufen\") überspringen. Geben Sie durch Kommas getrennte relative Ordnerpfade ein.", + "validation": { + "noPaths": "Geben Sie mindestens einen durch Kommas getrennten Pfad ein.", + "saveFailed": "Übersprungene Pfade konnten nicht gespeichert werden: {message}" + } + }, "layoutSettings": { "displayDensity": "Anzeige-Dichte", "displayDensityOptions": { diff --git a/locales/en.json b/locales/en.json index 7ffdf026..cfe28771 100644 --- a/locales/en.json +++ b/locales/en.json @@ -292,6 +292,15 @@ "saveFailed": "Unable to save exclusions: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "Metadata refresh skip paths", + "placeholder": "Example: temp, archived/old, test_models", + "help": "Skip models in these directory paths during bulk metadata refresh (\"Fetch All Metadata\"). Enter relative folder paths separated by commas.", + "validation": { + "noPaths": "Enter at least one path separated by commas.", + "saveFailed": "Unable to save skip paths: {message}" + } + }, "layoutSettings": { "displayDensity": "Display Density", "displayDensityOptions": { diff --git a/locales/es.json b/locales/es.json index 43dce688..9eed4f91 100644 --- a/locales/es.json +++ b/locales/es.json @@ -292,6 +292,15 @@ "saveFailed": "No se pudieron guardar las exclusiones: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "Rutas a omitir en la actualización de metadatos", + "placeholder": "Ejemplo: temp, archived/old, test_models", + "help": "Omitir modelos en estas rutas de directorio durante la actualización masiva de metadatos (\"Obtener todos los metadatos\"). Ingrese rutas de carpetas relativas separadas por comas.", + "validation": { + "noPaths": "Ingrese al menos una ruta separada por comas.", + "saveFailed": "No se pudieron guardar las rutas a omitir: {message}" + } + }, "layoutSettings": { "displayDensity": "Densidad de visualización", "displayDensityOptions": { diff --git a/locales/fr.json b/locales/fr.json index 30b1cc8a..6a85f0c1 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -292,6 +292,15 @@ "saveFailed": "Impossible d'enregistrer les exclusions : {message}" } }, + "metadataRefreshSkipPaths": { + "label": "Chemins à ignorer pour l'actualisation des métadonnées", + "placeholder": "Exemple : temp, archived/old, test_models", + "help": "Ignorer les modèles dans ces chemins de répertoires lors de l'actualisation groupée des métadonnées (\"Récupérer toutes les métadonnées\"). Entrez des chemins de dossiers relatifs séparés par des virgules.", + "validation": { + "noPaths": "Entrez au moins un chemin séparé par des virgules.", + "saveFailed": "Impossible d'enregistrer les chemins à ignorer : {message}" + } + }, "layoutSettings": { "displayDensity": "Densité d'affichage", "displayDensityOptions": { diff --git a/locales/he.json b/locales/he.json index b29d2708..a502036f 100644 --- a/locales/he.json +++ b/locales/he.json @@ -292,6 +292,15 @@ "saveFailed": "לא ניתן לשמור את ההוצאות: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "נתיבים לדילוג ברענון מטא-נתונים", + "placeholder": "דוגמה: temp, archived/old, test_models", + "help": "דלג על מודלים בנתיבי תיקיות אלה בעת רענון מטא-נתונים המוני (\"אחזר את כל המטא-נתונים\"). הזן נתיבי תיקיות יחסיים מופרדים בפסיקים.", + "validation": { + "noPaths": "הזן לפחות נתיב אחד מופרד בפסיקים.", + "saveFailed": "לא ניתן לשמור נתיבי דילוג: {message}" + } + }, "layoutSettings": { "displayDensity": "צפיפות תצוגה", "displayDensityOptions": { diff --git a/locales/ja.json b/locales/ja.json index 7067d4e5..1d7a46d2 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -292,6 +292,15 @@ "saveFailed": "除外設定を保存できませんでした: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "メタデータ更新スキップパス", + "placeholder": "例:temp, archived/old, test_models", + "help": "一括メタデータ更新(「すべてのメタデータを取得」)時にこれらのディレクトリパス内のモデルをスキップします。カンマ区切りで相対フォルダパスを入力してください。", + "validation": { + "noPaths": "カンマで区切って少なくとも1つのパスを入力してください。", + "saveFailed": "スキップパスの保存に失敗しました:{message}" + } + }, "layoutSettings": { "displayDensity": "表示密度", "displayDensityOptions": { diff --git a/locales/ko.json b/locales/ko.json index fe7c1394..13af8831 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -292,6 +292,15 @@ "saveFailed": "제외 항목을 저장할 수 없습니다: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "메타데이터 새로고침 건너뛰기 경로", + "placeholder": "예: temp, archived/old, test_models", + "help": "일괄 메타데이터 새로고침(\"모든 메타데이터 가져오기\") 시 이 디렉터리 경로의 모델을 건너뜁니다. 쉼표로 구분된 상대 폴더 경로를 입력하세요.", + "validation": { + "noPaths": "쉼표로 구분하여 하나 이상의 경로를 입력하세요.", + "saveFailed": "건너뛰기 경로를 저장할 수 없습니다: {message}" + } + }, "layoutSettings": { "displayDensity": "표시 밀도", "displayDensityOptions": { diff --git a/locales/ru.json b/locales/ru.json index a202f4ab..fd83b3a2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -292,6 +292,15 @@ "saveFailed": "Не удалось сохранить исключения: {message}" } }, + "metadataRefreshSkipPaths": { + "label": "Пути для пропуска обновления метаданных", + "placeholder": "Пример: temp, archived/old, test_models", + "help": "Пропускать модели в этих каталогах при массовом обновлении метаданных («Получить все метаданные»). Введите относительные пути папок через запятую.", + "validation": { + "noPaths": "Введите хотя бы один путь, разделённый запятыми.", + "saveFailed": "Не удалось сохранить пути для пропуска: {message}" + } + }, "layoutSettings": { "displayDensity": "Плотность отображения", "displayDensityOptions": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index d9264ab8..288439a4 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -292,6 +292,15 @@ "saveFailed": "无法保存排除项:{message}" } }, + "metadataRefreshSkipPaths": { + "label": "元数据刷新跳过路径", + "placeholder": "示例:temp, archived/old, test_models", + "help": "批量刷新元数据(\"获取全部元数据\")时跳过这些目录路径中的模型。输入以逗号分隔的相对文件夹路径。", + "validation": { + "noPaths": "请输入至少一个路径,以逗号分隔。", + "saveFailed": "无法保存跳过路径:{message}" + } + }, "layoutSettings": { "displayDensity": "显示密度", "displayDensityOptions": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 5f95a310..c67e88b0 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -292,6 +292,15 @@ "saveFailed": "無法儲存排除項目:{message}" } }, + "metadataRefreshSkipPaths": { + "label": "中繼資料重新整理跳過路徑", + "placeholder": "範例:temp, archived/old, test_models", + "help": "批次重新整理中繼資料(「擷取所有中繼資料」)時跳過這些目錄路徑中的模型。輸入以逗號分隔的相對資料夾路徑。", + "validation": { + "noPaths": "請輸入至少一個路徑,以逗號分隔。", + "saveFailed": "無法儲存跳過路徑:{message}" + } + }, "layoutSettings": { "displayDensity": "顯示密度", "displayDensityOptions": { diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 8293bf55..85497e4f 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -69,6 +69,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "model_card_footer_action": "replace_preview", "update_flag_strategy": "same_base", "auto_organize_exclusions": [], + "metadata_refresh_skip_paths": [], } @@ -261,6 +262,17 @@ class SettingsManager: self.settings["auto_organize_exclusions"] = [] inserted_defaults = True + if "metadata_refresh_skip_paths" in self.settings: + normalized_skip_paths = self.normalize_metadata_refresh_skip_paths( + self.settings.get("metadata_refresh_skip_paths") + ) + if normalized_skip_paths != self.settings.get("metadata_refresh_skip_paths"): + self.settings["metadata_refresh_skip_paths"] = normalized_skip_paths + updated_existing = True + else: + self.settings["metadata_refresh_skip_paths"] = [] + inserted_defaults = True + for key, value in defaults.items(): if key == "priority_tags": continue @@ -805,6 +817,7 @@ class SettingsManager: defaults['priority_tags'] = DEFAULT_PRIORITY_TAG_CONFIG.copy() defaults.setdefault('folder_paths', {}) defaults['auto_organize_exclusions'] = [] + defaults['metadata_refresh_skip_paths'] = [] library_name = defaults.get("active_library") or "default" default_library = self._build_library_payload( @@ -876,6 +889,44 @@ class SettingsManager: self._save_settings() return exclusions + def normalize_metadata_refresh_skip_paths(self, value: Any) -> List[str]: + if value is None: + return [] + + if isinstance(value, str): + candidates: Iterable[str] = ( + value.replace("\n", ",").replace(";", ",").split(",") + ) + elif isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)): + candidates = value + else: + return [] + + paths: List[str] = [] + for raw in candidates: + if isinstance(raw, str): + token = raw.replace("\\", "/").strip().strip("/") + if token: + paths.append(token) + + unique_paths: List[str] = [] + seen = set() + for path in paths: + if path not in seen: + seen.add(path) + unique_paths.append(path) + + return unique_paths + + def get_metadata_refresh_skip_paths(self) -> List[str]: + skip_paths = self.normalize_metadata_refresh_skip_paths( + self.settings.get("metadata_refresh_skip_paths") + ) + if skip_paths != self.settings.get("metadata_refresh_skip_paths"): + self.settings["metadata_refresh_skip_paths"] = skip_paths + self._save_settings() + return skip_paths + def get_startup_messages(self) -> List[Dict[str, Any]]: return [message.copy() for message in self._startup_messages] @@ -913,6 +964,8 @@ class SettingsManager: """Set setting value and save""" if key == "auto_organize_exclusions": value = self.normalize_auto_organize_exclusions(value) + elif key == "metadata_refresh_skip_paths": + value = self.normalize_metadata_refresh_skip_paths(value) self.settings[key] = value portable_switch_pending = False if key == "use_portable_settings" and isinstance(value, bool): diff --git a/py/services/use_cases/bulk_metadata_refresh_use_case.py b/py/services/use_cases/bulk_metadata_refresh_use_case.py index 590840ca..51f1fb28 100644 --- a/py/services/use_cases/bulk_metadata_refresh_use_case.py +++ b/py/services/use_cases/bulk_metadata_refresh_use_case.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional, Protocol, Sequence +from typing import Any, Dict, List, Optional, Protocol, Sequence from ..metadata_sync_service import MetadataSyncService from ...utils.metadata_manager import MetadataManager @@ -43,11 +43,13 @@ class BulkMetadataRefreshUseCase: total_models = len(cache.raw_data) enable_metadata_archive_db = self._settings.get("enable_metadata_archive_db", False) + skip_paths = self._settings.get("metadata_refresh_skip_paths", []) to_process: Sequence[Dict[str, Any]] = [ model for model in cache.raw_data if model.get("sha256") and not model.get("skip_metadata_refresh", False) + and not self._is_in_skip_path(model.get("folder", ""), skip_paths) and (not model.get("civitai") or not model["civitai"].get("id")) and not ( # Skip models confirmed not on CivitAI when no need to retry @@ -121,6 +123,21 @@ class BulkMetadataRefreshUseCase: return {"success": True, "message": message, "processed": processed, "updated": success, "total": total_models} + @staticmethod + def _is_in_skip_path(folder: str, skip_paths: List[str]) -> bool: + if not skip_paths or not folder: + return False + normalized = folder.replace("\\", "/").strip("/") + if not normalized: + return False + for sp in skip_paths: + nsp = sp.replace("\\", "/").strip("/") + if not nsp: + continue + if normalized == nsp or normalized.startswith(nsp + "/"): + return True + return False + async def execute_with_error_handling( self, *, diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 8c1da249..d491a8d1 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -133,6 +133,10 @@ export class SettingsManager { backendSettings?.auto_organize_exclusions ?? defaults.auto_organize_exclusions ); + merged.metadata_refresh_skip_paths = this.normalizePatternList( + backendSettings?.metadata_refresh_skip_paths ?? defaults.metadata_refresh_skip_paths + ); + Object.keys(merged).forEach(key => this.backendSettingKeys.add(key)); return merged; @@ -349,6 +353,16 @@ export class SettingsManager { }); } + const metadataRefreshSkipPathsInput = document.getElementById('metadataRefreshSkipPaths'); + if (metadataRefreshSkipPathsInput) { + metadataRefreshSkipPathsInput.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + this.saveMetadataRefreshSkipPaths(); + } + }); + } + this.setupPriorityTagInputs(); this.initialized = true; @@ -410,6 +424,16 @@ export class SettingsManager { autoOrganizeExclusionsError.textContent = ''; } + const metadataRefreshSkipPathsInput = document.getElementById('metadataRefreshSkipPaths'); + if (metadataRefreshSkipPathsInput) { + const skipPaths = this.normalizePatternList(state.global.settings.metadata_refresh_skip_paths); + metadataRefreshSkipPathsInput.value = skipPaths.join(', '); + } + const metadataRefreshSkipPathsError = document.getElementById('metadataRefreshSkipPathsError'); + if (metadataRefreshSkipPathsError) { + metadataRefreshSkipPathsError.textContent = ''; + } + // Set video autoplay on hover setting const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover'); if (autoplayOnHoverCheckbox) { @@ -1721,6 +1745,58 @@ export class SettingsManager { } } + async saveMetadataRefreshSkipPaths() { + const input = document.getElementById('metadataRefreshSkipPaths'); + const errorElement = document.getElementById('metadataRefreshSkipPathsError'); + if (!input) return; + + const normalized = this.normalizePatternList(input.value); + + if (input.value.trim() && normalized.length === 0) { + if (errorElement) { + errorElement.textContent = translate( + 'settings.metadataRefreshSkipPaths.validation.noPaths', + {}, + 'Enter at least one path separated by commas.' + ); + } + return; + } + + const current = this.normalizePatternList(state.global.settings.metadata_refresh_skip_paths); + if (normalized.join('|') === current.join('|')) { + if (errorElement) { + errorElement.textContent = ''; + } + return; + } + + try { + if (errorElement) { + errorElement.textContent = ''; + } + + await this.saveSetting('metadata_refresh_skip_paths', normalized); + input.value = normalized.join(', '); + + showToast( + 'toast.settings.settingsUpdated', + { setting: translate('settings.metadataRefreshSkipPaths.label') }, + 'success' + ); + } catch (error) { + console.error('Failed to save metadata refresh skip paths:', error); + if (errorElement) { + errorElement.textContent = translate( + 'settings.metadataRefreshSkipPaths.validation.saveFailed', + { message: error.message }, + `Unable to save skip paths: ${error.message}` + ); + } + showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); + } + } + async saveInputSetting(elementId, settingKey) { const element = document.getElementById(elementId); if (!element) return; diff --git a/static/js/state/index.js b/static/js/state/index.js index 0a61e7b5..d5efcba4 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -35,6 +35,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({ priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG }, update_flag_strategy: 'same_base', auto_organize_exclusions: [], + metadata_refresh_skip_paths: [], }); export function createDefaultSettings() { diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 3cc8dae9..9026f6e9 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -536,6 +536,25 @@
+
+
+
+ +
+
+
+ {{ t('settings.metadataRefreshSkipPaths.help') }} +
+ +
+
+

{{ t('settings.sections.exampleImages') }}