From f351a68ebe62e78f1310491f4771c935cf01a34a Mon Sep 17 00:00:00 2001 From: Rakly3 Date: Sun, 16 Nov 2025 09:36:18 +0100 Subject: [PATCH] feat(metadata-archive): add custom database path functionality and update path handling Implement the ability to specify a custom path for the metadata archive database across multiple locales. This includes updating the database path, moving the database if necessary, and providing user feedback through the UI. The changes enhance the flexibility of database management for users. --- .gitignore | 1 + locales/de.json | 10 +- locales/en.json | 10 +- locales/es.json | 10 +- locales/fr.json | 10 +- locales/he.json | 10 +- locales/ja.json | 10 +- locales/ko.json | 10 +- locales/ru.json | 10 +- locales/zh-CN.json | 10 +- locales/zh-TW.json | 10 +- py/routes/handlers/misc_handlers.py | 111 ++++++++++++- py/routes/misc_route_registrar.py | 1 + py/services/metadata_archive_manager.py | 152 ++++++++++++++++-- py/services/metadata_service.py | 10 +- py/services/settings_manager.py | 1 + static/js/managers/SettingsManager.js | 101 ++++++++++++ static/js/state/index.js | 1 + .../components/modals/settings_modal.html | 23 +++ 19 files changed, 469 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index bdf79ac6..38b87168 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules/ coverage/ .coverage model_cache/ +venv/ diff --git a/locales/de.json b/locales/de.json index a35c06e8..907fcdef 100644 --- a/locales/de.json +++ b/locales/de.json @@ -386,7 +386,15 @@ "preparing": "Download wird vorbereitet...", "connecting": "Verbindung zum Download-Server wird hergestellt...", "completed": "Abgeschlossen", - "downloadComplete": "Download erfolgreich abgeschlossen" + "downloadComplete": "Download erfolgreich abgeschlossen", + "customPath": "Benutzerdefinierter Datenbankpfad", + "customPathPlaceholder": "Leer lassen, um den Standardspeicherort zu verwenden", + "customPathHelp": "Geben Sie einen benutzerdefinierten Speicherort für die Metadaten-Archiv-Datenbankdatei an. Wenn eine Datenbank bereits existiert und Sie diesen Pfad ändern, wird sie automatisch an den neuen Speicherort verschoben.", + "path": "Datenbankpfad", + "pathUpdated": "Datenbankpfad erfolgreich aktualisiert", + "pathUpdatedAndMoved": "Datenbankpfad aktualisiert und Datenbank erfolgreich verschoben", + "pathUpdateError": "Fehler beim Aktualisieren des Datenbankpfads", + "movingDatabase": "Datenbank wird verschoben..." }, "proxySettings": { "enableProxy": "App-Proxy aktivieren", diff --git a/locales/en.json b/locales/en.json index 95be0d09..e24e632c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -385,7 +385,15 @@ "preparing": "Preparing download...", "connecting": "Connecting to download server...", "completed": "Completed", - "downloadComplete": "Download completed successfully" + "downloadComplete": "Download completed successfully", + "customPath": "Custom Database Path", + "customPathPlaceholder": "Leave empty to use default location", + "customPathHelp": "Specify a custom location for the metadata archive database file. If a database already exists and you change this path, it will be automatically moved to the new location.", + "path": "Database Path", + "pathUpdated": "Database path updated successfully", + "pathUpdatedAndMoved": "Database path updated and database moved successfully", + "pathUpdateError": "Failed to update database path", + "movingDatabase": "Moving Database..." }, "proxySettings": { "enableProxy": "Enable App-level Proxy", diff --git a/locales/es.json b/locales/es.json index 9596613e..eede5c15 100644 --- a/locales/es.json +++ b/locales/es.json @@ -385,7 +385,15 @@ "preparing": "Preparando descarga...", "connecting": "Conectando al servidor de descarga...", "completed": "Completado", - "downloadComplete": "Descarga completada exitosamente" + "downloadComplete": "Descarga completada exitosamente", + "customPath": "Ruta personalizada de la base de datos", + "customPathPlaceholder": "Dejar vacío para usar la ubicación predeterminada", + "customPathHelp": "Especifica una ubicación personalizada para el archivo de base de datos del archivo de metadatos. Si ya existe una base de datos y cambias esta ruta, se moverá automáticamente a la nueva ubicación.", + "path": "Ruta de la base de datos", + "pathUpdated": "Ruta de la base de datos actualizada exitosamente", + "pathUpdatedAndMoved": "Ruta de la base de datos actualizada y base de datos movida exitosamente", + "pathUpdateError": "Error al actualizar la ruta de la base de datos", + "movingDatabase": "Moviendo base de datos..." }, "proxySettings": { "enableProxy": "Habilitar proxy a nivel de aplicación", diff --git a/locales/fr.json b/locales/fr.json index 5d0d35a2..af1f3704 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -365,7 +365,15 @@ "preparing": "Préparation du téléchargement...", "connecting": "Connexion au serveur de téléchargement...", "completed": "Terminé", - "downloadComplete": "Téléchargement terminé avec succès" + "downloadComplete": "Téléchargement terminé avec succès", + "customPath": "Chemin personnalisé de la base de données", + "customPathPlaceholder": "Laisser vide pour utiliser l'emplacement par défaut", + "customPathHelp": "Spécifiez un emplacement personnalisé pour le fichier de base de données d'archive des métadonnées. Si une base de données existe déjà et que vous modifiez ce chemin, elle sera automatiquement déplacée vers le nouvel emplacement.", + "path": "Chemin de la base de données", + "pathUpdated": "Chemin de la base de données mis à jour avec succès", + "pathUpdatedAndMoved": "Chemin de la base de données mis à jour et base de données déplacée avec succès", + "pathUpdateError": "Échec de la mise à jour du chemin de la base de données", + "movingDatabase": "Déplacement de la base de données..." }, "proxySettings": { "enableProxy": "Activer le proxy au niveau de l'application", diff --git a/locales/he.json b/locales/he.json index 03ad7a2d..753f4ccb 100644 --- a/locales/he.json +++ b/locales/he.json @@ -365,7 +365,15 @@ "preparing": "מכין הורדה...", "connecting": "מתחבר לשרת ההורדות...", "completed": "הושלם", - "downloadComplete": "ההורדה הושלמה בהצלחה" + "downloadComplete": "ההורדה הושלמה בהצלחה", + "customPath": "נתיב מסד נתונים מותאם אישית", + "customPathPlaceholder": "השאר ריק לשימוש במיקום ברירת המחדל", + "customPathHelp": "ציין מיקום מותאם אישית לקובץ מסד הנתונים של ארכיון המטא-דאטה. אם מסד נתונים כבר קיים ואתה משנה את הנתיב הזה, הוא יעבור אוטומטית למיקום החדש.", + "path": "נתיב מסד נתונים", + "pathUpdated": "נתיב מסד הנתונים עודכן בהצלחה", + "pathUpdatedAndMoved": "נתיב מסד הנתונים עודכן ומסד הנתונים הועבר בהצלחה", + "pathUpdateError": "עדכון נתיב מסד הנתונים נכשל", + "movingDatabase": "מעביר מסד נתונים..." }, "proxySettings": { "enableProxy": "הפעל פרוקסי ברמת האפליקציה", diff --git a/locales/ja.json b/locales/ja.json index c8fbcc68..ec3eaede 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -365,7 +365,15 @@ "preparing": "ダウンロードを準備中...", "connecting": "ダウンロードサーバーに接続中...", "completed": "完了", - "downloadComplete": "ダウンロードが正常に完了しました" + "downloadComplete": "ダウンロードが正常に完了しました", + "customPath": "カスタムデータベースパス", + "customPathPlaceholder": "空欄の場合はデフォルトの場所を使用", + "customPathHelp": "メタデータアーカイブデータベースファイルのカスタム場所を指定します。データベースが既に存在し、このパスを変更すると、自動的に新しい場所に移動されます。", + "path": "データベースパス", + "pathUpdated": "データベースパスが正常に更新されました", + "pathUpdatedAndMoved": "データベースパスが更新され、データベースが移動されました", + "pathUpdateError": "データベースパスの更新に失敗しました", + "movingDatabase": "データベースを移動中..." }, "proxySettings": { "enableProxy": "アプリレベルのプロキシを有効化", diff --git a/locales/ko.json b/locales/ko.json index 06195f3f..e778a848 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -365,7 +365,15 @@ "preparing": "다운로드 준비 중...", "connecting": "다운로드 서버에 연결 중...", "completed": "완료됨", - "downloadComplete": "다운로드가 성공적으로 완료되었습니다" + "downloadComplete": "다운로드가 성공적으로 완료되었습니다", + "customPath": "사용자 지정 데이터베이스 경로", + "customPathPlaceholder": "비워두면 기본 위치 사용", + "customPathHelp": "메타데이터 아카이브 데이터베이스 파일의 사용자 지정 위치를 지정합니다. 데이터베이스가 이미 존재하고 이 경로를 변경하면 자동으로 새 위치로 이동됩니다.", + "path": "데이터베이스 경로", + "pathUpdated": "데이터베이스 경로가 성공적으로 업데이트되었습니다", + "pathUpdatedAndMoved": "데이터베이스 경로가 업데이트되고 데이터베이스가 이동되었습니다", + "pathUpdateError": "데이터베이스 경로 업데이트 실패", + "movingDatabase": "데이터베이스 이동 중..." }, "proxySettings": { "enableProxy": "앱 수준 프록시 활성화", diff --git a/locales/ru.json b/locales/ru.json index c4186b78..7e79ab39 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -365,7 +365,15 @@ "preparing": "Подготовка к загрузке...", "connecting": "Подключение к серверу загрузки...", "completed": "Завершено", - "downloadComplete": "Загрузка успешно завершена" + "downloadComplete": "Загрузка успешно завершена", + "customPath": "Пользовательский путь к базе данных", + "customPathPlaceholder": "Оставить пустым для использования расположения по умолчанию", + "customPathHelp": "Укажите пользовательское расположение файла базы данных архива метаданных. Если база данных уже существует и вы измените этот путь, она будет автоматически перемещена в новое расположение.", + "path": "Путь к базе данных", + "pathUpdated": "Путь к базе данных успешно обновлен", + "pathUpdatedAndMoved": "Путь к базе данных обновлен и база данных перемещена успешно", + "pathUpdateError": "Не удалось обновить путь к базе данных", + "movingDatabase": "Перемещение базы данных..." }, "proxySettings": { "enableProxy": "Включить прокси на уровне приложения", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 8799c757..42ffc565 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -365,7 +365,15 @@ "preparing": "正在准备下载...", "connecting": "正在连接下载服务器...", "completed": "已完成", - "downloadComplete": "下载成功完成" + "downloadComplete": "下载成功完成", + "customPath": "自定义数据库路径", + "customPathPlaceholder": "留空以使用默认位置", + "customPathHelp": "指定元数据归档数据库文件的自定义位置。如果数据库已存在且您更改此路径,它将自动移动到新位置。", + "path": "数据库路径", + "pathUpdated": "数据库路径更新成功", + "pathUpdatedAndMoved": "数据库路径已更新,数据库已移动", + "pathUpdateError": "更新数据库路径失败", + "movingDatabase": "正在移动数据库..." }, "proxySettings": { "enableProxy": "启用应用级代理", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 14240a30..2527b555 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -365,7 +365,15 @@ "preparing": "準備下載中...", "connecting": "正在連接下載伺服器...", "completed": "已完成", - "downloadComplete": "下載成功完成" + "downloadComplete": "下載成功完成", + "customPath": "自訂資料庫路徑", + "customPathPlaceholder": "留空以使用預設位置", + "customPathHelp": "指定中繼資料封存資料庫檔案的自訂位置。如果資料庫已存在且您變更此路徑,它將自動移動到新位置。", + "path": "資料庫路徑", + "pathUpdated": "資料庫路徑更新成功", + "pathUpdatedAndMoved": "資料庫路徑已更新,資料庫已移動", + "pathUpdateError": "更新資料庫路徑失敗", + "movingDatabase": "正在移動資料庫..." }, "proxySettings": { "enableProxy": "啟用應用程式代理", diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index cb170ece..f5e95345 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -14,6 +14,7 @@ import subprocess import sys from dataclasses import dataclass +from pathlib import Path from typing import Awaitable, Callable, Dict, Mapping, Protocol from aiohttp import web @@ -68,6 +69,8 @@ async def get_model_versions(self, model_id: int) -> dict | None: # pragma: no class MetadataArchiveManagerProtocol(Protocol): + base_path: Path # pragma: no cover - protocol + async def download_and_extract_database( self, progress_callback: Callable[[str, str], None] ) -> bool: # pragma: no cover - protocol @@ -81,6 +84,9 @@ def is_database_available(self) -> bool: # pragma: no cover - protocol def get_database_path(self) -> str | None: # pragma: no cover - protocol ... + + def move_database(self, new_path: str) -> bool: # pragma: no cover - protocol + ... class NodeRegistry: @@ -831,22 +837,124 @@ async def get_metadata_archive_status(self, request: web.Request) -> web.Respons is_available = archive_manager.is_database_available() is_enabled = self._settings.get("enable_metadata_archive_db", False) db_size = 0 + db_path_str = None if is_available: db_path = archive_manager.get_database_path() if db_path and os.path.exists(db_path): db_size = os.path.getsize(db_path) + db_path_str = str(db_path) return web.json_response( { "success": True, "isAvailable": is_available, "isEnabled": is_enabled, "databaseSize": db_size, - "databasePath": archive_manager.get_database_path() if is_available else None, + "databasePath": db_path_str, } ) except Exception as exc: # pragma: no cover - defensive logging logger.error("Error getting metadata archive status: %s", exc, exc_info=True) return web.json_response({"success": False, "error": str(exc)}, status=500) + + async def update_metadata_archive_path(self, request: web.Request) -> web.Response: + """Update the metadata archive database path and move existing database if needed""" + try: + data = await request.json() + new_path = data.get("path", "").strip() + + archive_manager = await self._metadata_archive_manager_factory() + old_path = archive_manager.get_database_path() + database_exists = archive_manager.is_database_available() + + # Get current custom path from settings + current_custom_path = self._settings.get("metadata_archive_db_path", "") + + # Normalize the new path - if it's a directory, append the database filename + if new_path: + from pathlib import Path + new_path_obj = Path(new_path) + # Check if it's a directory (exists and is a directory, or doesn't exist but has no extension) + if new_path_obj.exists() and new_path_obj.is_dir(): + # It's a directory, append the database filename + new_path = str(new_path_obj / "civitai.sqlite") + logger.info(f"Path is a directory, using: {new_path}") + elif not new_path_obj.suffix: + # No extension, assume it's a directory path + new_path = str(new_path_obj / "civitai.sqlite") + logger.info(f"Path has no extension, treating as directory: {new_path}") + # Otherwise, use it as-is (should be a file path) + + # If path changed and database exists, move it + # Empty path means use default location + moved = False + if database_exists and old_path: + if new_path: + # Moving to custom path + if str(old_path) != new_path: + # Move the database + if not archive_manager.move_database(new_path): + return web.json_response( + {"success": False, "error": "Failed to move database to new location"}, + status=500 + ) + moved = True + logger.info(f"Moved metadata archive database from {old_path} to {new_path}") + elif current_custom_path: + # Moving from custom path back to default + # Get default path using the archive manager's base_path + from pathlib import Path + default_db_path = archive_manager.base_path / "civitai" / "civitai.sqlite" + default_path = str(default_db_path) + + if str(old_path) != default_path: + # Move the database + if not archive_manager.move_database(default_path): + return web.json_response( + {"success": False, "error": "Failed to move database to default location"}, + status=500 + ) + moved = True + logger.info(f"Moved metadata archive database from {old_path} to {default_path}") + + # Update settings (empty string means use default) + self._settings.set("metadata_archive_db_path", new_path) + + # Renew archive manager instance with the new path + updated_manager = await self._metadata_archive_manager_factory() + + # Verify the database is available at the new location (if it was moved) + if moved: + # Wait a moment to ensure file system operations are complete + import asyncio + await asyncio.sleep(0.1) + + # Verify the database exists and is accessible + if not updated_manager.is_database_available(): + logger.warning(f"Database not found at new location after move. Expected at: {updated_manager.get_database_path()}") + return web.json_response({ + "success": False, + "error": "Database was moved but not found at new location" + }, status=500) + else: + logger.info(f"Database verified at new location: {updated_manager.get_database_path()}") + + # Always reinitialize providers if enabled (this will pick up the new database location) + # This ensures the database is properly loaded even if path changed without moving + if self._settings.get("enable_metadata_archive_db", False): + await self._metadata_provider_updater() + logger.info("Metadata providers reinitialized after path update") + + # Get the actual path that will be used + actual_path = updated_manager.get_database_path() + + return web.json_response({ + "success": True, + "message": "Database path updated successfully" + (" and database moved" if moved else ""), + "databasePath": str(actual_path) if actual_path else None + }) + except Exception as exc: + logger.error("Error updating metadata archive path: %s", exc, exc_info=True) + return web.json_response({"success": False, "error": str(exc)}, status=500) class FileSystemHandler: @@ -1098,6 +1206,7 @@ def to_route_mapping(self) -> Mapping[str, Callable[[web.Request], Awaitable[web "download_metadata_archive": self.metadata_archive.download_metadata_archive, "remove_metadata_archive": self.metadata_archive.remove_metadata_archive, "get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status, + "update_metadata_archive_path": self.metadata_archive.update_metadata_archive_path, "get_model_versions_status": self.model_library.get_model_versions_status, "open_file_location": self.filesystem.open_file_location, } diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py index a68aa8eb..05a9210c 100644 --- a/py/routes/misc_route_registrar.py +++ b/py/routes/misc_route_registrar.py @@ -40,6 +40,7 @@ class RouteDefinition: RouteDefinition("POST", "/api/lm/download-metadata-archive", "download_metadata_archive"), RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"), RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"), + RouteDefinition("POST", "/api/lm/update-metadata-archive-path", "update_metadata_archive_path"), RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"), ) diff --git a/py/services/metadata_archive_manager.py b/py/services/metadata_archive_manager.py index c9e24697..ede9a892 100644 --- a/py/services/metadata_archive_manager.py +++ b/py/services/metadata_archive_manager.py @@ -1,6 +1,7 @@ import zipfile import logging import asyncio +import shutil from pathlib import Path from typing import Optional from .downloader import get_downloader, DownloadProgress @@ -15,12 +16,44 @@ class MetadataArchiveManager: "https://huggingface.co/datasets/willmiao/civitai-metadata-archive-db/blob/main/civitai.zip" ] - def __init__(self, base_path: str): - """Initialize with base path where files will be stored""" + def __init__(self, base_path: str, custom_db_path: Optional[str] = None): + """Initialize with base path where files will be stored + + Args: + base_path: Default base path (used if custom_db_path is not provided) + custom_db_path: Optional custom path to the database file or directory. + If it's a directory, will append "civitai.sqlite" to it. + """ self.base_path = Path(base_path) - self.civitai_folder = self.base_path / "civitai" - self.archive_path = self.base_path / "civitai.zip" - self.db_path = self.civitai_folder / "civitai.sqlite" + + if custom_db_path: + custom_path_obj = Path(custom_db_path) + # Check if it's a directory (exists and is a directory, or has no extension) + if custom_path_obj.exists() and custom_path_obj.is_dir(): + # It's a directory, append the database filename + self.custom_db_path = custom_path_obj / "civitai.sqlite" + logger.debug(f"Custom path is a directory, using: {self.custom_db_path}") + elif not custom_path_obj.suffix: + # No extension, assume it's a directory path + self.custom_db_path = custom_path_obj / "civitai.sqlite" + logger.debug(f"Custom path has no extension, treating as directory: {self.custom_db_path}") + else: + # Use as-is (should be a file path) + self.custom_db_path = custom_path_obj + else: + self.custom_db_path = None + + if self.custom_db_path: + # Use custom path as the database file + self.db_path = self.custom_db_path + # For archive operations, use the parent directory + self.civitai_folder = self.db_path.parent + self.archive_path = self.civitai_folder / "civitai.zip" + else: + # Use default structure + self.civitai_folder = self.base_path / "civitai" + self.archive_path = self.base_path / "civitai.zip" + self.db_path = self.civitai_folder / "civitai.sqlite" def is_database_available(self) -> bool: """Check if the SQLite database is available and valid""" @@ -43,16 +76,43 @@ async def download_and_extract_database(self, progress_callback=None) -> bool: """ try: # Create directories if they don't exist - self.base_path.mkdir(parents=True, exist_ok=True) - self.civitai_folder.mkdir(parents=True, exist_ok=True) + if self.custom_db_path: + # For custom path, ensure parent directory exists + self.db_path.parent.mkdir(parents=True, exist_ok=True) + # Use parent directory for archive operations + temp_extract_path = self.db_path.parent + else: + # Default structure + self.base_path.mkdir(parents=True, exist_ok=True) + self.civitai_folder.mkdir(parents=True, exist_ok=True) + temp_extract_path = self.base_path # Download the archive if not await self._download_archive(progress_callback): return False # Extract the archive - if not await self._extract_archive(progress_callback): + if not await self._extract_archive(progress_callback, temp_extract_path): return False + + # If using custom path, move the database file to the custom location + if self.custom_db_path: + extracted_db = temp_extract_path / "civitai" / "civitai.sqlite" + if extracted_db.exists(): + shutil.move(str(extracted_db), str(self.db_path)) + # Clean up extracted civitai folder if empty + try: + civitai_folder = temp_extract_path / "civitai" + if civitai_folder.exists(): + # Remove any remaining files + for item in civitai_folder.iterdir(): + if item.is_file(): + item.unlink() + elif item.is_dir(): + shutil.rmtree(item) + civitai_folder.rmdir() + except Exception: + pass # Ignore cleanup errors # Clean up the archive file if self.archive_path.exists(): @@ -109,34 +169,45 @@ async def download_progress(progress, snapshot=None): logger.error("Failed to download archive from any URL") return False - async def _extract_archive(self, progress_callback=None) -> bool: - """Extract the zip archive to the civitai folder""" + async def _extract_archive(self, progress_callback=None, extract_path=None) -> bool: + """Extract the zip archive to the specified path + + Args: + progress_callback: Optional callback function to report progress + extract_path: Path to extract to (defaults to base_path) + """ try: if progress_callback: progress_callback("extract", "Extracting archive...") + + extract_to = Path(extract_path) if extract_path else self.base_path # Run extraction in thread pool to avoid blocking loop = asyncio.get_event_loop() - await loop.run_in_executor(None, self._extract_zip_sync) + await loop.run_in_executor(None, self._extract_zip_sync, extract_to) if progress_callback: progress_callback("extract", "Extraction completed") - + return True except Exception as e: logger.error(f"Error extracting archive: {e}", exc_info=True) return False - def _extract_zip_sync(self): + def _extract_zip_sync(self, extract_path: Path): """Synchronous zip extraction (runs in thread pool)""" with zipfile.ZipFile(self.archive_path, 'r') as archive: - archive.extractall(path=self.base_path) + archive.extractall(path=extract_path) async def remove_database(self) -> bool: """Remove the metadata database and folder""" try: - if self.civitai_folder.exists(): + if self.custom_db_path and self.db_path.exists(): + # If using custom path, just remove the database file + self.db_path.unlink() + logger.info(f"Removed custom database file: {self.db_path}") + elif self.civitai_folder.exists(): # Remove all files in the civitai folder for file_path in self.civitai_folder.iterdir(): if file_path.is_file(): @@ -144,14 +215,61 @@ async def remove_database(self) -> bool: # Remove the folder itself self.civitai_folder.rmdir() - + # Also remove the archive file if it exists if self.archive_path.exists(): self.archive_path.unlink() - + logger.info("Successfully removed metadata database") return True except Exception as e: logger.error(f"Error removing metadata database: {e}", exc_info=True) return False + + def move_database(self, new_path: str) -> bool: + """Move the database to a new location + + Args: + new_path: New path for the database file + + Returns: + bool: True if successful, False otherwise + """ + try: + new_db_path = Path(new_path) + + # Ensure the new directory exists + new_db_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if database exists at current location + if not self.db_path.exists(): + logger.warning(f"Database not found at {self.db_path}, nothing to move") + return False + + # Move the database file + shutil.move(str(self.db_path), str(new_db_path)) + logger.info(f"Moved database from {self.db_path} to {new_db_path}") + + # Update internal path + self.db_path = new_db_path + self.custom_db_path = new_db_path + self.civitai_folder = new_db_path.parent + self.archive_path = self.civitai_folder / "civitai.zip" + + # Clean up old folder if it's empty (for default structure) + old_folder = self.base_path / "civitai" + if old_folder.exists() and old_folder != self.civitai_folder: + try: + # Check if folder is empty + if not any(old_folder.iterdir()): + old_folder.rmdir() + logger.info(f"Removed empty old folder: {old_folder}") + except Exception: + pass # Ignore errors cleaning up old folder + + return True + + except Exception as e: + logger.error(f"Error moving database: {e}", exc_info=True) + return False diff --git a/py/services/metadata_service.py b/py/services/metadata_service.py index 1a48ceaf..66332b26 100644 --- a/py/services/metadata_service.py +++ b/py/services/metadata_service.py @@ -32,9 +32,8 @@ async def initialize_metadata_providers(): # Initialize archive database provider if enabled if enable_archive_db: try: - # Initialize archive manager - base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - archive_manager = MetadataArchiveManager(base_path) + # Initialize archive manager with custom path if set + archive_manager = await get_metadata_archive_manager() db_path = archive_manager.get_database_path() if db_path and os.path.exists(db_path): @@ -106,8 +105,11 @@ async def update_metadata_providers(): async def get_metadata_archive_manager(): """Get metadata archive manager instance""" + from .settings_manager import get_settings_manager + settings_manager = get_settings_manager() base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - return MetadataArchiveManager(base_path) + custom_db_path = settings_manager.get("metadata_archive_db_path", "") + return MetadataArchiveManager(base_path, custom_db_path=custom_db_path if custom_db_path else None) def _wrap_provider_with_rate_limit(provider_name: str | None, provider: ModelMetadataProvider) -> ModelMetadataProvider: if isinstance(provider, (FallbackMetadataProvider, RateLimitRetryingProvider)): diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 3ba77183..2124ee83 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -33,6 +33,7 @@ "language": "en", "show_only_sfw": False, "enable_metadata_archive_db": False, + "metadata_archive_db_path": "", "proxy_enabled": False, "proxy_host": "", "proxy_port": "", diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 6a925970..b79b9c6c 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -326,6 +326,19 @@ export class SettingsManager { this.setupPriorityTagInputs(); + // Setup metadata archive database path input + const metadataArchiveDbPathInput = document.getElementById('metadataArchiveDbPath'); + if (metadataArchiveDbPathInput) { + metadataArchiveDbPathInput.addEventListener('blur', () => { + this.saveMetadataArchivePath(); + }); + metadataArchiveDbPathInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.target.blur(); + } + }); + } + this.initialized = true; } @@ -1348,6 +1361,12 @@ export class SettingsManager { if (enableMetadataArchiveCheckbox) { enableMetadataArchiveCheckbox.checked = state.global.settings.enable_metadata_archive_db || false; } + + // Load custom path + const pathInput = document.getElementById('metadataArchiveDbPath'); + if (pathInput) { + pathInput.value = state.global.settings.metadata_archive_db_path || ''; + } // Load status await this.updateMetadataArchiveStatus(); @@ -1355,6 +1374,88 @@ export class SettingsManager { console.error('Error loading metadata archive settings:', error); } } + + async saveMetadataArchivePath() { + try { + const pathInput = document.getElementById('metadataArchiveDbPath'); + if (!pathInput) return; + + const newPath = pathInput.value.trim(); + const currentPath = state.global.settings.metadata_archive_db_path || ''; + + // Only update if path changed + if (newPath === currentPath) { + return; + } + + // Check if we need to move the database (path changed and database exists) + const statusResponse = await fetch('/api/lm/metadata-archive-status'); + const statusData = await statusResponse.json(); + const needsMove = statusData.success && statusData.isAvailable && + ((newPath && currentPath && newPath !== currentPath) || + (!newPath && currentPath)); + + // Show "Moving Database..." status if a move is needed + if (needsMove) { + this.showMovingStatus(); + } + + const response = await fetch('/api/lm/update-metadata-archive-path', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ path: newPath }) + }); + + const data = await response.json(); + + if (data.success) { + // Update local state + await this.saveSetting('metadata_archive_db_path', newPath); + + // Refresh status to show new path + await this.updateMetadataArchiveStatus(); + + if (data.message && data.message.includes('moved')) { + showToast('settings.metadataArchive.pathUpdatedAndMoved', 'success'); + } else { + showToast('settings.metadataArchive.pathUpdated', 'success'); + } + } else { + // Refresh status on error to clear moving status + await this.updateMetadataArchiveStatus(); + showToast('settings.metadataArchive.pathUpdateError' + ': ' + data.error, 'error'); + // Revert input value on error + pathInput.value = currentPath; + } + } catch (error) { + console.error('Error saving metadata archive path:', error); + // Refresh status on error to clear moving status + await this.updateMetadataArchiveStatus(); + showToast('settings.metadataArchive.pathUpdateError' + ': ' + error.message, 'error'); + } + } + + showMovingStatus() { + const statusContainer = document.getElementById('metadataArchiveStatus'); + if (statusContainer) { + statusContainer.innerHTML = ` +
+ ${translate('settings.metadataArchive.status')}: + + ${translate('settings.metadataArchive.movingDatabase')} + +
+
+ ${translate('settings.metadataArchive.enabled')}: + + ${translate('common.status.enabled')} + +
+ `; + } + } async updateMetadataArchiveStatus() { try { diff --git a/static/js/state/index.js b/static/js/state/index.js index 7df333c9..d0b5795c 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -8,6 +8,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({ language: 'en', show_only_sfw: false, enable_metadata_archive_db: false, + metadata_archive_db_path: '', proxy_enabled: false, proxy_type: 'http', proxy_host: '', diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 4cbeab99..d8fbe9d6 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -543,6 +543,29 @@

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

+
+
+
+ +
+
+
+ +
+
+
+
+ {{ t('settings.metadataArchive.customPathHelp') }} +
+
+