@ -1,5 +1,6 @@
import json
import os
import sqlite3
from typing import Dict , Any , Optional
from cryptography . fernet import Fernet
from pathlib import Path
@ -9,6 +10,7 @@ class ConfigManager:
self . config_path = Path ( config_path )
self . config_data : Dict [ str , Any ] = { }
self . encryption_key : Optional [ bytes ] = None
self . database_path = Path ( " database/music_library.db " ) # Hardcoded - same as MusicDatabase
self . _load_config ( )
def _get_encryption_key ( self ) - > bytes :
@ -23,16 +25,202 @@ class ConfigManager:
key_file . chmod ( 0o600 )
return key
def _load_config ( self ) :
if not self . config_path . exists ( ) :
raise FileNotFoundError ( f " Configuration file not found: { self . config_path } " )
def _ensure_database_exists ( self ) :
""" Ensure database file and metadata table exist """
try :
# Create database directory if it doesn't exist
self . database_path . parent . mkdir ( parents = True , exist_ok = True )
# Connect to database (creates file if it doesn't exist)
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 :
self . config_data = json . load ( 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 ) :
""" Save configuration to database """
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 :
keys = key . split ( ' . ' )