first commit
This commit is contained in:
301
pytorrent/db.py
Normal file
301
pytorrent/db.py
Normal file
@@ -0,0 +1,301 @@
|
||||
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,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
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,
|
||||
table_columns_json TEXT,
|
||||
keyboard_json TEXT,
|
||||
mobile_mode INTEGER DEFAULT 0,
|
||||
peers_refresh_seconds INTEGER DEFAULT 0,
|
||||
port_check_enabled INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(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,
|
||||
is_remote INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
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,
|
||||
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 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,
|
||||
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,
|
||||
last_error TEXT,
|
||||
last_checked_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,
|
||||
save_path TEXT,
|
||||
label TEXT,
|
||||
start INTEGER DEFAULT 1,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_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,
|
||||
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,
|
||||
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 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 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 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 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 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
|
||||
);
|
||||
"""
|
||||
|
||||
MIGRATIONS = [
|
||||
"ALTER TABLE user_preferences ADD COLUMN mobile_mode INTEGER DEFAULT 0",
|
||||
"ALTER TABLE user_preferences ADD COLUMN peers_refresh_seconds INTEGER DEFAULT 0",
|
||||
"ALTER TABLE user_preferences ADD COLUMN port_check_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 rtorrent_profiles ADD COLUMN max_parallel_jobs INTEGER DEFAULT 5",
|
||||
"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 started_at TEXT",
|
||||
"ALTER TABLE jobs ADD COLUMN finished_at TEXT",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
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 journal_mode = WAL")
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
with connect() as conn:
|
||||
conn.executescript(SCHEMA)
|
||||
for sql in MIGRATIONS:
|
||||
try:
|
||||
conn.execute(sql)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
now = utcnow()
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users(id, username, password_hash, created_at) VALUES(1, 'default', NULL, ?)",
|
||||
(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 default_user_id() -> int:
|
||||
return 1
|
||||
Reference in New Issue
Block a user