feat: add metadata refresh skip paths setting, #790

This commit is contained in:
Will Miao
2026-02-09 09:53:40 +08:00
parent a13fbbff48
commit 024dfff021
15 changed files with 257 additions and 1 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "לא ניתן לשמור את ההוצאות: {message}"
}
},
"metadataRefreshSkipPaths": {
"label": "נתיבים לדילוג ברענון מטא-נתונים",
"placeholder": "דוגמה: temp, archived/old, test_models",
"help": "דלג על מודלים בנתיבי תיקיות אלה בעת רענון מטא-נתונים המוני (\"אחזר את כל המטא-נתונים\"). הזן נתיבי תיקיות יחסיים מופרדים בפסיקים.",
"validation": {
"noPaths": "הזן לפחות נתיב אחד מופרד בפסיקים.",
"saveFailed": "לא ניתן לשמור נתיבי דילוג: {message}"
}
},
"layoutSettings": {
"displayDensity": "צפיפות תצוגה",
"displayDensityOptions": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "除外設定を保存できませんでした: {message}"
}
},
"metadataRefreshSkipPaths": {
"label": "メタデータ更新スキップパス",
"placeholder": "例temp, archived/old, test_models",
"help": "一括メタデータ更新(「すべてのメタデータを取得」)時にこれらのディレクトリパス内のモデルをスキップします。カンマ区切りで相対フォルダパスを入力してください。",
"validation": {
"noPaths": "カンマで区切って少なくとも1つのパスを入力してください。",
"saveFailed": "スキップパスの保存に失敗しました:{message}"
}
},
"layoutSettings": {
"displayDensity": "表示密度",
"displayDensityOptions": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "제외 항목을 저장할 수 없습니다: {message}"
}
},
"metadataRefreshSkipPaths": {
"label": "메타데이터 새로고침 건너뛰기 경로",
"placeholder": "예: temp, archived/old, test_models",
"help": "일괄 메타데이터 새로고침(\"모든 메타데이터 가져오기\") 시 이 디렉터리 경로의 모델을 건너뜁니다. 쉼표로 구분된 상대 폴더 경로를 입력하세요.",
"validation": {
"noPaths": "쉼표로 구분하여 하나 이상의 경로를 입력하세요.",
"saveFailed": "건너뛰기 경로를 저장할 수 없습니다: {message}"
}
},
"layoutSettings": {
"displayDensity": "표시 밀도",
"displayDensityOptions": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "Не удалось сохранить исключения: {message}"
}
},
"metadataRefreshSkipPaths": {
"label": "Пути для пропуска обновления метаданных",
"placeholder": "Пример: temp, archived/old, test_models",
"help": "Пропускать модели в этих каталогах при массовом обновлении метаданных («Получить все метаданные»). Введите относительные пути папок через запятую.",
"validation": {
"noPaths": "Введите хотя бы один путь, разделённый запятыми.",
"saveFailed": "Не удалось сохранить пути для пропуска: {message}"
}
},
"layoutSettings": {
"displayDensity": "Плотность отображения",
"displayDensityOptions": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "无法保存排除项:{message}"
}
},
"metadataRefreshSkipPaths": {
"label": "元数据刷新跳过路径",
"placeholder": "示例temp, archived/old, test_models",
"help": "批量刷新元数据(\"获取全部元数据\")时跳过这些目录路径中的模型。输入以逗号分隔的相对文件夹路径。",
"validation": {
"noPaths": "请输入至少一个路径,以逗号分隔。",
"saveFailed": "无法保存跳过路径:{message}"
}
},
"layoutSettings": {
"displayDensity": "显示密度",
"displayDensityOptions": {

View File

@@ -292,6 +292,15 @@
"saveFailed": "無法儲存排除項目:{message}"
}
},
"metadataRefreshSkipPaths": {
"label": "中繼資料重新整理跳過路徑",
"placeholder": "範例temp, archived/old, test_models",
"help": "批次重新整理中繼資料(「擷取所有中繼資料」)時跳過這些目錄路徑中的模型。輸入以逗號分隔的相對資料夾路徑。",
"validation": {
"noPaths": "請輸入至少一個路徑,以逗號分隔。",
"saveFailed": "無法儲存跳過路徑:{message}"
}
},
"layoutSettings": {
"displayDensity": "顯示密度",
"displayDensityOptions": {

View File

@@ -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):

View File

@@ -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,
*,

View File

@@ -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;

View File

@@ -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() {

View File

@@ -536,6 +536,25 @@
<div class="settings-input-error-message" id="autoOrganizeExclusionsError"></div>
</div>
<div class="setting-item priority-tags-item auto-organize-exclusions-item">
<div class="setting-row priority-tags-header">
<div class="setting-info priority-tags-info">
<label>{{ t('settings.metadataRefreshSkipPaths.label') }}</label>
</div>
</div>
<div class="input-help">
{{ t('settings.metadataRefreshSkipPaths.help') }}
</div>
<textarea
id="metadataRefreshSkipPaths"
class="priority-tags-input auto-organize-exclusions-input"
rows="3"
placeholder="{{ t('settings.metadataRefreshSkipPaths.placeholder') }}"
onblur="settingsManager.saveMetadataRefreshSkipPaths()"
></textarea>
<div class="settings-input-error-message" id="metadataRefreshSkipPathsError"></div>
</div>
<!-- Add Example Images Settings Section -->
<div class="settings-section">
<h3>{{ t('settings.sections.exampleImages') }}</h3>