258 lines
8.2 KiB
Python
258 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Settings Manager for Media Downloader
|
|
Handles settings storage in database with JSON file compatibility
|
|
"""
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Any, Union, Tuple
|
|
from contextlib import contextmanager
|
|
import threading
|
|
from modules.universal_logger import get_logger
|
|
|
|
logger = get_logger('SettingsManager')
|
|
|
|
|
|
class SettingsManager:
|
|
"""Manage application settings in database (thread-safe)"""
|
|
|
|
def __init__(self, db_path: str):
|
|
"""
|
|
Initialize settings manager
|
|
|
|
Args:
|
|
db_path: Path to SQLite database
|
|
"""
|
|
self.db_path = db_path
|
|
self._write_lock = threading.RLock() # Reentrant lock for write operations
|
|
self._create_tables()
|
|
|
|
@contextmanager
|
|
def _get_connection(self, for_write: bool = False):
|
|
"""Get database connection (thread-safe)"""
|
|
conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
if for_write:
|
|
with self._write_lock:
|
|
yield conn
|
|
else:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
def _create_tables(self):
|
|
"""Create settings table if it doesn't exist"""
|
|
with self._get_connection(for_write=True) as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
value_type TEXT NOT NULL,
|
|
category TEXT,
|
|
description TEXT,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_by TEXT DEFAULT 'system'
|
|
)
|
|
''')
|
|
|
|
# Create index for category lookups
|
|
cursor.execute('''
|
|
CREATE INDEX IF NOT EXISTS idx_settings_category
|
|
ON settings(category)
|
|
''')
|
|
|
|
conn.commit()
|
|
logger.info("Settings tables initialized")
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""
|
|
Get a setting value
|
|
|
|
Args:
|
|
key: Setting key (supports dot notation, e.g., 'instagram.enabled')
|
|
default: Default value if not found
|
|
|
|
Returns:
|
|
Setting value or default
|
|
"""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT value, value_type FROM settings WHERE key = ?', (key,))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
return default
|
|
|
|
value, value_type = row['value'], row['value_type']
|
|
return self._deserialize_value(value, value_type)
|
|
|
|
def set(self, key: str, value: Any, category: str = None,
|
|
description: str = None, updated_by: str = 'system'):
|
|
"""
|
|
Set a setting value
|
|
|
|
Args:
|
|
key: Setting key
|
|
value: Setting value (will be serialized to JSON if needed)
|
|
category: Optional category
|
|
description: Optional description
|
|
updated_by: Who updated the setting
|
|
"""
|
|
value_str, value_type = self._serialize_value(value)
|
|
|
|
with self._get_connection(for_write=True) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
INSERT OR REPLACE INTO settings
|
|
(key, value, value_type, category, description, updated_at, updated_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', (key, value_str, value_type, category, description,
|
|
datetime.now().isoformat(), updated_by))
|
|
conn.commit()
|
|
logger.debug(f"Setting updated: {key} = {value_str[:100]}")
|
|
|
|
def get_category(self, category: str) -> Dict[str, Any]:
|
|
"""
|
|
Get all settings in a category
|
|
|
|
Args:
|
|
category: Category name
|
|
|
|
Returns:
|
|
Dictionary of settings
|
|
"""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT key, value, value_type
|
|
FROM settings
|
|
WHERE category = ?
|
|
''', (category,))
|
|
|
|
result = {}
|
|
for row in cursor.fetchall():
|
|
key = row['key']
|
|
value = self._deserialize_value(row['value'], row['value_type'])
|
|
result[key] = value
|
|
|
|
return result
|
|
|
|
def get_all(self) -> Dict[str, Any]:
|
|
"""
|
|
Get all settings as a nested dictionary
|
|
|
|
Returns:
|
|
Nested dictionary of all settings
|
|
"""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT key, value, value_type FROM settings')
|
|
|
|
result = {}
|
|
for row in cursor.fetchall():
|
|
key = row['key']
|
|
value = self._deserialize_value(row['value'], row['value_type'])
|
|
|
|
# Support nested keys like 'instagram.enabled'
|
|
self._set_nested(result, key, value)
|
|
|
|
return result
|
|
|
|
def delete(self, key: str):
|
|
"""Delete a setting"""
|
|
with self._get_connection(for_write=True) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('DELETE FROM settings WHERE key = ?', (key,))
|
|
conn.commit()
|
|
logger.debug(f"Setting deleted: {key}")
|
|
|
|
def migrate_from_json(self, json_path: str):
|
|
"""
|
|
Migrate settings from JSON file to database
|
|
|
|
Args:
|
|
json_path: Path to settings.json file
|
|
"""
|
|
json_file = Path(json_path)
|
|
if not json_file.exists():
|
|
logger.warning(f"JSON file not found: {json_path}")
|
|
return
|
|
|
|
with open(json_file, 'r') as f:
|
|
settings = json.load(f)
|
|
|
|
# Flatten and store settings
|
|
self._migrate_dict(settings, prefix='', category='root')
|
|
logger.info(f"Settings migrated from {json_path}")
|
|
|
|
def _migrate_dict(self, data: Dict, prefix: str = '', category: str = None):
|
|
"""Recursively migrate nested dictionary"""
|
|
for key, value in data.items():
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
|
|
if isinstance(value, dict):
|
|
# Store the entire dict as a value
|
|
self.set(full_key, value, category=category or key)
|
|
else:
|
|
# Store primitive value
|
|
self.set(full_key, value, category=category or prefix.split('.')[0])
|
|
|
|
def export_to_json(self, json_path: str):
|
|
"""
|
|
Export settings to JSON file
|
|
|
|
Args:
|
|
json_path: Path to save settings.json
|
|
"""
|
|
settings = self.get_all()
|
|
|
|
with open(json_path, 'w') as f:
|
|
json.dump(settings, f, indent=2)
|
|
|
|
logger.info(f"Settings exported to {json_path}")
|
|
|
|
def _serialize_value(self, value: Any) -> Tuple[str, str]:
|
|
"""
|
|
Serialize value to string and determine type
|
|
|
|
Returns:
|
|
Tuple of (value_string, value_type)
|
|
"""
|
|
if isinstance(value, bool):
|
|
return (json.dumps(value), 'boolean')
|
|
elif isinstance(value, int):
|
|
return (json.dumps(value), 'number')
|
|
elif isinstance(value, float):
|
|
return (json.dumps(value), 'number')
|
|
elif isinstance(value, str):
|
|
return (value, 'string')
|
|
elif isinstance(value, (dict, list)):
|
|
return (json.dumps(value), 'object' if isinstance(value, dict) else 'array')
|
|
else:
|
|
return (json.dumps(value), 'object')
|
|
|
|
def _deserialize_value(self, value_str: str, value_type: str) -> Any:
|
|
"""Deserialize value from string"""
|
|
if value_type == 'string':
|
|
return value_str
|
|
else:
|
|
return json.loads(value_str)
|
|
|
|
def _set_nested(self, data: Dict, key: str, value: Any):
|
|
"""Set value in nested dictionary using dot notation"""
|
|
parts = key.split('.')
|
|
current = data
|
|
|
|
for part in parts[:-1]:
|
|
if part not in current:
|
|
current[part] = {}
|
|
current = current[part]
|
|
|
|
current[parts[-1]] = value
|