diff --git a/pytorrent/db.py b/pytorrent/db.py index 62dd0e9..a32de2f 100644 --- a/pytorrent/db.py +++ b/pytorrent/db.py @@ -519,219 +519,31 @@ CREATE TABLE IF NOT EXISTS tracker_favicon_cache ( ); """ -MIGRATIONS = [ - "ALTER TABLE api_tokens ADD COLUMN last_used_at TEXT", - "ALTER TABLE users ADD COLUMN email TEXT", - "ALTER TABLE users ADD COLUMN display_name TEXT", - "ALTER TABLE users ADD COLUMN external_auth_provider TEXT", - "ALTER TABLE users ADD COLUMN external_subject TEXT", - "ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'", - "ALTER TABLE users ADD COLUMN is_active INTEGER DEFAULT 1", - "ALTER TABLE users ADD COLUMN updated_at TEXT", - "ALTER TABLE user_preferences ADD COLUMN mobile_mode INTEGER DEFAULT 0", - "ALTER TABLE user_preferences ADD COLUMN compact_torrent_list_enabled INTEGER DEFAULT 0", - "ALTER TABLE user_preferences ADD COLUMN torrent_list_font_size INTEGER DEFAULT 13", - "ALTER TABLE user_preferences ADD COLUMN bootstrap_theme TEXT DEFAULT 'default'", - "ALTER TABLE user_preferences ADD COLUMN font_family TEXT DEFAULT 'default'", - "ALTER TABLE user_preferences ADD COLUMN footer_items_json TEXT", - "ALTER TABLE user_preferences ADD COLUMN title_speed_enabled INTEGER DEFAULT 0", - "ALTER TABLE user_preferences ADD COLUMN interface_scale INTEGER DEFAULT 100", - "ALTER TABLE user_preferences ADD COLUMN detail_panel_height INTEGER DEFAULT 255", - "ALTER TABLE user_preferences ADD COLUMN easter_egg_enabled INTEGER DEFAULT 0", - "ALTER TABLE user_preferences ADD COLUMN easter_egg_loading_image_url TEXT DEFAULT ''", - "ALTER TABLE user_preferences ADD COLUMN easter_egg_click_image_url TEXT DEFAULT ''", - "ALTER TABLE rtorrent_profiles ADD COLUMN max_parallel_jobs INTEGER DEFAULT 5", - "ALTER TABLE rtorrent_profiles ADD COLUMN light_parallel_jobs INTEGER DEFAULT 4", - "ALTER TABLE rtorrent_profiles ADD COLUMN light_job_timeout_seconds INTEGER DEFAULT 300", - "ALTER TABLE rtorrent_profiles ADD COLUMN heavy_job_timeout_seconds INTEGER DEFAULT 7200", - "ALTER TABLE rtorrent_profiles ADD COLUMN pending_job_timeout_seconds INTEGER DEFAULT 900", - "ALTER TABLE rtorrent_profiles ADD COLUMN is_remote INTEGER DEFAULT 0", - "ALTER TABLE jobs ADD COLUMN attempts INTEGER DEFAULT 0", - "ALTER TABLE jobs ADD COLUMN max_attempts INTEGER DEFAULT 2", - "ALTER TABLE jobs ADD COLUMN result_json TEXT", - "ALTER TABLE jobs ADD COLUMN state_json TEXT", - "ALTER TABLE jobs ADD COLUMN progress_current INTEGER DEFAULT 0", - "ALTER TABLE jobs ADD COLUMN progress_total INTEGER DEFAULT 0", - "ALTER TABLE jobs ADD COLUMN heartbeat_at TEXT", - "ALTER TABLE jobs ADD COLUMN started_at TEXT", - "ALTER TABLE jobs ADD COLUMN finished_at TEXT", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_updated ON jobs(status, updated_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_started ON jobs(status, started_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_heartbeat ON jobs(status, heartbeat_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_user_profile_created ON jobs(user_id, profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_profile_status_active ON jobs(profile_id, status)", - "ALTER TABLE automation_rules ADD COLUMN cooldown_minutes INTEGER DEFAULT 60", - "ALTER TABLE rtorrent_config_overrides ADD COLUMN apply_on_start INTEGER DEFAULT 0", - "ALTER TABLE rtorrent_config_overrides ADD COLUMN baseline_value TEXT", - "ALTER TABLE torrent_stats_cache ADD COLUMN updated_epoch REAL DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN manage_stopped INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN min_peers INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN ignore_seed_peer INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN ignore_speed INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_stalled ADD COLUMN timer_key TEXT DEFAULT ''", - "CREATE TABLE IF NOT EXISTS tracker_summary_cache (profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, trackers_json TEXT NOT NULL, updated_at TEXT NOT NULL, updated_epoch REAL DEFAULT 0, PRIMARY KEY(profile_id, torrent_hash))", - "CREATE INDEX IF NOT EXISTS idx_tracker_summary_cache_profile ON tracker_summary_cache(profile_id, updated_epoch)", - "CREATE TABLE IF NOT EXISTS tracker_favicon_cache (domain TEXT PRIMARY KEY, source_url TEXT, file_path TEXT, mime_type TEXT, updated_at TEXT NOT NULL, updated_epoch REAL DEFAULT 0, error TEXT)", - "ALTER TABLE user_preferences ADD COLUMN automation_toasts_enabled INTEGER DEFAULT 1", - "ALTER TABLE user_preferences ADD COLUMN smart_queue_toasts_enabled INTEGER DEFAULT 1", - "ALTER TABLE smart_queue_settings ADD COLUMN cooldown_minutes INTEGER DEFAULT 10", - "ALTER TABLE smart_queue_settings ADD COLUMN last_run_at TEXT", - "ALTER TABLE smart_queue_settings ADD COLUMN refill_enabled INTEGER DEFAULT 1", - "ALTER TABLE smart_queue_settings ADD COLUMN refill_interval_minutes INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN last_refill_at TEXT", - "ALTER TABLE smart_queue_settings ADD COLUMN surge_refill_enabled INTEGER DEFAULT 0", - "ALTER TABLE smart_queue_settings ADD COLUMN surge_refill_interval_minutes INTEGER DEFAULT 1440", - "ALTER TABLE smart_queue_settings ADD COLUMN surge_refill_batch_size INTEGER DEFAULT 2000", - "ALTER TABLE smart_queue_settings ADD COLUMN last_surge_refill_at TEXT", - "ALTER TABLE smart_queue_settings ADD COLUMN stop_batch_size INTEGER DEFAULT 50", - "ALTER TABLE smart_queue_settings ADD COLUMN start_grace_seconds INTEGER DEFAULT 900", - "ALTER TABLE smart_queue_settings ADD COLUMN protect_active_below_cap INTEGER DEFAULT 1", - "ALTER TABLE smart_queue_settings ADD COLUMN prefer_partial_progress INTEGER DEFAULT 1", - "ALTER TABLE smart_queue_settings ADD COLUMN auto_stop_idle INTEGER DEFAULT 0", - "CREATE TABLE IF NOT EXISTS smart_queue_start_grace (profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, started_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id, torrent_hash))", - "ALTER TABLE rss_feeds ADD COLUMN interval_minutes INTEGER DEFAULT 30", - "ALTER TABLE rss_feeds ADD COLUMN next_check_at TEXT", - "ALTER TABLE rss_rules ADD COLUMN exclude_pattern TEXT", - "ALTER TABLE rss_rules ADD COLUMN min_size_mb INTEGER DEFAULT 0", - "ALTER TABLE rss_rules ADD COLUMN max_size_mb INTEGER DEFAULT 0", - "ALTER TABLE rss_rules ADD COLUMN category TEXT", - "ALTER TABLE rss_rules ADD COLUMN quality TEXT", - "ALTER TABLE rss_rules ADD COLUMN season INTEGER", - "ALTER TABLE rss_rules ADD COLUMN episode INTEGER", - "ALTER TABLE ratio_groups ADD COLUMN min_seed_time_minutes INTEGER DEFAULT 0", - "ALTER TABLE ratio_groups ADD COLUMN ignore_private INTEGER DEFAULT 1", - "ALTER TABLE ratio_groups ADD COLUMN ignore_active_upload INTEGER DEFAULT 1", - "ALTER TABLE ratio_groups ADD COLUMN active_upload_min_bytes INTEGER DEFAULT 1024", - "ALTER TABLE ratio_groups ADD COLUMN move_path TEXT", - "ALTER TABLE ratio_groups ADD COLUMN set_label TEXT", - "ALTER TABLE automation_history ADD COLUMN torrent_name TEXT", - "ALTER TABLE automation_history ADD COLUMN rule_name TEXT", - "ALTER TABLE automation_history ADD COLUMN actions_json TEXT", - "ALTER TABLE automation_history ADD COLUMN torrent_hash TEXT", - "CREATE TABLE IF NOT EXISTS rss_history (id INTEGER PRIMARY KEY AUTOINCREMENT, profile_id INTEGER NOT NULL, feed_id INTEGER, rule_id INTEGER, title TEXT, link TEXT, status TEXT NOT NULL, message TEXT, created_at TEXT NOT NULL)", - "CREATE INDEX IF NOT EXISTS idx_rss_history_profile_created ON rss_history(profile_id, created_at)", - "CREATE UNIQUE INDEX IF NOT EXISTS idx_rss_history_unique_success ON rss_history(profile_id, COALESCE(rule_id,0), link) WHERE status IN ('queued','added')", - "CREATE TABLE IF NOT EXISTS ratio_assignments (profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, group_id INTEGER, group_name TEXT, applied_at TEXT, last_status TEXT, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id, torrent_hash))", - "CREATE TABLE IF NOT EXISTS ratio_history (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, group_id INTEGER, group_name TEXT, torrent_hash TEXT NOT NULL, torrent_name TEXT, action TEXT NOT NULL, status TEXT NOT NULL, reason TEXT, details_json TEXT, created_at TEXT NOT NULL)", - "CREATE INDEX IF NOT EXISTS idx_ratio_history_profile_created ON ratio_history(profile_id, created_at)", - "CREATE TABLE IF NOT EXISTS app_backups (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL)", - "CREATE TABLE IF NOT EXISTS disk_monitor_preferences (user_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, paths_json TEXT, mode TEXT DEFAULT 'default', selected_path TEXT, stop_enabled INTEGER DEFAULT 0, stop_threshold INTEGER DEFAULT 98, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(user_id, profile_id), FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id))", - "CREATE TABLE IF NOT EXISTS download_plan_settings (user_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, settings_json TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(user_id, profile_id))", - "CREATE TABLE IF NOT EXISTS download_plan_paused (profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, reason TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id, torrent_hash))", - "CREATE INDEX IF NOT EXISTS idx_download_plan_paused_profile ON download_plan_paused(profile_id, updated_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_created ON jobs(created_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_profile_created ON jobs(profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_rss_feeds_profile_enabled_next ON rss_feeds(profile_id, enabled, next_check_at)", - "CREATE INDEX IF NOT EXISTS idx_rss_rules_profile_enabled ON rss_rules(profile_id, enabled)", - "CREATE INDEX IF NOT EXISTS idx_rss_history_profile_status ON rss_history(profile_id, status)", - "CREATE INDEX IF NOT EXISTS idx_rss_history_profile_status ON rss_history(profile_id, status)", - "CREATE INDEX IF NOT EXISTS idx_ratio_groups_user_profile_enabled ON ratio_groups(user_id, profile_id, enabled)", - "CREATE INDEX IF NOT EXISTS idx_ratio_assignments_profile_status ON ratio_assignments(profile_id, last_status)", - "CREATE INDEX IF NOT EXISTS idx_ratio_history_user_profile_id ON ratio_history(user_id, profile_id, id)", - "CREATE INDEX IF NOT EXISTS idx_smart_queue_exclusions_profile_created ON smart_queue_exclusions(profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_smart_queue_history_profile_created ON smart_queue_history(profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_automation_rules_user_profile_enabled ON automation_rules(user_id, profile_id, enabled)", - "CREATE INDEX IF NOT EXISTS idx_automation_history_user_profile_created ON automation_history(user_id, profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_user_preferences_user ON user_preferences(user_id)", - "CREATE INDEX IF NOT EXISTS idx_rtorrent_profiles_user_default_name ON rtorrent_profiles(user_id, is_default, name COLLATE NOCASE)", - "CREATE TABLE IF NOT EXISTS operation_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, profile_id INTEGER, event_type TEXT NOT NULL, severity TEXT DEFAULT 'info', source TEXT DEFAULT 'system', torrent_hash TEXT, torrent_name TEXT, action TEXT, message TEXT NOT NULL, details_json TEXT, created_at TEXT NOT NULL)", - "CREATE INDEX IF NOT EXISTS idx_operation_logs_profile_created ON operation_logs(profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_operation_logs_user_profile_created ON operation_logs(user_id, profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_operation_logs_event_type ON operation_logs(event_type, created_at)", - "CREATE TABLE IF NOT EXISTS profile_preferences (user_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, table_columns_json TEXT, torrent_sort_json TEXT, active_filter TEXT DEFAULT 'all', peers_refresh_seconds INTEGER DEFAULT 0, port_check_enabled INTEGER DEFAULT 0, tracker_favicons_enabled INTEGER DEFAULT 0, reverse_dns_enabled INTEGER DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(user_id, profile_id), FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id))", - "ALTER TABLE app_backups ADD COLUMN backup_type TEXT DEFAULT 'app'", - 'ALTER TABLE app_backups ADD COLUMN profile_id INTEGER', - 'CREATE TABLE IF NOT EXISTS poller_settings (profile_id INTEGER PRIMARY KEY, settings_json TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id))', - "CREATE TABLE IF NOT EXISTS operation_log_settings (user_id INTEGER NOT NULL, profile_id INTEGER NOT NULL DEFAULT 0, retention_mode TEXT DEFAULT 'days', retention_days INTEGER DEFAULT 30, retention_lines INTEGER DEFAULT 5000, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(user_id, profile_id))", -] -POST_MIGRATION_INDEXES = [ - "CREATE INDEX IF NOT EXISTS idx_api_tokens_active_user ON api_tokens(revoked_at, user_id)", - "CREATE INDEX IF NOT EXISTS idx_user_profile_permissions_user ON user_profile_permissions(user_id, profile_id)", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_updated ON jobs(status, updated_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_started ON jobs(status, started_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_status_heartbeat ON jobs(status, heartbeat_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_user_profile_created ON jobs(user_id, profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_jobs_profile_status_active ON jobs(profile_id, status)", - "CREATE INDEX IF NOT EXISTS idx_operation_logs_profile_created ON operation_logs(profile_id, created_at)", - "CREATE INDEX IF NOT EXISTS idx_operation_logs_user_profile_created ON operation_logs(user_id, profile_id, created_at)", -] +def create_schema(conn: sqlite3.Connection) -> None: + """Create the current database schema without running legacy migrations.""" + conn.executescript(SCHEMA) -PROFILE_ONLY_TABLES = { - "rss_feeds": { - "columns": "id INTEGER PRIMARY KEY AUTOINCREMENT, profile_id INTEGER NOT NULL, name TEXT NOT NULL, url TEXT NOT NULL, enabled INTEGER DEFAULT 1, interval_minutes INTEGER DEFAULT 30, last_error TEXT, last_checked_at TEXT, next_check_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL", - "copy": ["id", "profile_id", "name", "url", "enabled", "interval_minutes", "last_error", "last_checked_at", "next_check_at", "created_at", "updated_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_rss_feeds_profile_enabled_next ON rss_feeds(profile_id, enabled, next_check_at)"], - }, - "rss_rules": { - "columns": "id INTEGER PRIMARY KEY AUTOINCREMENT, profile_id INTEGER NOT NULL, name TEXT NOT NULL, pattern TEXT NOT NULL, exclude_pattern TEXT, min_size_mb INTEGER DEFAULT 0, max_size_mb INTEGER DEFAULT 0, category TEXT, quality TEXT, season INTEGER, episode INTEGER, save_path TEXT, label TEXT, start INTEGER DEFAULT 1, enabled INTEGER DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL", - "copy": ["id", "profile_id", "name", "pattern", "exclude_pattern", "min_size_mb", "max_size_mb", "category", "quality", "season", "episode", "save_path", "label", "start", "enabled", "created_at", "updated_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_rss_rules_profile_enabled ON rss_rules(profile_id, enabled)"], - }, - "rss_history": { - "columns": "id INTEGER PRIMARY KEY AUTOINCREMENT, profile_id INTEGER NOT NULL, feed_id INTEGER, rule_id INTEGER, title TEXT, link TEXT, status TEXT NOT NULL, message TEXT, created_at TEXT NOT NULL", - "copy": ["id", "profile_id", "feed_id", "rule_id", "title", "link", "status", "message", "created_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_rss_history_profile_created ON rss_history(profile_id, created_at)", "CREATE INDEX IF NOT EXISTS idx_rss_history_profile_status ON rss_history(profile_id, status)", "CREATE UNIQUE INDEX IF NOT EXISTS idx_rss_history_unique_success ON rss_history(profile_id, COALESCE(rule_id,0), link) WHERE status IN ('queued','added')"], - }, - "smart_queue_settings": { - "columns": "profile_id INTEGER NOT NULL, enabled INTEGER DEFAULT 0, max_active_downloads INTEGER DEFAULT 5, stalled_seconds INTEGER DEFAULT 300, min_speed_bytes INTEGER DEFAULT 1024, min_seeds INTEGER DEFAULT 1, min_peers INTEGER DEFAULT 0, ignore_seed_peer INTEGER DEFAULT 0, ignore_speed INTEGER DEFAULT 0, manage_stopped INTEGER DEFAULT 0, cooldown_minutes INTEGER DEFAULT 10, last_run_at TEXT, refill_enabled INTEGER DEFAULT 1, refill_interval_minutes INTEGER DEFAULT 0, last_refill_at TEXT, surge_refill_enabled INTEGER DEFAULT 0, surge_refill_interval_minutes INTEGER DEFAULT 1440, surge_refill_batch_size INTEGER DEFAULT 2000, last_surge_refill_at TEXT, stop_batch_size INTEGER DEFAULT 50, start_grace_seconds INTEGER DEFAULT 900, protect_active_below_cap INTEGER DEFAULT 1, prefer_partial_progress INTEGER DEFAULT 1, auto_stop_idle INTEGER DEFAULT 0, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id)", - "copy": ["profile_id", "enabled", "max_active_downloads", "stalled_seconds", "min_speed_bytes", "min_seeds", "min_peers", "ignore_seed_peer", "ignore_speed", "manage_stopped", "cooldown_minutes", "last_run_at", "refill_enabled", "refill_interval_minutes", "last_refill_at", "surge_refill_enabled", "surge_refill_interval_minutes", "surge_refill_batch_size", "last_surge_refill_at", "stop_batch_size", "start_grace_seconds", "protect_active_below_cap", "prefer_partial_progress", "auto_stop_idle", "updated_at"], - "indexes": [], - }, - "smart_queue_exclusions": { - "columns": "profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, reason TEXT, created_at TEXT NOT NULL, PRIMARY KEY(profile_id, torrent_hash)", - "copy": ["profile_id", "torrent_hash", "reason", "created_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_smart_queue_exclusions_profile_created ON smart_queue_exclusions(profile_id, created_at)"], - }, - "smart_queue_history": { - "columns": "id INTEGER PRIMARY KEY AUTOINCREMENT, profile_id INTEGER NOT NULL, event TEXT NOT NULL, paused_count INTEGER DEFAULT 0, resumed_count INTEGER DEFAULT 0, checked_count INTEGER DEFAULT 0, details_json TEXT, created_at TEXT NOT NULL", - "copy": ["id", "profile_id", "event", "paused_count", "resumed_count", "checked_count", "details_json", "created_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_smart_queue_history_profile_created ON smart_queue_history(profile_id, created_at)"], - }, - "rtorrent_config_overrides": { - "columns": "profile_id INTEGER NOT NULL, key TEXT NOT NULL, value TEXT, baseline_value TEXT, apply_on_start INTEGER DEFAULT 0, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id, key)", - "copy": ["profile_id", "key", "value", "baseline_value", "apply_on_start", "updated_at"], - "indexes": ["CREATE INDEX IF NOT EXISTS idx_rtorrent_config_overrides_profile ON rtorrent_config_overrides(profile_id, apply_on_start)"], - }, -} +def seed_default_user(conn: sqlite3.Connection) -> None: + """Ensure the built-in admin user and default preferences exist.""" + now = utcnow() + conn.execute( + "INSERT OR IGNORE INTO users(id, username, password_hash, role, is_active, created_at, updated_at) VALUES(1, 'default', NULL, 'admin', 1, ?, ?)", + (now, now), + ) + conn.execute( + "UPDATE users SET role=COALESCE(role, 'admin'), is_active=COALESCE(is_active, 1), updated_at=COALESCE(updated_at, ?) WHERE id=1", + (now,), + ) + pref = conn.execute("SELECT id FROM user_preferences WHERE user_id=1").fetchone() + if not pref: + conn.execute( + "INSERT INTO user_preferences(user_id, theme, created_at, updated_at) VALUES(1, 'dark', ?, ?)", + (now, now), + ) -def _table_columns(conn, table: str) -> set[str]: - try: - return {str(row["name"]) for row in conn.execute(f"PRAGMA table_info({table})").fetchall()} - except sqlite3.OperationalError: - return set() - - -def _normalize_profile_only_tables(conn) -> None: - """Move operational settings from user scope to profile scope on existing databases.""" - for table, spec in PROFILE_ONLY_TABLES.items(): - columns = _table_columns(conn, table) - if not columns or "user_id" not in columns: - for index_sql in spec["indexes"]: - try: - conn.execute(index_sql) - except sqlite3.OperationalError: - pass - continue - tmp = f"{table}_profile_scope_tmp" - conn.execute("PRAGMA foreign_keys = OFF") - conn.execute(f"DROP TABLE IF EXISTS {tmp}") - conn.execute(f"CREATE TABLE {tmp} ({spec['columns']})") - copy_cols = [col for col in spec["copy"] if col in columns] - if copy_cols: - col_sql = ",".join(copy_cols) - if table in {"smart_queue_settings", "smart_queue_exclusions", "rtorrent_config_overrides"}: - conn.execute(f"INSERT OR REPLACE INTO {tmp}({col_sql}) SELECT {col_sql} FROM {table} WHERE profile_id IS NOT NULL") - else: - conn.execute(f"INSERT INTO {tmp}({col_sql}) SELECT {col_sql} FROM {table} WHERE profile_id IS NOT NULL") - conn.execute(f"DROP TABLE {table}") - conn.execute(f"ALTER TABLE {tmp} RENAME TO {table}") - for index_sql in spec["indexes"]: - conn.execute(index_sql) - conn.execute("PRAGMA foreign_keys = ON") - def utcnow() -> str: return datetime.now(timezone.utc).isoformat(timespec="seconds") @@ -756,37 +568,20 @@ def connect(): def init_db(): + """Initialize SQLite using the current schema only. + + Note: migration execution is intentionally not part of this flow. + """ with connect() as conn: try: conn.execute("PRAGMA journal_mode = WAL") except sqlite3.OperationalError: pass - conn.executescript(SCHEMA) - for sql in MIGRATIONS: - try: - conn.execute(sql) - except sqlite3.OperationalError: - pass - for sql in POST_MIGRATION_INDEXES: - try: - conn.execute(sql) - except sqlite3.OperationalError: - pass - _normalize_profile_only_tables(conn) - now = utcnow() - conn.execute( - "INSERT OR IGNORE INTO users(id, username, password_hash, role, is_active, created_at, updated_at) VALUES(1, 'default', NULL, 'admin', 1, ?, ?)", - (now, now), - ) - conn.execute("UPDATE users SET role=COALESCE(role, 'admin'), is_active=COALESCE(is_active, 1), updated_at=COALESCE(updated_at, ?) WHERE id=1", (now,)) - pref = conn.execute("SELECT id FROM user_preferences WHERE user_id=1").fetchone() - if not pref: - conn.execute( - "INSERT INTO user_preferences(user_id, theme, created_at, updated_at) VALUES(1, 'dark', ?, ?)", - (now, now), - ) + create_schema(conn) + seed_default_user(conn) try: from .services.auth import ensure_admin_user + ensure_admin_user() except Exception: pass diff --git a/pytorrent/migrations.py b/pytorrent/migrations.py new file mode 100644 index 0000000..ac5dd83 --- /dev/null +++ b/pytorrent/migrations.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import sqlite3 + +MIGRATIONS: tuple[str, ...] = () + + +def run_database_migrations(conn: sqlite3.Connection) -> int: + """Run pending database migrations. + + Note: no migrations are currently required because supported databases are + already expected to use the current schema version. + """ + applied = 0 + for sql in MIGRATIONS: + conn.execute(sql) + applied += 1 + return applied