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