711 lines
31 KiB
Python
711 lines
31 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timezone
|
|
from .config import DB_PATH
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT,
|
|
email TEXT,
|
|
display_name TEXT,
|
|
external_auth_provider TEXT,
|
|
external_subject TEXT,
|
|
role TEXT DEFAULT 'user',
|
|
is_active INTEGER DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_profile_permissions (
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER NOT NULL DEFAULT 0,
|
|
access_level TEXT NOT NULL DEFAULT 'ro',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY(user_id, profile_id),
|
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
);
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS api_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL,
|
|
token_prefix TEXT NOT NULL,
|
|
last_used_at TEXT,
|
|
revoked_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_api_tokens_user_active ON api_tokens(user_id, revoked_at);
|
|
CREATE INDEX IF NOT EXISTS idx_api_tokens_prefix ON api_tokens(token_prefix);
|
|
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 TABLE IF NOT EXISTS user_preferences (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
theme TEXT DEFAULT 'dark',
|
|
bootstrap_theme TEXT DEFAULT 'default',
|
|
font_family TEXT DEFAULT 'default',
|
|
active_rtorrent_id INTEGER,
|
|
keyboard_json TEXT,
|
|
mobile_mode INTEGER DEFAULT 0,
|
|
compact_torrent_list_enabled INTEGER DEFAULT 0,
|
|
footer_items_json TEXT,
|
|
title_speed_enabled INTEGER DEFAULT 0,
|
|
automation_toasts_enabled INTEGER DEFAULT 1,
|
|
smart_queue_toasts_enabled INTEGER DEFAULT 1,
|
|
interface_scale INTEGER DEFAULT 100,
|
|
detail_panel_height INTEGER DEFAULT 255,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_user_preferences_user ON user_preferences(user_id);
|
|
|
|
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)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_profile_preferences_user_profile ON profile_preferences(user_id, profile_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS rtorrent_profiles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
scgi_url TEXT NOT NULL,
|
|
is_default INTEGER DEFAULT 0,
|
|
timeout_seconds INTEGER DEFAULT 5,
|
|
max_parallel_jobs INTEGER DEFAULT 5,
|
|
light_parallel_jobs INTEGER DEFAULT 4,
|
|
light_job_timeout_seconds INTEGER DEFAULT 300,
|
|
heavy_job_timeout_seconds INTEGER DEFAULT 7200,
|
|
pending_job_timeout_seconds INTEGER DEFAULT 900,
|
|
is_remote INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(user_id) REFERENCES users(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 jobs (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
payload_json TEXT,
|
|
status TEXT NOT NULL,
|
|
attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 2,
|
|
error TEXT,
|
|
result_json TEXT,
|
|
state_json TEXT,
|
|
progress_current INTEGER DEFAULT 0,
|
|
progress_total INTEGER DEFAULT 0,
|
|
heartbeat_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
started_at TEXT,
|
|
finished_at TEXT,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_profile_status ON jobs(profile_id, status, created_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 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 labels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
name TEXT NOT NULL,
|
|
color TEXT DEFAULT '#64748b',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(user_id, profile_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ratio_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
name TEXT NOT NULL,
|
|
min_ratio REAL DEFAULT 1.0,
|
|
max_ratio REAL DEFAULT 2.0,
|
|
seed_time_minutes INTEGER DEFAULT 0,
|
|
min_seed_time_minutes INTEGER DEFAULT 0,
|
|
ignore_private INTEGER DEFAULT 1,
|
|
ignore_active_upload INTEGER DEFAULT 1,
|
|
active_upload_min_bytes INTEGER DEFAULT 1024,
|
|
move_path TEXT,
|
|
set_label TEXT,
|
|
action TEXT DEFAULT 'stop',
|
|
enabled INTEGER DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(user_id, profile_id, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS rss_feeds (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
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
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS rss_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
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
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_rss_feeds_user_profile_enabled_next ON rss_feeds(user_id, profile_id, enabled, next_check_at);
|
|
CREATE INDEX IF NOT EXISTS idx_rss_rules_user_profile_enabled ON rss_rules(user_id, profile_id, enabled);
|
|
|
|
CREATE TABLE IF NOT EXISTS rss_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
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 INDEX IF NOT EXISTS idx_rss_history_user_profile_created ON rss_history(user_id, profile_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_rss_history_user_profile_status ON rss_history(user_id, 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');
|
|
|
|
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 INDEX IF NOT EXISTS idx_ratio_history_user_profile_id ON ratio_history(user_id, profile_id, id);
|
|
CREATE INDEX IF NOT EXISTS idx_ratio_assignments_profile_status ON ratio_assignments(profile_id, last_status);
|
|
CREATE INDEX IF NOT EXISTS idx_ratio_groups_user_profile_enabled ON ratio_groups(user_id, profile_id, enabled);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_backups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
backup_type TEXT DEFAULT 'app',
|
|
profile_id INTEGER,
|
|
payload_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS smart_queue_settings (
|
|
user_id INTEGER NOT NULL,
|
|
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,
|
|
stop_batch_size INTEGER DEFAULT 50,
|
|
start_grace_seconds INTEGER DEFAULT 900,
|
|
protect_active_below_cap INTEGER DEFAULT 1,
|
|
auto_stop_idle INTEGER DEFAULT 0,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY(user_id, profile_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS smart_queue_stalled (
|
|
profile_id INTEGER NOT NULL,
|
|
torrent_hash TEXT NOT NULL,
|
|
first_stalled_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
timer_key TEXT DEFAULT '',
|
|
PRIMARY KEY(profile_id, torrent_hash)
|
|
);
|
|
|
|
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)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS smart_queue_exclusions (
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER NOT NULL,
|
|
torrent_hash TEXT NOT NULL,
|
|
reason TEXT,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY(user_id, profile_id, torrent_hash)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_smart_queue_exclusions_user_profile_created ON smart_queue_exclusions(user_id, profile_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS smart_queue_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
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
|
|
);
|
|
|
|
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_smart_queue_history_user_profile_created ON smart_queue_history(user_id, profile_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS smart_queue_auto_labels (
|
|
profile_id INTEGER NOT NULL,
|
|
torrent_hash TEXT NOT NULL,
|
|
previous_label TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY(profile_id, torrent_hash)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS traffic_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
profile_id INTEGER NOT NULL,
|
|
down_rate INTEGER DEFAULT 0,
|
|
up_rate INTEGER DEFAULT 0,
|
|
total_down INTEGER DEFAULT 0,
|
|
total_up INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_traffic_history_profile_created ON traffic_history(profile_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS transfer_speed_peaks (
|
|
profile_id INTEGER PRIMARY KEY,
|
|
session_started_at TEXT NOT NULL,
|
|
session_down_peak INTEGER DEFAULT 0,
|
|
session_up_peak INTEGER DEFAULT 0,
|
|
session_down_peak_at TEXT,
|
|
session_up_peak_at TEXT,
|
|
all_time_down_peak INTEGER DEFAULT 0,
|
|
all_time_up_peak INTEGER DEFAULT 0,
|
|
all_time_down_peak_at TEXT,
|
|
all_time_up_peak_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(profile_id) REFERENCES rtorrent_profiles(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS automation_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER,
|
|
name TEXT NOT NULL,
|
|
enabled INTEGER DEFAULT 1,
|
|
conditions_json TEXT NOT NULL,
|
|
effects_json TEXT NOT NULL,
|
|
cooldown_minutes INTEGER DEFAULT 60,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_automation_rules_profile_enabled ON automation_rules(profile_id, enabled);
|
|
CREATE INDEX IF NOT EXISTS idx_automation_rules_user_profile_enabled ON automation_rules(user_id, profile_id, enabled);
|
|
CREATE TABLE IF NOT EXISTS automation_rule_state (
|
|
rule_id INTEGER NOT NULL,
|
|
profile_id INTEGER NOT NULL,
|
|
torrent_hash TEXT NOT NULL,
|
|
condition_since_at TEXT,
|
|
last_matched_at TEXT,
|
|
last_applied_at TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY(rule_id, profile_id, torrent_hash)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS automation_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
profile_id INTEGER NOT NULL,
|
|
rule_id INTEGER,
|
|
torrent_hash TEXT,
|
|
torrent_name TEXT,
|
|
rule_name TEXT,
|
|
actions_json TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_automation_history_profile_created ON automation_history(profile_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_automation_history_user_profile_created ON automation_history(user_id, profile_id, created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS rtorrent_config_overrides (
|
|
user_id INTEGER NOT NULL,
|
|
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(user_id, profile_id, key)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_rtorrent_config_overrides_profile ON rtorrent_config_overrides(profile_id, apply_on_start);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
|
|
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 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 TABLE IF NOT EXISTS torrent_stats_cache (
|
|
profile_id INTEGER PRIMARY KEY,
|
|
payload_json TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
updated_epoch REAL DEFAULT 0
|
|
);
|
|
|
|
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 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 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)
|
|
);
|
|
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
|
|
);
|
|
"""
|
|
|
|
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 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 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 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 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, user_id INTEGER NOT NULL, profile_id INTEGER, 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_user_profile_enabled_next ON rss_feeds(user_id, profile_id, enabled, next_check_at)",
|
|
"CREATE INDEX IF NOT EXISTS idx_rss_rules_user_profile_enabled ON rss_rules(user_id, profile_id, enabled)",
|
|
"CREATE INDEX IF NOT EXISTS idx_rss_history_user_profile_created ON rss_history(user_id, profile_id, created_at)",
|
|
"CREATE INDEX IF NOT EXISTS idx_rss_history_user_profile_status ON rss_history(user_id, 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_user_profile_created ON smart_queue_exclusions(user_id, profile_id, created_at)",
|
|
"CREATE INDEX IF NOT EXISTS idx_smart_queue_history_user_profile_created ON smart_queue_history(user_id, 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 utcnow() -> str:
|
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
|
|
|
|
def dict_factory(cursor, row):
|
|
return {col[0]: row[idx] for idx, col in enumerate(cursor.description)}
|
|
|
|
|
|
@contextmanager
|
|
def connect():
|
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(DB_PATH, timeout=30)
|
|
conn.row_factory = dict_factory
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
conn.execute("PRAGMA busy_timeout = 30000")
|
|
conn.execute("PRAGMA synchronous = NORMAL")
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def init_db():
|
|
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
|
|
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),
|
|
)
|
|
try:
|
|
from .services.auth import ensure_admin_user
|
|
ensure_admin_user()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def default_user_id() -> int:
|
|
return 1
|