feat(settings): add auto-organize exclusions

This commit is contained in:
pixelpaws
2025-11-20 16:08:32 +08:00
parent 5093c30c06
commit 07721af87c
21 changed files with 339 additions and 18 deletions

View File

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

View File

@@ -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
@@ -79,9 +80,10 @@ class ModelFileService:
return self.scanner.get_model_roots()
async def auto_organize_models(
self,
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
@@ -100,6 +102,13 @@ class ModelFileService:
# Get all models from cache
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:
@@ -107,7 +116,16 @@ class ModelFileService:
result.operation_type = 'bulk'
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:
@@ -301,10 +319,24 @@ class ModelFileService:
# Normalize paths for comparison
normalized_root = os.path.normpath(root).replace(os.sep, '/')
normalized_file = os.path.normpath(file_path).replace(os.sep, '/')
if normalized_file.startswith(normalized_root):
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,

View File

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

View File

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