mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
feat(settings): add auto-organize exclusions
This commit is contained in:
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "Download-Pfad-Vorlagen",
|
||||
"exampleImages": "Beispielbilder",
|
||||
"updateFlags": "Update-Markierungen",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "Verschiedenes",
|
||||
"metadataArchive": "Metadaten-Archiv-Datenbank",
|
||||
"storageLocation": "Einstellungsort",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "Videos bei Hover automatisch abspielen",
|
||||
"autoplayOnHoverHelp": "Video-Vorschauen nur beim Darüberfahren mit der Maus abspielen"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "Anzeige-Dichte",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "Download Path Templates",
|
||||
"exampleImages": "Example Images",
|
||||
"updateFlags": "Update Flags",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "Misc.",
|
||||
"metadataArchive": "Metadata Archive Database",
|
||||
"storageLocation": "Settings Location",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "Autoplay Videos on Hover",
|
||||
"autoplayOnHoverHelp": "Only play video previews when hovering over them"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "Display Density",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "Plantillas de rutas de descarga",
|
||||
"exampleImages": "Imágenes de ejemplo",
|
||||
"updateFlags": "Indicadores de actualización",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "Varios",
|
||||
"metadataArchive": "Base de datos de archivo de metadatos",
|
||||
"storageLocation": "Ubicación de ajustes",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "Reproducir videos automáticamente al pasar el ratón",
|
||||
"autoplayOnHoverHelp": "Solo reproducir vistas previas de video al pasar el ratón sobre ellas"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "Densidad de visualización",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "Modèles de chemin de téléchargement",
|
||||
"exampleImages": "Images d'exemple",
|
||||
"updateFlags": "Indicateurs de mise à jour",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "Divers",
|
||||
"metadataArchive": "Base de données d'archive des métadonnées",
|
||||
"storageLocation": "Emplacement des paramètres",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "Lecture automatique vidéo au survol",
|
||||
"autoplayOnHoverHelp": "Lire les aperçus vidéo uniquement lors du survol"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "Densité d'affichage",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
"downloadPathTemplates": "תבניות נתיב הורדה",
|
||||
"exampleImages": "תמונות דוגמה",
|
||||
"updateFlags": "תגי עדכון",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "שונות",
|
||||
"metadataArchive": "מסד נתונים של ארכיון מטא-דאטה",
|
||||
"storageLocation": "מיקום ההגדרות",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "נגן וידאו אוטומטית בריחוף",
|
||||
"autoplayOnHoverHelp": "נגן תצוגות מקדימות של וידאו רק בעת ריחוף מעליהן"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "צפיפות תצוגה",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "ダウンロードパステンプレート",
|
||||
"exampleImages": "例画像",
|
||||
"updateFlags": "アップデートフラグ",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "その他",
|
||||
"metadataArchive": "メタデータアーカイブデータベース",
|
||||
"storageLocation": "設定の場所",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "ホバー時に動画を自動再生",
|
||||
"autoplayOnHoverHelp": "動画プレビューはホバー時にのみ再生されます"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "表示密度",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "다운로드 경로 템플릿",
|
||||
"exampleImages": "예시 이미지",
|
||||
"updateFlags": "업데이트 표시",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "기타",
|
||||
"metadataArchive": "메타데이터 아카이브 데이터베이스",
|
||||
"storageLocation": "설정 위치",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "호버 시 비디오 자동 재생",
|
||||
"autoplayOnHoverHelp": "마우스를 올렸을 때만 비디오 미리보기를 재생합니다"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "표시 밀도",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "Шаблоны путей загрузки",
|
||||
"exampleImages": "Примеры изображений",
|
||||
"updateFlags": "Метки обновлений",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "Разное",
|
||||
"metadataArchive": "Архив метаданных",
|
||||
"storageLocation": "Расположение настроек",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "Автовоспроизведение видео при наведении",
|
||||
"autoplayOnHoverHelp": "Воспроизводить превью видео только при наведении курсора"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "Плотность отображения",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "下载路径模板",
|
||||
"exampleImages": "示例图片",
|
||||
"updateFlags": "更新标记",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "元数据归档数据库",
|
||||
"storageLocation": "设置位置",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "悬停时自动播放视频",
|
||||
"autoplayOnHoverHelp": "仅在悬停时播放视频预览"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "显示密度",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"downloadPathTemplates": "下載路徑範本",
|
||||
"exampleImages": "範例圖片",
|
||||
"updateFlags": "更新標記",
|
||||
"autoOrganize": "Auto-organize",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "中繼資料封存資料庫",
|
||||
"storageLocation": "設定位置",
|
||||
@@ -251,6 +252,15 @@
|
||||
"autoplayOnHover": "滑鼠懸停自動播放影片",
|
||||
"autoplayOnHoverHelp": "僅在滑鼠懸停時播放影片預覽"
|
||||
},
|
||||
"autoOrganizeExclusions": {
|
||||
"label": "Auto-organize exclusions",
|
||||
"placeholder": "Example: extras/*, */backups/*; *_temp.safetensors",
|
||||
"help": "Skip moving files that match these wildcard patterns. Separate multiple patterns with commas or semicolons.",
|
||||
"validation": {
|
||||
"noPatterns": "Enter at least one pattern separated by commas or semicolons.",
|
||||
"saveFailed": "Unable to save exclusions: {message}"
|
||||
}
|
||||
},
|
||||
"layoutSettings": {
|
||||
"displayDensity": "顯示密度",
|
||||
"displayDensityOptions": {
|
||||
|
||||
@@ -16,7 +16,7 @@ from ...services.download_coordinator import DownloadCoordinator
|
||||
from ...services.metadata_sync_service import MetadataSyncService
|
||||
from ...services.model_file_service import ModelMoveService
|
||||
from ...services.preview_asset_service import PreviewAssetService
|
||||
from ...services.settings_manager import SettingsManager
|
||||
from ...services.settings_manager import SettingsManager, get_settings_manager
|
||||
from ...services.tag_update_service import TagUpdateService
|
||||
from ...services.use_cases import (
|
||||
AutoOrganizeInProgressError,
|
||||
@@ -1051,16 +1051,23 @@ class ModelAutoOrganizeHandler:
|
||||
async def auto_organize_models(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
file_paths = None
|
||||
exclusion_patterns = None
|
||||
settings_manager = get_settings_manager()
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = await request.json()
|
||||
file_paths = data.get("file_paths")
|
||||
if "exclusion_patterns" in data:
|
||||
exclusion_patterns = settings_manager.normalize_auto_organize_exclusions(
|
||||
data.get("exclusion_patterns")
|
||||
)
|
||||
except Exception: # pragma: no cover - permissive path
|
||||
pass
|
||||
|
||||
result = await self._use_case.execute(
|
||||
file_paths=file_paths,
|
||||
progress_callback=self._progress_callback,
|
||||
exclusion_patterns=exclusion_patterns,
|
||||
)
|
||||
return web.json_response(result.to_dict())
|
||||
except AutoOrganizeInProgressError:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
import fnmatch
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Any, Set
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ..utils.utils import calculate_relative_path_for_model, remove_empty_dirs
|
||||
@@ -81,7 +82,8 @@ class ModelFileService:
|
||||
async def auto_organize_models(
|
||||
self,
|
||||
file_paths: Optional[List[str]] = None,
|
||||
progress_callback: Optional[ProgressCallback] = None
|
||||
progress_callback: Optional[ProgressCallback] = None,
|
||||
exclusion_patterns: Optional[Sequence[str]] = None,
|
||||
) -> AutoOrganizeResult:
|
||||
"""Auto-organize models based on current settings
|
||||
|
||||
@@ -101,6 +103,13 @@ class ModelFileService:
|
||||
cache = await self.scanner.get_cached_data()
|
||||
all_models = cache.raw_data
|
||||
|
||||
settings_manager = get_settings_manager()
|
||||
normalized_exclusions = settings_manager.normalize_auto_organize_exclusions(
|
||||
exclusion_patterns
|
||||
if exclusion_patterns is not None
|
||||
else settings_manager.get_auto_organize_exclusions()
|
||||
)
|
||||
|
||||
# Filter models if specific file paths are provided
|
||||
if file_paths:
|
||||
all_models = [model for model in all_models if model.get('file_path') in file_paths]
|
||||
@@ -108,6 +117,15 @@ class ModelFileService:
|
||||
else:
|
||||
result.operation_type = 'all'
|
||||
|
||||
if normalized_exclusions:
|
||||
all_models = [
|
||||
model
|
||||
for model in all_models
|
||||
if not self._should_exclude_model(
|
||||
model.get('file_path'), normalized_exclusions
|
||||
)
|
||||
]
|
||||
|
||||
# Get model roots for this scanner
|
||||
model_roots = self.get_model_roots()
|
||||
if not model_roots:
|
||||
@@ -306,6 +324,20 @@ class ModelFileService:
|
||||
return root
|
||||
return None
|
||||
|
||||
def _should_exclude_model(
|
||||
self, file_path: Optional[str], patterns: Sequence[str]
|
||||
) -> bool:
|
||||
if not file_path or not patterns:
|
||||
return False
|
||||
|
||||
normalized_path = os.path.normpath(file_path).replace(os.sep, '/')
|
||||
filename = os.path.basename(normalized_path)
|
||||
|
||||
for pattern in patterns:
|
||||
if fnmatch.fnmatch(filename, pattern) or fnmatch.fnmatch(normalized_path, pattern):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _calculate_target_directory(
|
||||
self,
|
||||
model: Dict[str, Any],
|
||||
|
||||
@@ -62,6 +62,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"model_name_display": "model_name",
|
||||
"model_card_footer_action": "example_images",
|
||||
"update_flag_strategy": "same_base",
|
||||
"auto_organize_exclusions": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +240,17 @@ class SettingsManager:
|
||||
)
|
||||
inserted_defaults = True
|
||||
|
||||
if "auto_organize_exclusions" in self.settings:
|
||||
normalized_exclusions = self.normalize_auto_organize_exclusions(
|
||||
self.settings.get("auto_organize_exclusions")
|
||||
)
|
||||
if normalized_exclusions != self.settings.get("auto_organize_exclusions"):
|
||||
self.settings["auto_organize_exclusions"] = normalized_exclusions
|
||||
updated_existing = True
|
||||
else:
|
||||
self.settings["auto_organize_exclusions"] = []
|
||||
inserted_defaults = True
|
||||
|
||||
for key, value in defaults.items():
|
||||
if key == "priority_tags":
|
||||
continue
|
||||
@@ -719,6 +731,7 @@ class SettingsManager:
|
||||
defaults['download_path_templates'] = {}
|
||||
defaults['priority_tags'] = DEFAULT_PRIORITY_TAG_CONFIG.copy()
|
||||
defaults.setdefault('folder_paths', {})
|
||||
defaults['auto_organize_exclusions'] = []
|
||||
|
||||
library_name = defaults.get("active_library") or "default"
|
||||
default_library = self._build_library_payload(
|
||||
@@ -744,6 +757,35 @@ class SettingsManager:
|
||||
|
||||
return normalized
|
||||
|
||||
def normalize_auto_organize_exclusions(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 []
|
||||
|
||||
patterns: List[str] = []
|
||||
for raw in candidates:
|
||||
if isinstance(raw, str):
|
||||
token = raw.strip()
|
||||
if token:
|
||||
patterns.append(token)
|
||||
|
||||
unique_patterns: List[str] = []
|
||||
seen = set()
|
||||
for pattern in patterns:
|
||||
if pattern not in seen:
|
||||
seen.add(pattern)
|
||||
unique_patterns.append(pattern)
|
||||
|
||||
return unique_patterns
|
||||
|
||||
def get_priority_tag_config(self) -> Dict[str, str]:
|
||||
stored_value = self.settings.get("priority_tags")
|
||||
normalized = self._normalize_priority_tag_config(stored_value)
|
||||
@@ -752,6 +794,15 @@ class SettingsManager:
|
||||
self._save_settings()
|
||||
return normalized.copy()
|
||||
|
||||
def get_auto_organize_exclusions(self) -> List[str]:
|
||||
exclusions = self.normalize_auto_organize_exclusions(
|
||||
self.settings.get("auto_organize_exclusions")
|
||||
)
|
||||
if exclusions != self.settings.get("auto_organize_exclusions"):
|
||||
self.settings["auto_organize_exclusions"] = exclusions
|
||||
self._save_settings()
|
||||
return exclusions
|
||||
|
||||
def get_startup_messages(self) -> List[Dict[str, Any]]:
|
||||
return [message.copy() for message in self._startup_messages]
|
||||
|
||||
@@ -787,6 +838,8 @@ class SettingsManager:
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""Set setting value and save"""
|
||||
if key == "auto_organize_exclusions":
|
||||
value = self.normalize_auto_organize_exclusions(value)
|
||||
self.settings[key] = value
|
||||
portable_switch_pending = False
|
||||
if key == "use_portable_settings" and isinstance(value, bool):
|
||||
|
||||
@@ -39,6 +39,7 @@ class AutoOrganizeUseCase:
|
||||
*,
|
||||
file_paths: Optional[Sequence[str]] = None,
|
||||
progress_callback: Optional[ProgressCallback] = None,
|
||||
exclusion_patterns: Optional[Sequence[str]] = None,
|
||||
) -> AutoOrganizeResult:
|
||||
"""Run the auto-organize routine guarded by a shared lock."""
|
||||
|
||||
@@ -53,4 +54,5 @@ class AutoOrganizeUseCase:
|
||||
return await self._file_service.auto_organize_models(
|
||||
file_paths=list(file_paths) if file_paths is not None else None,
|
||||
progress_callback=progress_callback,
|
||||
exclusion_patterns=exclusion_patterns,
|
||||
)
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"C:/path/to/your/embeddings_folder",
|
||||
"C:/path/to/another/embeddings_folder"
|
||||
]
|
||||
}
|
||||
},
|
||||
"auto_organize_exclusions": []
|
||||
}
|
||||
|
||||
@@ -1252,14 +1252,23 @@ export class BaseModelApiClient {
|
||||
|
||||
// Start the auto-organize operation
|
||||
const endpoint = this.apiConfig.endpoints.autoOrganize;
|
||||
const requestOptions = {
|
||||
method: filePaths ? 'POST' : 'GET',
|
||||
headers: filePaths ? { 'Content-Type': 'application/json' } : {}
|
||||
};
|
||||
const exclusionPatterns = (state.global.settings.auto_organize_exclusions || [])
|
||||
.filter(pattern => typeof pattern === 'string' && pattern.trim())
|
||||
.map(pattern => pattern.trim());
|
||||
|
||||
const requestBody = {};
|
||||
if (filePaths) {
|
||||
requestOptions.body = JSON.stringify({ file_paths: filePaths });
|
||||
requestBody.file_paths = filePaths;
|
||||
}
|
||||
if (exclusionPatterns.length > 0) {
|
||||
requestBody.exclusion_patterns = exclusionPatterns;
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody),
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, requestOptions);
|
||||
|
||||
|
||||
@@ -131,11 +131,36 @@ export class SettingsManager {
|
||||
}
|
||||
merged.priority_tags = normalizedPriority;
|
||||
|
||||
merged.auto_organize_exclusions = this.normalizePatternList(
|
||||
backendSettings?.auto_organize_exclusions ?? defaults.auto_organize_exclusions
|
||||
);
|
||||
|
||||
Object.keys(merged).forEach(key => this.backendSettingKeys.add(key));
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
normalizePatternList(value) {
|
||||
if (Array.isArray(value)) {
|
||||
const sanitized = value
|
||||
.map(item => typeof item === 'string' ? item.trim() : '')
|
||||
.filter(Boolean);
|
||||
return [...new Set(sanitized)];
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const sanitized = value
|
||||
.replace(/\n/g, ',')
|
||||
.replace(/;/g, ',')
|
||||
.split(',')
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean);
|
||||
return [...new Set(sanitized)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
registerStartupMessages(messages = []) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return;
|
||||
@@ -376,6 +401,16 @@ export class SettingsManager {
|
||||
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
|
||||
}
|
||||
|
||||
const autoOrganizeExclusionsInput = document.getElementById('autoOrganizeExclusions');
|
||||
if (autoOrganizeExclusionsInput) {
|
||||
const patterns = this.normalizePatternList(state.global.settings.auto_organize_exclusions);
|
||||
autoOrganizeExclusionsInput.value = patterns.join(', ');
|
||||
}
|
||||
const autoOrganizeExclusionsError = document.getElementById('autoOrganizeExclusionsError');
|
||||
if (autoOrganizeExclusionsError) {
|
||||
autoOrganizeExclusionsError.textContent = '';
|
||||
}
|
||||
|
||||
// Set video autoplay on hover setting
|
||||
const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover');
|
||||
if (autoplayOnHoverCheckbox) {
|
||||
@@ -1593,6 +1628,58 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
async saveAutoOrganizeExclusions() {
|
||||
const input = document.getElementById('autoOrganizeExclusions');
|
||||
const errorElement = document.getElementById('autoOrganizeExclusionsError');
|
||||
if (!input) return;
|
||||
|
||||
const normalized = this.normalizePatternList(input.value);
|
||||
|
||||
if (input.value.trim() && normalized.length === 0) {
|
||||
if (errorElement) {
|
||||
errorElement.textContent = translate(
|
||||
'settings.autoOrganizeExclusions.validation.noPatterns',
|
||||
{},
|
||||
'Enter at least one pattern separated by commas or semicolons.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.normalizePatternList(state.global.settings.auto_organize_exclusions);
|
||||
if (normalized.join('|') === current.join('|')) {
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
|
||||
await this.saveSetting('auto_organize_exclusions', normalized);
|
||||
input.value = normalized.join(', ');
|
||||
|
||||
showToast(
|
||||
'toast.settings.settingsUpdated',
|
||||
{ setting: translate('settings.autoOrganizeExclusions.label') },
|
||||
'success'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save auto-organize exclusions:', error);
|
||||
if (errorElement) {
|
||||
errorElement.textContent = translate(
|
||||
'settings.autoOrganizeExclusions.validation.saveFailed',
|
||||
{ message: error.message },
|
||||
`Unable to save exclusions: ${error.message}`
|
||||
);
|
||||
}
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async saveInputSetting(elementId, settingKey) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
@@ -34,6 +34,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
update_flag_strategy: 'same_base',
|
||||
auto_organize_exclusions: [],
|
||||
});
|
||||
|
||||
export function createDefaultSettings() {
|
||||
|
||||
@@ -341,6 +341,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.sections.autoOrganize') }}</h3>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="autoOrganizeExclusions">{{ t('settings.autoOrganizeExclusions.label') }}</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<textarea
|
||||
id="autoOrganizeExclusions"
|
||||
rows="3"
|
||||
placeholder="{{ t('settings.autoOrganizeExclusions.placeholder') }}"
|
||||
onblur="settingsManager.saveAutoOrganizeExclusions()"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
{{ t('settings.autoOrganizeExclusions.help') }}
|
||||
</div>
|
||||
<div class="settings-input-error-message" id="autoOrganizeExclusionsError"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Path Customization Section -->
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.downloadPathTemplates.title') }}</h3>
|
||||
|
||||
@@ -540,7 +540,7 @@ def test_auto_organize_progress_returns_latest_snapshot(mock_service):
|
||||
|
||||
|
||||
def test_auto_organize_route_emits_progress(mock_service, monkeypatch: pytest.MonkeyPatch):
|
||||
async def fake_auto_organize(self, file_paths=None, progress_callback=None):
|
||||
async def fake_auto_organize(self, file_paths=None, progress_callback=None, exclusion_patterns=None):
|
||||
result = AutoOrganizeResult()
|
||||
result.total = 1
|
||||
result.processed = 1
|
||||
|
||||
@@ -53,10 +53,15 @@ class StubFileService:
|
||||
*,
|
||||
file_paths: Optional[List[str]] = None,
|
||||
progress_callback=None,
|
||||
exclusion_patterns=None,
|
||||
) -> AutoOrganizeResult:
|
||||
result = AutoOrganizeResult()
|
||||
result.total = len(file_paths or [])
|
||||
self.calls.append({"file_paths": file_paths, "progress_callback": progress_callback})
|
||||
self.calls.append({
|
||||
"file_paths": file_paths,
|
||||
"progress_callback": progress_callback,
|
||||
"exclusion_patterns": exclusion_patterns,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@@ -144,6 +149,7 @@ async def test_auto_organize_use_case_executes_with_lock() -> None:
|
||||
|
||||
assert isinstance(result, AutoOrganizeResult)
|
||||
assert file_service.calls[0]["file_paths"] == ["model1"]
|
||||
assert file_service.calls[0]["exclusion_patterns"] is None
|
||||
|
||||
|
||||
async def test_auto_organize_use_case_rejects_when_running() -> None:
|
||||
|
||||
Reference in New Issue
Block a user