From 821827a375d5a08ab8f57a5fe13e3b4eda0a6928 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 8 Sep 2025 13:17:16 +0800 Subject: [PATCH] feat(metadata): implement metadata archive management and update settings for metadata providers --- locales/en.json | 32 ++- py/routes/checkpoint_routes.py | 7 +- py/routes/embedding_routes.py | 7 +- py/routes/lora_routes.py | 13 +- py/routes/misc_routes.py | 112 +++++++++++ py/services/metadata_archive_manager.py | 150 ++++++++++++++ py/services/metadata_service.py | 97 +++++++-- py/services/model_metadata_provider.py | 22 +- py/services/settings_manager.py | 4 +- static/js/managers/SettingsManager.js | 189 ++++++++++++++++++ .../components/modals/settings_modal.html | 64 ++++++ 11 files changed, 659 insertions(+), 38 deletions(-) create mode 100644 py/services/metadata_archive_manager.py diff --git a/locales/en.json b/locales/en.json index 093433c8..52bc8580 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,7 +16,9 @@ "loading": "Loading...", "unknown": "Unknown", "date": "Date", - "version": "Version" + "version": "Version", + "enabled": "Enabled", + "disabled": "Disabled" }, "language": { "select": "Language", @@ -178,7 +180,8 @@ "folderSettings": "Folder Settings", "downloadPathTemplates": "Download Path Templates", "exampleImages": "Example Images", - "misc": "Misc." + "misc": "Misc.", + "metadataArchive": "Metadata Archive Database" }, "contentFiltering": { "blurNsfwContent": "Blur NSFW Content", @@ -273,6 +276,31 @@ "misc": { "includeTriggerWords": "Include Trigger Words in LoRA Syntax", "includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard" + }, + "metadataArchive": { + "enableArchiveDb": "Enable Metadata Archive Database", + "enableArchiveDbHelp": "Use local database for faster metadata retrieval and access to deleted models. Recommended for better performance.", + "providerPriority": "Metadata Provider Priority", + "providerPriorityHelp": "Choose which metadata source to try first when loading model information", + "priorityArchiveDb": "Archive Database (Recommended)", + "priorityCivitaiApi": "Civitai API", + "status": "Status", + "statusAvailable": "Available", + "statusUnavailable": "Not Available", + "enabled": "Enabled", + "currentPriority": "Current Priority", + "management": "Database Management", + "managementHelp": "Download or remove the metadata archive database", + "downloadButton": "Download Database", + "downloadingButton": "Downloading...", + "downloadedButton": "Downloaded", + "removeButton": "Remove Database", + "removingButton": "Removing...", + "downloadSuccess": "Metadata archive database downloaded successfully", + "downloadError": "Failed to download metadata archive database", + "removeSuccess": "Metadata archive database removed successfully", + "removeError": "Failed to remove metadata archive database", + "removeConfirm": "Are you sure you want to remove the metadata archive database? This will delete the local database file and you'll need to download it again to use this feature." } }, "loras": { diff --git a/py/routes/checkpoint_routes.py b/py/routes/checkpoint_routes.py index 9b5d20a6..b93700cf 100644 --- a/py/routes/checkpoint_routes.py +++ b/py/routes/checkpoint_routes.py @@ -4,6 +4,7 @@ from aiohttp import web from .base_model_routes import BaseModelRoutes from ..services.checkpoint_service import CheckpointService from ..services.service_registry import ServiceRegistry +from ..services.model_metadata_provider import ModelMetadataProviderManager from ..config import config logger = logging.getLogger(__name__) @@ -15,14 +16,14 @@ class CheckpointRoutes(BaseModelRoutes): """Initialize Checkpoint routes with Checkpoint service""" # Service will be initialized later via setup_routes self.service = None - self.civitai_client = None + self.metadata_provider = None self.template_name = "checkpoints.html" async def initialize_services(self): """Initialize services from ServiceRegistry""" checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner() self.service = CheckpointService(checkpoint_scanner) - self.civitai_client = await ServiceRegistry.get_civitai_client() + self.metadata_provider = await ModelMetadataProviderManager.get_instance() # Initialize parent with the service super().__init__(self.service) @@ -66,7 +67,7 @@ class CheckpointRoutes(BaseModelRoutes): """Get available versions for a Civitai checkpoint model with local availability info""" try: model_id = request.match_info['model_id'] - response = await self.civitai_client.get_model_versions(model_id) + response = await self.metadata_provider.get_model_versions(model_id) if not response or not response.get('modelVersions'): return web.Response(status=404, text="Model not found") diff --git a/py/routes/embedding_routes.py b/py/routes/embedding_routes.py index eb9a5203..65a66824 100644 --- a/py/routes/embedding_routes.py +++ b/py/routes/embedding_routes.py @@ -4,6 +4,7 @@ from aiohttp import web from .base_model_routes import BaseModelRoutes from ..services.embedding_service import EmbeddingService from ..services.service_registry import ServiceRegistry +from ..services.model_metadata_provider import ModelMetadataProviderManager logger = logging.getLogger(__name__) @@ -14,14 +15,14 @@ class EmbeddingRoutes(BaseModelRoutes): """Initialize Embedding routes with Embedding service""" # Service will be initialized later via setup_routes self.service = None - self.civitai_client = None + self.metadata_provider = None self.template_name = "embeddings.html" async def initialize_services(self): """Initialize services from ServiceRegistry""" embedding_scanner = await ServiceRegistry.get_embedding_scanner() self.service = EmbeddingService(embedding_scanner) - self.civitai_client = await ServiceRegistry.get_civitai_client() + self.metadata_provider = await ModelMetadataProviderManager.get_instance() # Initialize parent with the service super().__init__(self.service) @@ -61,7 +62,7 @@ class EmbeddingRoutes(BaseModelRoutes): """Get available versions for a Civitai embedding model with local availability info""" try: model_id = request.match_info['model_id'] - response = await self.civitai_client.get_model_versions(model_id) + response = await self.metadata_provider.get_model_versions(model_id) if not response or not response.get('modelVersions'): return web.Response(status=404, text="Model not found") diff --git a/py/routes/lora_routes.py b/py/routes/lora_routes.py index 2da33cb1..4c1c0467 100644 --- a/py/routes/lora_routes.py +++ b/py/routes/lora_routes.py @@ -7,6 +7,7 @@ from server import PromptServer # type: ignore from .base_model_routes import BaseModelRoutes from ..services.lora_service import LoraService from ..services.service_registry import ServiceRegistry +from ..services.model_metadata_provider import ModelMetadataProviderManager from ..utils.routes_common import ModelRouteUtils from ..utils.utils import get_lora_info @@ -19,14 +20,14 @@ class LoraRoutes(BaseModelRoutes): """Initialize LoRA routes with LoRA service""" # Service will be initialized later via setup_routes self.service = None - self.civitai_client = None + self.metadata_provider = None self.template_name = "loras.html" async def initialize_services(self): """Initialize services from ServiceRegistry""" lora_scanner = await ServiceRegistry.get_lora_scanner() self.service = LoraService(lora_scanner) - self.civitai_client = await ServiceRegistry.get_civitai_client() + self.metadata_provider = await ModelMetadataProviderManager.get_instance() # Initialize parent with the service super().__init__(self.service) @@ -217,7 +218,7 @@ class LoraRoutes(BaseModelRoutes): """Get available versions for a Civitai LoRA model with local availability info""" try: model_id = request.match_info['model_id'] - response = await self.civitai_client.get_model_versions(model_id) + response = await self.metadata_provider.get_model_versions(model_id) if not response or not response.get('modelVersions'): return web.Response(status=404, text="Model not found") @@ -261,8 +262,8 @@ class LoraRoutes(BaseModelRoutes): try: model_version_id = request.match_info.get('modelVersionId') - # Get model details from Civitai API - model, error_msg = await self.civitai_client.get_model_version_info(model_version_id) + # Get model details from metadata provider + model, error_msg = await self.metadata_provider.get_model_version_info(model_version_id) if not model: # Log warning for failed model retrieval @@ -288,7 +289,7 @@ class LoraRoutes(BaseModelRoutes): """Get CivitAI model details by hash""" try: hash = request.match_info.get('hash') - model = await self.civitai_client.get_model_by_hash(hash) + model = await self.metadata_provider.get_model_by_hash(hash) return web.json_response(model) except Exception as e: logger.error(f"Error fetching model details by hash: {e}") diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 87a16aac..9a29a24d 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -11,6 +11,8 @@ from ..utils.lora_metadata import extract_trained_words from ..config import config from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS, NODE_TYPES, DEFAULT_NODE_COLOR from ..services.service_registry import ServiceRegistry +from ..services.metadata_service import get_metadata_archive_manager, update_metadata_provider_priority +from ..services.websocket_manager import ws_manager import re logger = logging.getLogger(__name__) @@ -112,6 +114,11 @@ class MiscRoutes: # Add new route for checking if a model exists in the library app.router.add_get('/api/check-model-exists', MiscRoutes.check_model_exists) + + # Add routes for metadata archive database management + app.router.add_post('/api/download-metadata-archive', MiscRoutes.download_metadata_archive) + app.router.add_post('/api/remove-metadata-archive', MiscRoutes.remove_metadata_archive) + app.router.add_get('/api/metadata-archive-status', MiscRoutes.get_metadata_archive_status) @staticmethod async def clear_cache(request): @@ -697,3 +704,108 @@ class MiscRoutes: 'success': False, 'error': str(e) }, status=500) + + @staticmethod + async def download_metadata_archive(request): + """Download and extract the metadata archive database""" + try: + archive_manager = await get_metadata_archive_manager() + + # Progress callback to send updates via WebSocket + def progress_callback(stage, message): + asyncio.create_task(ws_manager.broadcast({ + 'stage': stage, + 'message': message, + 'type': 'metadata_archive_download' + })) + + # Download and extract in background + success = await archive_manager.download_and_extract_database(progress_callback) + + if success: + # Update settings to enable metadata archive + settings.set('enable_metadata_archive_db', True) + + # Update provider priority + await update_metadata_provider_priority() + + return web.json_response({ + 'success': True, + 'message': 'Metadata archive database downloaded and extracted successfully' + }) + else: + return web.json_response({ + 'success': False, + 'error': 'Failed to download and extract metadata archive database' + }, status=500) + + except Exception as e: + logger.error(f"Error downloading metadata archive: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + @staticmethod + async def remove_metadata_archive(request): + """Remove the metadata archive database""" + try: + archive_manager = await get_metadata_archive_manager() + + success = await archive_manager.remove_database() + + if success: + # Update settings to disable metadata archive + settings.set('enable_metadata_archive_db', False) + + # Update provider priority + await update_metadata_provider_priority() + + return web.json_response({ + 'success': True, + 'message': 'Metadata archive database removed successfully' + }) + else: + return web.json_response({ + 'success': False, + 'error': 'Failed to remove metadata archive database' + }, status=500) + + except Exception as e: + logger.error(f"Error removing metadata archive: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) + + @staticmethod + async def get_metadata_archive_status(request): + """Get the status of metadata archive database""" + try: + archive_manager = await get_metadata_archive_manager() + + is_available = archive_manager.is_database_available() + is_enabled = settings.get('enable_metadata_archive_db', False) + priority = settings.get('metadata_provider_priority', 'archive_db') + + db_size = 0 + 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) + + return web.json_response({ + 'success': True, + 'isAvailable': is_available, + 'isEnabled': is_enabled, + 'priority': priority, + 'databaseSize': db_size, + 'databasePath': archive_manager.get_database_path() if is_available else None + }) + + except Exception as e: + logger.error(f"Error getting metadata archive status: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/py/services/metadata_archive_manager.py b/py/services/metadata_archive_manager.py new file mode 100644 index 00000000..3daf761b --- /dev/null +++ b/py/services/metadata_archive_manager.py @@ -0,0 +1,150 @@ +import zipfile +import aiohttp +import logging +import asyncio +from pathlib import Path +from typing import Optional + +logger = logging.getLogger(__name__) + +class MetadataArchiveManager: + """Manages downloading and extracting Civitai metadata archive database""" + + DOWNLOAD_URLS = [ + "https://github.com/willmiao/civitai-metadata-archive-db/releases/download/db-2025-08-08/civitai.zip", + "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""" + 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" + + def is_database_available(self) -> bool: + """Check if the SQLite database is available and valid""" + return self.db_path.exists() and self.db_path.stat().st_size > 0 + + def get_database_path(self) -> Optional[str]: + """Get the path to the SQLite database if available""" + if self.is_database_available(): + return str(self.db_path) + return None + + async def download_and_extract_database(self, progress_callback=None) -> bool: + """Download and extract the metadata archive database + + Args: + progress_callback: Optional callback function to report progress + + Returns: + bool: True if successful, False otherwise + """ + 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) + + # Download the archive + if not await self._download_archive(progress_callback): + return False + + # Extract the archive + if not await self._extract_archive(progress_callback): + return False + + # Clean up the archive file + if self.archive_path.exists(): + self.archive_path.unlink() + + logger.info(f"Successfully downloaded and extracted metadata database to {self.db_path}") + return True + + except Exception as e: + logger.error(f"Error downloading and extracting metadata database: {e}", exc_info=True) + return False + + async def _download_archive(self, progress_callback=None) -> bool: + """Download the zip archive from one of the available URLs""" + for url in self.DOWNLOAD_URLS: + try: + logger.info(f"Attempting to download from {url}") + + if progress_callback: + progress_callback("download", f"Downloading from {url}") + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + total_size = int(response.headers.get('content-length', 0)) + downloaded = 0 + + with open(self.archive_path, 'wb') as f: + async for chunk in response.content.iter_chunked(8192): + f.write(chunk) + downloaded += len(chunk) + + if progress_callback and total_size > 0: + percentage = (downloaded / total_size) * 100 + progress_callback("download", f"Downloaded {percentage:.1f}%") + + logger.info(f"Successfully downloaded archive from {url}") + return True + else: + logger.warning(f"Failed to download from {url}: HTTP {response.status}") + continue + + except Exception as e: + logger.warning(f"Error downloading from {url}: {e}") + continue + + 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""" + try: + if progress_callback: + progress_callback("extract", "Extracting archive...") + + # Run extraction in thread pool to avoid blocking + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, self._extract_zip_sync) + + 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): + """Synchronous zip extraction (runs in thread pool)""" + with zipfile.ZipFile(self.archive_path, 'r') as archive: + archive.extractall(path=self.base_path) + + async def remove_database(self) -> bool: + """Remove the metadata database and folder""" + try: + if self.civitai_folder.exists(): + # Remove all files in the civitai folder + for file_path in self.civitai_folder.iterdir(): + if file_path.is_file(): + file_path.unlink() + + # 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 diff --git a/py/services/metadata_service.py b/py/services/metadata_service.py index beb8912d..0e5d9199 100644 --- a/py/services/metadata_service.py +++ b/py/services/metadata_service.py @@ -1,28 +1,97 @@ import os import logging -from .model_metadata_provider import ModelMetadataProviderManager, SQLiteModelMetadataProvider +from .model_metadata_provider import ( + ModelMetadataProviderManager, + SQLiteModelMetadataProvider, + CivitaiModelMetadataProvider, + FallbackMetadataProvider +) +from .settings_manager import settings +from .metadata_archive_manager import MetadataArchiveManager +from .service_registry import ServiceRegistry logger = logging.getLogger(__name__) async def initialize_metadata_providers(): - """Initialize and configure all metadata providers""" + """Initialize and configure all metadata providers based on settings""" provider_manager = await ModelMetadataProviderManager.get_instance() - # Use hardcoded SQLite DB path if not set in settings - db_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - 'civitai', 'civitai.sqlite' - ) - if db_path and os.path.exists(db_path): - try: - sqlite_provider = SQLiteModelMetadataProvider(db_path) - provider_manager.register_provider('sqlite', sqlite_provider) - logger.info(f"SQLite metadata provider registered with database: {db_path}") - except Exception as e: - logger.error(f"Failed to initialize SQLite metadata provider: {e}") + # Get settings + enable_archive_db = settings.get('enable_metadata_archive_db', False) + priority = settings.get('metadata_provider_priority', 'archive_db') + + providers = [] + + # Initialize archive database provider if enabled + if enable_archive_db: + # Initialize archive manager + base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + archive_manager = MetadataArchiveManager(base_path) + + db_path = archive_manager.get_database_path() + if db_path: + try: + sqlite_provider = SQLiteModelMetadataProvider(db_path) + provider_manager.register_provider('sqlite', sqlite_provider) + providers.append(('sqlite', sqlite_provider)) + logger.info(f"SQLite metadata provider registered with database: {db_path}") + except Exception as e: + logger.error(f"Failed to initialize SQLite metadata provider: {e}") + else: + logger.warning("Metadata archive database is enabled but not available") + + # Initialize Civitai API provider + try: + civitai_client = await ServiceRegistry.get_civitai_client() + civitai_provider = CivitaiModelMetadataProvider(civitai_client) + provider_manager.register_provider('civitai_api', civitai_provider) + providers.append(('civitai_api', civitai_provider)) + logger.info("Civitai API metadata provider registered") + except Exception as e: + logger.error(f"Failed to initialize Civitai API metadata provider: {e}") + + # Set up fallback provider based on priority + if len(providers) > 1: + # Order providers based on priority setting + if priority == 'archive_db': + # Archive DB first, then Civitai API + ordered_providers = [p[1] for p in providers if p[0] == 'sqlite'] + [p[1] for p in providers if p[0] == 'civitai_api'] + else: + # Civitai API first, then Archive DB + ordered_providers = [p[1] for p in providers if p[0] == 'civitai_api'] + [p[1] for p in providers if p[0] == 'sqlite'] + + if ordered_providers: + fallback_provider = FallbackMetadataProvider(ordered_providers) + provider_manager.register_provider('fallback', fallback_provider, is_default=True) + logger.info(f"Fallback metadata provider registered with priority: {priority}") + elif len(providers) == 1: + # Only one provider available, set it as default + provider_name, provider = providers[0] + provider_manager.register_provider(provider_name, provider, is_default=True) + logger.info(f"Single metadata provider registered as default: {provider_name}") + else: + logger.warning("No metadata providers available") return provider_manager +async def update_metadata_provider_priority(): + """Update metadata provider priority based on current settings""" + provider_manager = await ModelMetadataProviderManager.get_instance() + + # Get current settings + enable_archive_db = settings.get('enable_metadata_archive_db', False) + priority = settings.get('metadata_provider_priority', 'archive_db') + + # Rebuild providers with new priority + await initialize_metadata_providers() + + logger.info(f"Updated metadata provider priority to: {priority}") + +async def get_metadata_archive_manager(): + """Get metadata archive manager instance""" + base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + return MetadataArchiveManager(base_path) + async def get_metadata_provider(provider_name: str = None): """Get a specific metadata provider or default provider""" provider_manager = await ModelMetadataProviderManager.get_instance() diff --git a/py/services/model_metadata_provider.py b/py/services/model_metadata_provider.py index 95dfc1da..2a30df11 100644 --- a/py/services/model_metadata_provider.py +++ b/py/services/model_metadata_provider.py @@ -297,7 +297,8 @@ class FallbackMetadataProvider(ModelMetadataProvider): result = await provider.get_model_versions(model_id) if result: return result - except Exception: + except Exception as e: + logger.debug(f"Provider failed for get_model_versions: {e}") continue return None @@ -307,27 +308,30 @@ class FallbackMetadataProvider(ModelMetadataProvider): result = await provider.get_model_version(model_id, version_id) if result: return result - except Exception: + except Exception as e: + logger.debug(f"Provider failed for get_model_version: {e}") continue return None async def get_model_version_info(self, version_id: str) -> Tuple[Optional[Dict], Optional[str]]: for provider in self.providers: try: - result, err = await provider.get_model_version_info(version_id) + result, error = await provider.get_model_version_info(version_id) if result: - return result, err - except Exception: + return result, error + except Exception as e: + logger.debug(f"Provider failed for get_model_version_info: {e}") continue - return None, "Not found in any provider" + return None, "No provider could retrieve the data" async def get_model_metadata(self, model_id: str) -> Tuple[Optional[Dict], int]: for provider in self.providers: try: - result, code = await provider.get_model_metadata(model_id) + result, status = await provider.get_model_metadata(model_id) if result: - return result, code - except Exception: + return result, status + except Exception as e: + logger.debug(f"Provider failed for get_model_metadata: {e}") continue return None, 404 diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 53146613..0b86ce82 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -81,7 +81,9 @@ class SettingsManager: return { "civitai_api_key": "", "show_only_sfw": False, - "language": "en" # 添加默认语言设置 + "language": "en", # 添加默认语言设置 + "enable_metadata_archive_db": False, # Enable metadata archive database + "metadata_provider_priority": "archive_db" # Default priority: 'archive_db' or 'civitai_api' } def get(self, key: str, default: Any = None) -> Any: diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index b31613e2..1a15bdb4 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -260,6 +260,9 @@ export class SettingsManager { includeTriggerWordsCheckbox.checked = state.global.settings.includeTriggerWords || false; } + // Load metadata archive settings + await this.loadMetadataArchiveSettings(); + // Load base model path mappings this.loadBaseModelMappings(); @@ -838,6 +841,11 @@ export class SettingsManager { state: value ? 'toast.settings.compactModeEnabled' : 'toast.settings.compactModeDisabled' }, 'success'); } + + // Special handling for metadata archive settings + if (settingKey === 'enable_metadata_archive_db' || settingKey === 'metadata_provider_priority') { + await this.updateMetadataArchiveStatus(); + } } catch (error) { showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); @@ -910,11 +918,192 @@ export class SettingsManager { showToast('toast.settings.displayDensitySet', { density: densityName }, 'success'); } + + // Special handling for metadata archive settings + if (settingKey === 'metadata_provider_priority') { + await this.updateMetadataArchiveStatus(); + } } catch (error) { showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error'); } } + + async loadMetadataArchiveSettings() { + try { + // Load current settings from state + const enableMetadataArchiveCheckbox = document.getElementById('enableMetadataArchive'); + if (enableMetadataArchiveCheckbox) { + enableMetadataArchiveCheckbox.checked = state.global.settings.enable_metadata_archive_db || false; + } + + const metadataProviderPrioritySelect = document.getElementById('metadataProviderPriority'); + if (metadataProviderPrioritySelect) { + metadataProviderPrioritySelect.value = state.global.settings.metadata_provider_priority || 'archive_db'; + } + + // Load status + await this.updateMetadataArchiveStatus(); + } catch (error) { + console.error('Error loading metadata archive settings:', error); + } + } + + async updateMetadataArchiveStatus() { + try { + const response = await fetch('/api/metadata-archive-status'); + const data = await response.json(); + + const statusContainer = document.getElementById('metadataArchiveStatus'); + if (statusContainer && data.success) { + const status = data; + const sizeText = status.databaseSize > 0 ? ` (${this.formatFileSize(status.databaseSize)})` : ''; + + statusContainer.innerHTML = ` +
+
+ ${translate('settings.metadataArchive.status')}: + + ${status.isAvailable ? translate('settings.metadataArchive.statusAvailable') : translate('settings.metadataArchive.statusUnavailable')} + + ${sizeText} +
+
+ ${translate('settings.metadataArchive.enabled')}: + + ${status.isEnabled ? translate('common.enabled') : translate('common.disabled')} + +
+
+ ${translate('settings.metadataArchive.currentPriority')}: + ${status.priority === 'archive_db' ? translate('settings.metadataArchive.priorityArchiveDb') : translate('settings.metadataArchive.priorityCivitaiApi')} +
+
+ `; + + // Update button states + const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); + const removeBtn = document.getElementById('removeMetadataArchiveBtn'); + + if (downloadBtn) { + downloadBtn.disabled = status.isAvailable; + downloadBtn.textContent = status.isAvailable ? + translate('settings.metadataArchive.downloadedButton') : + translate('settings.metadataArchive.downloadButton'); + } + + if (removeBtn) { + removeBtn.disabled = !status.isAvailable; + } + } + } catch (error) { + console.error('Error updating metadata archive status:', error); + } + } + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + async downloadMetadataArchive() { + try { + const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); + if (downloadBtn) { + downloadBtn.disabled = true; + downloadBtn.textContent = translate('settings.metadataArchive.downloadingButton'); + } + + const response = await fetch('/api/download-metadata-archive', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + showNotification(translate('settings.metadataArchive.downloadSuccess'), 'success'); + + // Update settings in state + state.global.settings.enable_metadata_archive_db = true; + setStorageItem('settings', state.global.settings); + + // Update UI + const enableCheckbox = document.getElementById('enableMetadataArchive'); + if (enableCheckbox) { + enableCheckbox.checked = true; + } + + await this.updateMetadataArchiveStatus(); + } else { + showNotification(translate('settings.metadataArchive.downloadError') + ': ' + data.error, 'error'); + } + } catch (error) { + console.error('Error downloading metadata archive:', error); + showNotification(translate('settings.metadataArchive.downloadError') + ': ' + error.message, 'error'); + } finally { + const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); + if (downloadBtn) { + downloadBtn.disabled = false; + downloadBtn.textContent = translate('settings.metadataArchive.downloadButton'); + } + } + } + + async removeMetadataArchive() { + if (!confirm(translate('settings.metadataArchive.removeConfirm'))) { + return; + } + + try { + const removeBtn = document.getElementById('removeMetadataArchiveBtn'); + if (removeBtn) { + removeBtn.disabled = true; + removeBtn.textContent = translate('settings.metadataArchive.removingButton'); + } + + const response = await fetch('/api/remove-metadata-archive', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + showNotification(translate('settings.metadataArchive.removeSuccess'), 'success'); + + // Update settings in state + state.global.settings.enable_metadata_archive_db = false; + setStorageItem('settings', state.global.settings); + + // Update UI + const enableCheckbox = document.getElementById('enableMetadataArchive'); + if (enableCheckbox) { + enableCheckbox.checked = false; + } + + await this.updateMetadataArchiveStatus(); + } else { + showNotification(translate('settings.metadataArchive.removeError') + ': ' + data.error, 'error'); + } + } catch (error) { + console.error('Error removing metadata archive:', error); + showNotification(translate('settings.metadataArchive.removeError') + ': ' + error.message, 'error'); + } finally { + const removeBtn = document.getElementById('removeMetadataArchiveBtn'); + if (removeBtn) { + removeBtn.disabled = false; + removeBtn.textContent = translate('settings.metadataArchive.removeButton'); + } + } + } async saveInputSetting(elementId, settingKey) { const element = document.getElementById(elementId); diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 2b9be5e7..a2f85716 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -419,6 +419,70 @@ + + +
+

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

+ +
+
+
+ +
+
+ +
+
+
+ {{ t('settings.metadataArchive.enableArchiveDbHelp') }} +
+
+ +
+
+
+ +
+
+ +
+
+
+ {{ t('settings.metadataArchive.providerPriorityHelp') }} +
+
+ +
+ +
+ +
+
+
+ +
+
+ + +
+
+
+ {{ t('settings.metadataArchive.managementHelp') }} +
+
+
\ No newline at end of file