Migrate config storage to SQLite database
ConfigManager now stores configuration in a SQLite database instead of a JSON file. The class supports migration from existing config.json files and falls back to defaults if no configuration is found. This change improves reliability and centralizes configuration management.
This commit is contained in:
parent
bfabd26469
commit
36418e05b4
1 changed files with 220 additions and 32 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -9,6 +10,7 @@ class ConfigManager:
|
||||||
self.config_path = Path(config_path)
|
self.config_path = Path(config_path)
|
||||||
self.config_data: Dict[str, Any] = {}
|
self.config_data: Dict[str, Any] = {}
|
||||||
self.encryption_key: Optional[bytes] = None
|
self.encryption_key: Optional[bytes] = None
|
||||||
|
self.database_path = Path("database/music_library.db") # Hardcoded - same as MusicDatabase
|
||||||
self._load_config()
|
self._load_config()
|
||||||
|
|
||||||
def _get_encryption_key(self) -> bytes:
|
def _get_encryption_key(self) -> bytes:
|
||||||
|
|
@ -23,16 +25,202 @@ class ConfigManager:
|
||||||
key_file.chmod(0o600)
|
key_file.chmod(0o600)
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def _load_config(self):
|
def _ensure_database_exists(self):
|
||||||
if not self.config_path.exists():
|
"""Ensure database file and metadata table exist"""
|
||||||
raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
|
try:
|
||||||
|
# Create database directory if it doesn't exist
|
||||||
|
self.database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(self.config_path, 'r') as f:
|
# Connect to database (creates file if it doesn't exist)
|
||||||
self.config_data = json.load(f)
|
conn = sqlite3.connect(str(self.database_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create metadata table if it doesn't exist
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS metadata (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not ensure database exists: {e}")
|
||||||
|
|
||||||
|
def _load_from_database(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Load configuration from database"""
|
||||||
|
try:
|
||||||
|
self._ensure_database_exists()
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(self.database_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT value FROM metadata WHERE key = 'app_config'")
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row and row[0]:
|
||||||
|
config_data = json.loads(row[0])
|
||||||
|
print("[OK] Configuration loaded from database")
|
||||||
|
return config_data
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not load config from database: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_to_database(self, config_data: Dict[str, Any]) -> bool:
|
||||||
|
"""Save configuration to database"""
|
||||||
|
try:
|
||||||
|
self._ensure_database_exists()
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(self.database_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
config_json = json.dumps(config_data, indent=2)
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT OR REPLACE INTO metadata (key, value, updated_at)
|
||||||
|
VALUES ('app_config', ?, CURRENT_TIMESTAMP)
|
||||||
|
""", (config_json,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: Could not save config to database: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_from_config_file(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Load configuration from config.json file (for migration)"""
|
||||||
|
try:
|
||||||
|
if self.config_path.exists():
|
||||||
|
with open(self.config_path, 'r') as f:
|
||||||
|
config_data = json.load(f)
|
||||||
|
print(f"[OK] Configuration loaded from {self.config_path}")
|
||||||
|
return config_data
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not load config from file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_default_config(self) -> Dict[str, Any]:
|
||||||
|
"""Get default configuration"""
|
||||||
|
return {
|
||||||
|
"active_media_server": "plex",
|
||||||
|
"spotify": {
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"redirect_uri": "http://127.0.0.1:8888/callback"
|
||||||
|
},
|
||||||
|
"tidal": {
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
"redirect_uri": "http://127.0.0.1:8889/tidal/callback"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"base_url": "",
|
||||||
|
"token": "",
|
||||||
|
"auto_detect": True
|
||||||
|
},
|
||||||
|
"jellyfin": {
|
||||||
|
"base_url": "",
|
||||||
|
"api_key": "",
|
||||||
|
"auto_detect": True
|
||||||
|
},
|
||||||
|
"navidrome": {
|
||||||
|
"base_url": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"auto_detect": True
|
||||||
|
},
|
||||||
|
"soulseek": {
|
||||||
|
"slskd_url": "",
|
||||||
|
"api_key": "",
|
||||||
|
"download_path": "./downloads",
|
||||||
|
"transfer_path": "./Transfer"
|
||||||
|
},
|
||||||
|
"listenbrainz": {
|
||||||
|
"token": ""
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"path": "logs/app.log",
|
||||||
|
"level": "INFO"
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"path": "database/music_library.db",
|
||||||
|
"max_workers": 5
|
||||||
|
},
|
||||||
|
"metadata_enhancement": {
|
||||||
|
"enabled": True,
|
||||||
|
"embed_album_art": True
|
||||||
|
},
|
||||||
|
"playlist_sync": {
|
||||||
|
"create_backup": True
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"audio_quality": "flac"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
"""
|
||||||
|
Load configuration with priority:
|
||||||
|
1. Database (primary storage)
|
||||||
|
2. config.json (migration from file-based config)
|
||||||
|
3. Defaults (fresh install)
|
||||||
|
"""
|
||||||
|
# Try loading from database first
|
||||||
|
config_data = self._load_from_database()
|
||||||
|
|
||||||
|
if config_data:
|
||||||
|
# Configuration exists in database
|
||||||
|
self.config_data = config_data
|
||||||
|
return
|
||||||
|
|
||||||
|
# Database is empty - try migration from config.json
|
||||||
|
config_data = self._load_from_config_file()
|
||||||
|
|
||||||
|
if config_data:
|
||||||
|
# Migrate from config.json to database
|
||||||
|
print("[MIGRATE] Migrating configuration from config.json to database...")
|
||||||
|
if self._save_to_database(config_data):
|
||||||
|
print("[OK] Configuration migrated successfully")
|
||||||
|
self.config_data = config_data
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("[WARN] Migration failed - using file-based config")
|
||||||
|
self.config_data = config_data
|
||||||
|
return
|
||||||
|
|
||||||
|
# No config.json either - use defaults
|
||||||
|
print("[INFO] No existing configuration found - using defaults")
|
||||||
|
config_data = self._get_default_config()
|
||||||
|
|
||||||
|
# Try to save defaults to database
|
||||||
|
if self._save_to_database(config_data):
|
||||||
|
print("[OK] Default configuration saved to database")
|
||||||
|
else:
|
||||||
|
print("[WARN] Could not save defaults to database - using in-memory config")
|
||||||
|
|
||||||
|
self.config_data = config_data
|
||||||
|
|
||||||
def _save_config(self):
|
def _save_config(self):
|
||||||
with open(self.config_path, 'w') as f:
|
"""Save configuration to database"""
|
||||||
json.dump(self.config_data, f, indent=2)
|
success = self._save_to_database(self.config_data)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
# Fallback: Try to save to config.json if database fails
|
||||||
|
print("[WARN] Database save failed - attempting file fallback")
|
||||||
|
try:
|
||||||
|
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.config_path, 'w') as f:
|
||||||
|
json.dump(self.config_data, f, indent=2)
|
||||||
|
print("[OK] Configuration saved to config.json as fallback")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to save configuration: {e}")
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
keys = key.split('.')
|
keys = key.split('.')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue