- Add SQLAlchemy database setup with session management - Add Job model with status, priority and progress tracking - Add QueueManager with priority queue and deduplication - Add SystemSettings model for database-backed configuration - Add SettingsService with caching and defaults - Add SystemMonitor for CPU/RAM/GPU resource monitoring - Add LanguageCode utilities (moved from root)
542 lines
18 KiB
Python
542 lines
18 KiB
Python
"""Settings service for database-backed configuration."""
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from backend.core.database import database
|
|
from backend.core.settings_model import SystemSettings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SettingsService:
|
|
"""
|
|
Service for managing system settings in database.
|
|
|
|
Provides caching and type-safe access to settings.
|
|
Settings are organized by category: general, workers, transcription, scanner, bazarr
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize settings service."""
|
|
self._cache: Dict[str, Any] = {}
|
|
self._cache_valid = False
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""
|
|
Get setting value by key.
|
|
|
|
Args:
|
|
key: Setting key
|
|
default: Default value if not found
|
|
|
|
Returns:
|
|
Parsed setting value or default
|
|
"""
|
|
# Refresh cache if needed
|
|
if not self._cache_valid:
|
|
self._load_cache()
|
|
|
|
return self._cache.get(key, default)
|
|
|
|
def set(self, key: str, value: Any, description: str = None, category: str = None, value_type: str = None) -> bool:
|
|
"""
|
|
Set setting value.
|
|
|
|
Args:
|
|
key: Setting key
|
|
value: Setting value (will be converted to string)
|
|
description: Optional description
|
|
category: Optional category
|
|
value_type: Optional type (string, integer, boolean, float, list)
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
with database.get_session() as session:
|
|
setting = session.query(SystemSettings).filter(SystemSettings.key == key).first()
|
|
|
|
if setting:
|
|
# Update existing
|
|
setting.value = str(value) if value is not None else None
|
|
if description:
|
|
setting.description = description
|
|
if category:
|
|
setting.category = category
|
|
if value_type:
|
|
setting.value_type = value_type
|
|
else:
|
|
# Create new
|
|
setting = SystemSettings(
|
|
key=key,
|
|
value=str(value) if value is not None else None,
|
|
description=description,
|
|
category=category,
|
|
value_type=value_type or "string"
|
|
)
|
|
session.add(setting)
|
|
|
|
session.commit()
|
|
|
|
# Invalidate cache
|
|
self._cache_valid = False
|
|
|
|
logger.info(f"Setting updated: {key}={value}")
|
|
return True
|
|
|
|
def get_by_category(self, category: str) -> List[SystemSettings]:
|
|
"""
|
|
Get all settings in a category.
|
|
|
|
Args:
|
|
category: Category name
|
|
|
|
Returns:
|
|
List of SystemSettings objects
|
|
"""
|
|
with database.get_session() as session:
|
|
settings = session.query(SystemSettings).filter(
|
|
SystemSettings.category == category
|
|
).all()
|
|
|
|
# Detach from session
|
|
for setting in settings:
|
|
session.expunge(setting)
|
|
|
|
return settings
|
|
|
|
def get_all(self) -> List[SystemSettings]:
|
|
"""
|
|
Get all settings.
|
|
|
|
Returns:
|
|
List of SystemSettings objects
|
|
"""
|
|
with database.get_session() as session:
|
|
settings = session.query(SystemSettings).all()
|
|
|
|
# Detach from session
|
|
for setting in settings:
|
|
session.expunge(setting)
|
|
|
|
return settings
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""
|
|
Delete a setting.
|
|
|
|
Args:
|
|
key: Setting key
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
with database.get_session() as session:
|
|
setting = session.query(SystemSettings).filter(SystemSettings.key == key).first()
|
|
|
|
if not setting:
|
|
return False
|
|
|
|
session.delete(setting)
|
|
session.commit()
|
|
|
|
# Invalidate cache
|
|
self._cache_valid = False
|
|
|
|
logger.info(f"Setting deleted: {key}")
|
|
return True
|
|
|
|
def bulk_update(self, settings: Dict[str, Any]) -> bool:
|
|
"""
|
|
Update multiple settings at once.
|
|
|
|
Args:
|
|
settings: Dictionary of key-value pairs
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
with database.get_session() as session:
|
|
for key, value in settings.items():
|
|
setting = session.query(SystemSettings).filter(SystemSettings.key == key).first()
|
|
|
|
if setting:
|
|
setting.value = str(value) if value is not None else None
|
|
else:
|
|
logger.warning(f"Setting not found for bulk update: {key}")
|
|
|
|
session.commit()
|
|
|
|
# Invalidate cache
|
|
self._cache_valid = False
|
|
|
|
logger.info(f"Bulk updated {len(settings)} settings")
|
|
return True
|
|
|
|
def init_default_settings(self):
|
|
"""
|
|
Initialize default settings if they don't exist.
|
|
Called on first run or after database reset.
|
|
"""
|
|
defaults = self._get_default_settings()
|
|
|
|
with database.get_session() as session:
|
|
for key, config in defaults.items():
|
|
existing = session.query(SystemSettings).filter(SystemSettings.key == key).first()
|
|
|
|
if not existing:
|
|
setting = SystemSettings(
|
|
key=key,
|
|
value=str(config["value"]) if config["value"] is not None else None,
|
|
description=config.get("description"),
|
|
category=config.get("category"),
|
|
value_type=config.get("value_type", "string")
|
|
)
|
|
session.add(setting)
|
|
logger.info(f"Created default setting: {key}")
|
|
|
|
session.commit()
|
|
|
|
# Invalidate cache
|
|
self._cache_valid = False
|
|
|
|
logger.info("Default settings initialized")
|
|
|
|
def _load_cache(self):
|
|
"""Load all settings into cache."""
|
|
with database.get_session() as session:
|
|
settings = session.query(SystemSettings).all()
|
|
|
|
self._cache = {}
|
|
for setting in settings:
|
|
self._cache[setting.key] = setting.get_parsed_value()
|
|
|
|
self._cache_valid = True
|
|
|
|
def _get_default_settings(self) -> Dict[str, Dict]:
|
|
"""
|
|
Get default settings configuration.
|
|
|
|
All settings have sensible defaults. Configuration is managed
|
|
through the Web UI Settings page or the Settings API.
|
|
|
|
Returns:
|
|
Dictionary of setting configurations
|
|
"""
|
|
return {
|
|
# === General ===
|
|
"operation_mode": {
|
|
"value": "standalone",
|
|
"description": "Operation mode: standalone, provider, or standalone,provider",
|
|
"category": "general",
|
|
"value_type": "string"
|
|
},
|
|
"library_paths": {
|
|
"value": "",
|
|
"description": "Comma-separated library paths to scan",
|
|
"category": "general",
|
|
"value_type": "list"
|
|
},
|
|
"api_host": {
|
|
"value": "0.0.0.0",
|
|
"description": "API server host",
|
|
"category": "general",
|
|
"value_type": "string"
|
|
},
|
|
"api_port": {
|
|
"value": "8000",
|
|
"description": "API server port",
|
|
"category": "general",
|
|
"value_type": "integer"
|
|
},
|
|
"debug": {
|
|
"value": "false",
|
|
"description": "Enable debug mode",
|
|
"category": "general",
|
|
"value_type": "boolean"
|
|
},
|
|
"setup_completed": {
|
|
"value": "false",
|
|
"description": "Whether setup wizard has been completed",
|
|
"category": "general",
|
|
"value_type": "boolean"
|
|
},
|
|
|
|
# === Workers ===
|
|
"worker_cpu_count": {
|
|
"value": "0",
|
|
"description": "Number of CPU workers to start on boot",
|
|
"category": "workers",
|
|
"value_type": "integer"
|
|
},
|
|
"worker_gpu_count": {
|
|
"value": "0",
|
|
"description": "Number of GPU workers to start on boot",
|
|
"category": "workers",
|
|
"value_type": "integer"
|
|
},
|
|
"concurrent_transcriptions": {
|
|
"value": "2",
|
|
"description": "Maximum concurrent transcriptions",
|
|
"category": "workers",
|
|
"value_type": "integer"
|
|
},
|
|
"worker_healthcheck_interval": {
|
|
"value": "60",
|
|
"description": "Worker health check interval (seconds)",
|
|
"category": "workers",
|
|
"value_type": "integer"
|
|
},
|
|
"worker_auto_restart": {
|
|
"value": "true",
|
|
"description": "Auto-restart failed workers",
|
|
"category": "workers",
|
|
"value_type": "boolean"
|
|
},
|
|
"clear_vram_on_complete": {
|
|
"value": "true",
|
|
"description": "Clear VRAM after job completion",
|
|
"category": "workers",
|
|
"value_type": "boolean"
|
|
},
|
|
|
|
# === Whisper/Transcription ===
|
|
"whisper_model": {
|
|
"value": "medium",
|
|
"description": "Whisper model: tiny, base, small, medium, large-v3, large-v3-turbo",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"model_path": {
|
|
"value": "./models",
|
|
"description": "Path to store Whisper models",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"transcribe_device": {
|
|
"value": "cpu",
|
|
"description": "Device for transcription (cpu, cuda, gpu)",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"cpu_compute_type": {
|
|
"value": "auto",
|
|
"description": "CPU compute type (auto, int8, float32)",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"gpu_compute_type": {
|
|
"value": "auto",
|
|
"description": "GPU compute type (auto, float16, float32, int8_float16, int8)",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"whisper_threads": {
|
|
"value": "4",
|
|
"description": "Number of CPU threads for Whisper",
|
|
"category": "transcription",
|
|
"value_type": "integer"
|
|
},
|
|
"transcribe_or_translate": {
|
|
"value": "transcribe",
|
|
"description": "Default mode: transcribe or translate",
|
|
"category": "transcription",
|
|
"value_type": "string"
|
|
},
|
|
"word_level_highlight": {
|
|
"value": "false",
|
|
"description": "Enable word-level highlighting in subtitles",
|
|
"category": "transcription",
|
|
"value_type": "boolean"
|
|
},
|
|
"detect_language_length": {
|
|
"value": "30",
|
|
"description": "Seconds of audio to use for language detection",
|
|
"category": "transcription",
|
|
"value_type": "integer"
|
|
},
|
|
"detect_language_offset": {
|
|
"value": "0",
|
|
"description": "Offset in seconds for language detection sample",
|
|
"category": "transcription",
|
|
"value_type": "integer"
|
|
},
|
|
|
|
# === Subtitle Settings ===
|
|
"subtitle_language_name": {
|
|
"value": "",
|
|
"description": "Custom subtitle language name",
|
|
"category": "subtitles",
|
|
"value_type": "string"
|
|
},
|
|
"subtitle_language_naming_type": {
|
|
"value": "ISO_639_2_B",
|
|
"description": "Language naming: ISO_639_1, ISO_639_2_T, ISO_639_2_B, NAME, NATIVE",
|
|
"category": "subtitles",
|
|
"value_type": "string"
|
|
},
|
|
"custom_regroup": {
|
|
"value": "cm_sl=84_sl=42++++++1",
|
|
"description": "Custom regrouping algorithm for subtitles",
|
|
"category": "subtitles",
|
|
"value_type": "string"
|
|
},
|
|
|
|
# === Skip Configuration ===
|
|
"skip_if_external_subtitles_exist": {
|
|
"value": "false",
|
|
"description": "Skip if any external subtitle exists",
|
|
"category": "skip",
|
|
"value_type": "boolean"
|
|
},
|
|
"skip_if_target_subtitles_exist": {
|
|
"value": "true",
|
|
"description": "Skip if target language subtitle already exists",
|
|
"category": "skip",
|
|
"value_type": "boolean"
|
|
},
|
|
"skip_if_internal_subtitles_language": {
|
|
"value": "",
|
|
"description": "Skip if internal subtitle in this language exists",
|
|
"category": "skip",
|
|
"value_type": "string"
|
|
},
|
|
"skip_subtitle_languages": {
|
|
"value": "",
|
|
"description": "Pipe-separated language codes to skip",
|
|
"category": "skip",
|
|
"value_type": "list"
|
|
},
|
|
"skip_if_audio_languages": {
|
|
"value": "",
|
|
"description": "Skip if audio track is in these languages",
|
|
"category": "skip",
|
|
"value_type": "list"
|
|
},
|
|
"skip_unknown_language": {
|
|
"value": "false",
|
|
"description": "Skip files with unknown audio language",
|
|
"category": "skip",
|
|
"value_type": "boolean"
|
|
},
|
|
"skip_only_subgen_subtitles": {
|
|
"value": "false",
|
|
"description": "Only skip SubGen-generated subtitles",
|
|
"category": "skip",
|
|
"value_type": "boolean"
|
|
},
|
|
|
|
# === Scanner ===
|
|
"scanner_enabled": {
|
|
"value": "true",
|
|
"description": "Enable library scanner",
|
|
"category": "scanner",
|
|
"value_type": "boolean"
|
|
},
|
|
"scanner_cron": {
|
|
"value": "0 2 * * *",
|
|
"description": "Cron expression for scheduled scans",
|
|
"category": "scanner",
|
|
"value_type": "string"
|
|
},
|
|
"watcher_enabled": {
|
|
"value": "false",
|
|
"description": "Enable real-time file watcher",
|
|
"category": "scanner",
|
|
"value_type": "boolean"
|
|
},
|
|
"auto_scan_enabled": {
|
|
"value": "false",
|
|
"description": "Enable automatic scheduled scanning",
|
|
"category": "scanner",
|
|
"value_type": "boolean"
|
|
},
|
|
"scan_interval_minutes": {
|
|
"value": "30",
|
|
"description": "Scan interval in minutes",
|
|
"category": "scanner",
|
|
"value_type": "integer"
|
|
},
|
|
|
|
# === Bazarr Provider ===
|
|
"bazarr_provider_enabled": {
|
|
"value": "false",
|
|
"description": "Enable Bazarr provider mode",
|
|
"category": "bazarr",
|
|
"value_type": "boolean"
|
|
},
|
|
"bazarr_url": {
|
|
"value": "http://bazarr:6767",
|
|
"description": "Bazarr server URL",
|
|
"category": "bazarr",
|
|
"value_type": "string"
|
|
},
|
|
"bazarr_api_key": {
|
|
"value": "",
|
|
"description": "Bazarr API key",
|
|
"category": "bazarr",
|
|
"value_type": "string"
|
|
},
|
|
"provider_timeout_seconds": {
|
|
"value": "600",
|
|
"description": "Provider request timeout in seconds",
|
|
"category": "bazarr",
|
|
"value_type": "integer"
|
|
},
|
|
"provider_callback_enabled": {
|
|
"value": "true",
|
|
"description": "Enable callback to Bazarr on completion",
|
|
"category": "bazarr",
|
|
"value_type": "boolean"
|
|
},
|
|
"provider_polling_interval": {
|
|
"value": "30",
|
|
"description": "Polling interval for Bazarr jobs",
|
|
"category": "bazarr",
|
|
"value_type": "integer"
|
|
},
|
|
|
|
# === Advanced ===
|
|
"force_detected_language_to": {
|
|
"value": "",
|
|
"description": "Force detected language to specific code",
|
|
"category": "advanced",
|
|
"value_type": "string"
|
|
},
|
|
"preferred_audio_languages": {
|
|
"value": "eng",
|
|
"description": "Pipe-separated preferred audio languages",
|
|
"category": "advanced",
|
|
"value_type": "list"
|
|
},
|
|
"use_path_mapping": {
|
|
"value": "false",
|
|
"description": "Enable path mapping for network shares",
|
|
"category": "advanced",
|
|
"value_type": "boolean"
|
|
},
|
|
"path_mapping_from": {
|
|
"value": "/tv",
|
|
"description": "Path mapping source",
|
|
"category": "advanced",
|
|
"value_type": "string"
|
|
},
|
|
"path_mapping_to": {
|
|
"value": "/Volumes/TV",
|
|
"description": "Path mapping destination",
|
|
"category": "advanced",
|
|
"value_type": "string"
|
|
},
|
|
"lrc_for_audio_files": {
|
|
"value": "true",
|
|
"description": "Generate LRC files for audio-only files",
|
|
"category": "advanced",
|
|
"value_type": "boolean"
|
|
},
|
|
}
|
|
|
|
|
|
# Global settings service instance
|
|
settings_service = SettingsService()
|
|
|