Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

257
modules/settings_manager.py Normal file
View 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