257
modules/settings_manager.py
Normal file
257
modules/settings_manager.py
Normal file
@@ -0,0 +1,257 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user