first commit
This commit is contained in:
428
pytorrent/services/preferences.py
Normal file
428
pytorrent/services/preferences.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from ..db import connect, utcnow, default_user_id
|
||||
from . import auth
|
||||
|
||||
BOOTSTRAP_THEMES = {
|
||||
"default": "Default Bootstrap",
|
||||
"flatly": "Flatly",
|
||||
"litera": "Litera",
|
||||
"lumen": "Lumen",
|
||||
"minty": "Minty",
|
||||
"sketchy": "Sketchy",
|
||||
"solar": "Solar",
|
||||
"spacelab": "Spacelab",
|
||||
"united": "United",
|
||||
"zephyr": "Zephyr",
|
||||
}
|
||||
|
||||
FONT_FAMILIES = {
|
||||
"default": "Theme default",
|
||||
"adwaita-mono": "Adwaita Mono",
|
||||
"inter": "Inter",
|
||||
"system-ui": "System UI",
|
||||
"source-sans-3": "Source Sans 3",
|
||||
"jetbrains-mono": "JetBrains Mono",
|
||||
}
|
||||
|
||||
# Note: Backend owns the recommended torrent table layout so frontend builds do not duplicate presets.
|
||||
RECOMMENDED_TABLE_COLUMNS = {
|
||||
"hidden": ["hash", "priority", "hashing", "active", "message", "complete", "state", "ratio_group"],
|
||||
"shown": ["down_total", "to_download", "up_total", "created"],
|
||||
"mobile": {
|
||||
"status": True, "size": True, "progress": True, "down_rate": True, "up_rate": True,
|
||||
"eta": True, "seeds": True, "peers": True, "ratio": True, "path": True, "label": True,
|
||||
"ratio_group": False, "down_total": True, "to_download": True, "up_total": True,
|
||||
"created": False, "priority": False, "state": False, "active": False, "complete": False,
|
||||
"hashing": False, "message": False, "hash": False,
|
||||
},
|
||||
"mobileSmartFiltersEnabled": False,
|
||||
"widths": {
|
||||
"select": 44, "name": 389, "status": 83, "size": 75, "progress": 177,
|
||||
"down_rate": 60, "up_rate": 55, "eta": 53, "seeds": 44, "peers": 49,
|
||||
"ratio": 47, "path": 135, "label": 67, "ratio_group": 87,
|
||||
"down_total": 82, "to_download": 89, "up_total": 44, "created": 150,
|
||||
"priority": 80, "state": 70, "active": 70, "complete": 82, "hashing": 82,
|
||||
"message": 220, "hash": 280,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def recommended_table_columns_json() -> str:
|
||||
return json.dumps(RECOMMENDED_TABLE_COLUMNS, separators=(",", ":"))
|
||||
|
||||
|
||||
def apply_recommended_table_columns(user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
get_preferences(user_id)
|
||||
now = utcnow()
|
||||
value = recommended_table_columns_json()
|
||||
with connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE user_preferences SET table_columns_json=?, updated_at=? WHERE user_id=?",
|
||||
(value, now, user_id),
|
||||
)
|
||||
return get_preferences(user_id)
|
||||
|
||||
def bootstrap_css_url(theme: str | None) -> str:
|
||||
from .frontend_assets import bootstrap_css_path
|
||||
|
||||
return bootstrap_css_path(theme)
|
||||
|
||||
|
||||
def _int_setting(data: dict, key: str, default: int, minimum: int, maximum: int) -> int:
|
||||
try:
|
||||
value = int(data.get(key) if data.get(key) is not None else default)
|
||||
except (TypeError, ValueError):
|
||||
value = default
|
||||
return max(minimum, min(maximum, value))
|
||||
|
||||
|
||||
def list_profiles(user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
visible = auth.visible_profile_ids(user_id)
|
||||
with connect() as conn:
|
||||
if visible is None:
|
||||
return conn.execute(
|
||||
"SELECT * FROM rtorrent_profiles ORDER BY is_default DESC, name COLLATE NOCASE"
|
||||
).fetchall()
|
||||
if not visible:
|
||||
return []
|
||||
placeholders = ",".join("?" for _ in visible)
|
||||
return conn.execute(
|
||||
f"SELECT * FROM rtorrent_profiles WHERE id IN ({placeholders}) ORDER BY is_default DESC, name COLLATE NOCASE",
|
||||
tuple(visible),
|
||||
).fetchall()
|
||||
|
||||
|
||||
def get_profile(profile_id: int, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
if not auth.can_access_profile(profile_id, user_id):
|
||||
return None
|
||||
with connect() as conn:
|
||||
return conn.execute("SELECT * FROM rtorrent_profiles WHERE id=?", (profile_id,)).fetchone()
|
||||
|
||||
|
||||
def active_profile(user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
with connect() as conn:
|
||||
pref = conn.execute("SELECT active_rtorrent_id FROM user_preferences WHERE user_id=?", (user_id,)).fetchone()
|
||||
if pref and pref.get("active_rtorrent_id") and auth.can_access_profile(int(pref["active_rtorrent_id"]), user_id):
|
||||
row = conn.execute("SELECT * FROM rtorrent_profiles WHERE id=?", (pref["active_rtorrent_id"],)).fetchone()
|
||||
if row:
|
||||
return row
|
||||
profiles = list_profiles(user_id)
|
||||
return profiles[0] if profiles else None
|
||||
|
||||
|
||||
def save_profile(data: dict, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
now = utcnow()
|
||||
name = str(data.get("name") or "rTorrent").strip()
|
||||
scgi_url = str(data.get("scgi_url") or "").strip()
|
||||
timeout = _int_setting(data, "timeout_seconds", 5, 1, 300)
|
||||
max_parallel = _int_setting(data, "max_parallel_jobs", 5, 1, 64)
|
||||
light_parallel = _int_setting(data, "light_parallel_jobs", 4, 1, 64)
|
||||
light_timeout = _int_setting(data, "light_job_timeout_seconds", 300, 30, 86400)
|
||||
heavy_timeout = _int_setting(data, "heavy_job_timeout_seconds", 7200, 300, 172800)
|
||||
pending_timeout = _int_setting(data, "pending_job_timeout_seconds", 900, 60, 86400)
|
||||
is_remote = 1 if data.get("is_remote") else 0
|
||||
is_default = 1 if data.get("is_default") else 0
|
||||
if not scgi_url.startswith("scgi://"):
|
||||
raise ValueError("SCGI URL must start with scgi://")
|
||||
with connect() as conn:
|
||||
if is_default:
|
||||
conn.execute("UPDATE rtorrent_profiles SET is_default=0 WHERE user_id=?", (user_id,))
|
||||
cur = conn.execute(
|
||||
"INSERT INTO rtorrent_profiles(user_id,name,scgi_url,is_default,timeout_seconds,max_parallel_jobs,light_parallel_jobs,light_job_timeout_seconds,heavy_job_timeout_seconds,pending_job_timeout_seconds,is_remote,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(user_id, name, scgi_url, is_default, timeout, max_parallel, light_parallel, light_timeout, heavy_timeout, pending_timeout, is_remote, now, now),
|
||||
)
|
||||
profile_id = cur.lastrowid
|
||||
pref = conn.execute("SELECT active_rtorrent_id FROM user_preferences WHERE user_id=?", (user_id,)).fetchone()
|
||||
if not pref or not pref.get("active_rtorrent_id") or is_default:
|
||||
conn.execute(
|
||||
"UPDATE user_preferences SET active_rtorrent_id=?, updated_at=? WHERE user_id=?",
|
||||
(profile_id, now, user_id),
|
||||
)
|
||||
return conn.execute("SELECT * FROM rtorrent_profiles WHERE id=?", (profile_id,)).fetchone()
|
||||
|
||||
|
||||
def update_profile(profile_id: int, data: dict, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
now = utcnow()
|
||||
name = str(data.get("name") or "rTorrent").strip()
|
||||
scgi_url = str(data.get("scgi_url") or "").strip()
|
||||
timeout = _int_setting(data, "timeout_seconds", 5, 1, 300)
|
||||
max_parallel = _int_setting(data, "max_parallel_jobs", 5, 1, 64)
|
||||
light_parallel = _int_setting(data, "light_parallel_jobs", 4, 1, 64)
|
||||
light_timeout = _int_setting(data, "light_job_timeout_seconds", 300, 30, 86400)
|
||||
heavy_timeout = _int_setting(data, "heavy_job_timeout_seconds", 7200, 300, 172800)
|
||||
pending_timeout = _int_setting(data, "pending_job_timeout_seconds", 900, 60, 86400)
|
||||
is_remote = 1 if data.get("is_remote") else 0
|
||||
is_default = 1 if data.get("is_default") else 0
|
||||
if not scgi_url.startswith("scgi://"):
|
||||
raise ValueError("SCGI URL must start with scgi://")
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT id FROM rtorrent_profiles WHERE id=?", (profile_id,)).fetchone()
|
||||
if not row or not auth.can_write_profile(profile_id, user_id):
|
||||
raise ValueError("Profil nie istnieje")
|
||||
if is_default:
|
||||
conn.execute("UPDATE rtorrent_profiles SET is_default=0 WHERE user_id=?", (user_id,))
|
||||
conn.execute(
|
||||
"UPDATE rtorrent_profiles SET name=?, scgi_url=?, is_default=?, timeout_seconds=?, max_parallel_jobs=?, light_parallel_jobs=?, light_job_timeout_seconds=?, heavy_job_timeout_seconds=?, pending_job_timeout_seconds=?, is_remote=?, updated_at=? WHERE id=?",
|
||||
(name, scgi_url, is_default, timeout, max_parallel, light_parallel, light_timeout, heavy_timeout, pending_timeout, is_remote, now, profile_id),
|
||||
)
|
||||
return conn.execute("SELECT * FROM rtorrent_profiles WHERE id=?", (profile_id,)).fetchone()
|
||||
|
||||
|
||||
def delete_profile(profile_id: int, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
auth.require_profile_write(profile_id)
|
||||
with connect() as conn:
|
||||
conn.execute("DELETE FROM rtorrent_profiles WHERE id=?", (profile_id,))
|
||||
active = active_profile(user_id)
|
||||
conn.execute(
|
||||
"UPDATE user_preferences SET active_rtorrent_id=?, updated_at=? WHERE user_id=?",
|
||||
(active["id"] if active else None, utcnow(), user_id),
|
||||
)
|
||||
|
||||
|
||||
def activate_profile(profile_id: int, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT id FROM rtorrent_profiles WHERE id=?", (profile_id,)).fetchone()
|
||||
if not row or not auth.can_access_profile(profile_id, user_id):
|
||||
raise ValueError("Profil nie istnieje")
|
||||
conn.execute(
|
||||
"UPDATE user_preferences SET active_rtorrent_id=?, updated_at=? WHERE user_id=?",
|
||||
(profile_id, utcnow(), user_id),
|
||||
)
|
||||
return get_profile(profile_id, user_id)
|
||||
|
||||
|
||||
|
||||
def export_profiles(user_id: int | None = None) -> dict:
|
||||
profiles = [dict(row) for row in list_profiles(user_id)]
|
||||
for p in profiles:
|
||||
p.pop("id", None)
|
||||
p.pop("user_id", None)
|
||||
p.pop("created_at", None)
|
||||
p.pop("updated_at", None)
|
||||
return {"version": 1, "profiles": profiles}
|
||||
|
||||
|
||||
def import_profiles(payload: dict, user_id: int | None = None) -> list[dict]:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
rows = payload.get("profiles") if isinstance(payload, dict) else None
|
||||
if not isinstance(rows, list):
|
||||
raise ValueError("Invalid profiles export")
|
||||
imported = []
|
||||
for item in rows:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
imported.append(dict(save_profile(item, user_id)))
|
||||
return imported
|
||||
|
||||
|
||||
def _active_profile_id_for_user(user_id: int) -> int | None:
|
||||
profile = active_profile(user_id)
|
||||
try:
|
||||
return int(profile["id"]) if profile else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _clean_disk_paths(value) -> list[str]:
|
||||
try:
|
||||
parsed = json.loads(value if isinstance(value, str) else json.dumps(value or []))
|
||||
except Exception:
|
||||
parsed = []
|
||||
if not isinstance(parsed, list):
|
||||
parsed = []
|
||||
clean: list[str] = []
|
||||
for item in parsed:
|
||||
path = str(item or "").strip()
|
||||
if path and path not in clean:
|
||||
clean.append(path)
|
||||
return clean
|
||||
|
||||
|
||||
def _normalize_disk_monitor(data: dict | None) -> dict:
|
||||
data = data or {}
|
||||
mode = str(data.get("mode") or data.get("disk_monitor_mode") or "default")
|
||||
if mode not in {"default", "selected", "aggregate"}:
|
||||
mode = "default"
|
||||
try:
|
||||
threshold = int(data.get("stop_threshold") if data.get("stop_threshold") is not None else data.get("disk_monitor_stop_threshold") or 98)
|
||||
except (TypeError, ValueError):
|
||||
threshold = 98
|
||||
threshold = max(1, min(100, threshold))
|
||||
return {
|
||||
"disk_monitor_paths_json": json.dumps(_clean_disk_paths(data.get("paths_json") if data.get("paths_json") is not None else data.get("disk_monitor_paths_json"))),
|
||||
"disk_monitor_mode": mode,
|
||||
"disk_monitor_selected_path": str(data.get("selected_path") if data.get("selected_path") is not None else data.get("disk_monitor_selected_path") or "").strip(),
|
||||
"disk_monitor_stop_enabled": 1 if (data.get("stop_enabled") if data.get("stop_enabled") is not None else data.get("disk_monitor_stop_enabled")) else 0,
|
||||
"disk_monitor_stop_threshold": threshold,
|
||||
}
|
||||
|
||||
|
||||
def legacy_disk_monitor_preferences(user_id: int | None = None) -> dict:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT * FROM user_preferences WHERE user_id=?", (user_id,)).fetchone() or {}
|
||||
return _normalize_disk_monitor(row)
|
||||
|
||||
|
||||
def get_disk_monitor_preferences(profile_id: int | None = None, user_id: int | None = None) -> dict:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
profile_id = int(profile_id or _active_profile_id_for_user(user_id) or 0)
|
||||
if not profile_id:
|
||||
return legacy_disk_monitor_preferences(user_id)
|
||||
with connect() as conn:
|
||||
row = conn.execute("SELECT * FROM disk_monitor_preferences WHERE user_id=? AND profile_id=?", (user_id, profile_id)).fetchone()
|
||||
if row:
|
||||
return _normalize_disk_monitor(row)
|
||||
# Backward-compatible seed: existing global disk monitor values become defaults for first use of a profile.
|
||||
return legacy_disk_monitor_preferences(user_id)
|
||||
|
||||
|
||||
def save_disk_monitor_preferences(profile_id: int | None, data: dict, user_id: int | None = None) -> dict:
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
profile_id = int(profile_id or _active_profile_id_for_user(user_id) or 0)
|
||||
if not profile_id:
|
||||
return legacy_disk_monitor_preferences(user_id)
|
||||
current = get_disk_monitor_preferences(profile_id, user_id)
|
||||
merged = dict(current)
|
||||
for key in ("disk_monitor_paths_json", "disk_monitor_mode", "disk_monitor_selected_path", "disk_monitor_stop_enabled", "disk_monitor_stop_threshold"):
|
||||
if key in data:
|
||||
merged[key] = data.get(key)
|
||||
clean = _normalize_disk_monitor(merged)
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO disk_monitor_preferences(user_id,profile_id,paths_json,mode,selected_path,stop_enabled,stop_threshold,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,profile_id) DO UPDATE SET paths_json=excluded.paths_json, mode=excluded.mode, selected_path=excluded.selected_path, stop_enabled=excluded.stop_enabled, stop_threshold=excluded.stop_threshold, updated_at=excluded.updated_at",
|
||||
(user_id, profile_id, clean["disk_monitor_paths_json"], clean["disk_monitor_mode"], clean["disk_monitor_selected_path"], clean["disk_monitor_stop_enabled"], clean["disk_monitor_stop_threshold"], now, now),
|
||||
)
|
||||
return clean
|
||||
|
||||
|
||||
def get_preferences(user_id: int | None = None, profile_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
with connect() as conn:
|
||||
pref = conn.execute("SELECT * FROM user_preferences WHERE user_id=?", (user_id,)).fetchone()
|
||||
if not pref:
|
||||
now = utcnow()
|
||||
conn.execute("INSERT INTO user_preferences(user_id, theme, created_at, updated_at) VALUES(?, 'dark', ?, ?)", (user_id, now, now))
|
||||
pref = conn.execute("SELECT * FROM user_preferences WHERE user_id=?", (user_id,)).fetchone()
|
||||
merged = dict(pref or {})
|
||||
merged.update(get_disk_monitor_preferences(profile_id, user_id))
|
||||
return merged
|
||||
|
||||
|
||||
def save_preferences(data: dict, user_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
allowed_theme = data.get("theme") if data.get("theme") in {"light", "dark"} else None
|
||||
bootstrap_theme = data.get("bootstrap_theme") if data.get("bootstrap_theme") in BOOTSTRAP_THEMES else None
|
||||
font_family = data.get("font_family") if data.get("font_family") in FONT_FAMILIES else None
|
||||
table_columns_json = data.get("table_columns_json")
|
||||
peers_refresh_seconds = data.get("peers_refresh_seconds")
|
||||
port_check_enabled = data.get("port_check_enabled")
|
||||
footer_items_json = data.get("footer_items_json")
|
||||
title_speed_enabled = data.get("title_speed_enabled")
|
||||
tracker_favicons_enabled = data.get("tracker_favicons_enabled")
|
||||
automation_toasts_enabled = data.get("automation_toasts_enabled")
|
||||
smart_queue_toasts_enabled = data.get("smart_queue_toasts_enabled")
|
||||
disk_monitor_paths_json = data.get("disk_monitor_paths_json")
|
||||
disk_monitor_mode = data.get("disk_monitor_mode")
|
||||
disk_monitor_selected_path = data.get("disk_monitor_selected_path")
|
||||
disk_monitor_stop_enabled = data.get("disk_monitor_stop_enabled")
|
||||
disk_monitor_stop_threshold = data.get("disk_monitor_stop_threshold")
|
||||
interface_scale = data.get("interface_scale")
|
||||
detail_panel_height = data.get("detail_panel_height")
|
||||
torrent_sort_json = data.get("torrent_sort_json")
|
||||
active_filter = data.get("active_filter")
|
||||
disk_payload = None
|
||||
if any(value is not None for value in (disk_monitor_paths_json, disk_monitor_mode, disk_monitor_selected_path, disk_monitor_stop_enabled, disk_monitor_stop_threshold)):
|
||||
disk_payload = {
|
||||
"disk_monitor_paths_json": disk_monitor_paths_json,
|
||||
"disk_monitor_mode": disk_monitor_mode,
|
||||
"disk_monitor_selected_path": disk_monitor_selected_path,
|
||||
"disk_monitor_stop_enabled": disk_monitor_stop_enabled,
|
||||
"disk_monitor_stop_threshold": disk_monitor_stop_threshold,
|
||||
}
|
||||
with connect() as conn:
|
||||
now = utcnow()
|
||||
if allowed_theme:
|
||||
conn.execute("UPDATE user_preferences SET theme=?, updated_at=? WHERE user_id=?", (allowed_theme, now, user_id))
|
||||
if bootstrap_theme:
|
||||
conn.execute("UPDATE user_preferences SET bootstrap_theme=?, updated_at=? WHERE user_id=?", (bootstrap_theme, now, user_id))
|
||||
if font_family:
|
||||
conn.execute("UPDATE user_preferences SET font_family=?, updated_at=? WHERE user_id=?", (font_family, now, user_id))
|
||||
if table_columns_json is not None:
|
||||
conn.execute("UPDATE user_preferences SET table_columns_json=?, updated_at=? WHERE user_id=?", (str(table_columns_json), now, user_id))
|
||||
if peers_refresh_seconds is not None:
|
||||
sec = int(peers_refresh_seconds or 0)
|
||||
if sec not in {0, 10, 15, 30, 60}: sec = 0
|
||||
conn.execute("UPDATE user_preferences SET peers_refresh_seconds=?, updated_at=? WHERE user_id=?", (sec, now, user_id))
|
||||
if port_check_enabled is not None:
|
||||
conn.execute("UPDATE user_preferences SET port_check_enabled=?, updated_at=? WHERE user_id=?", (1 if port_check_enabled else 0, now, user_id))
|
||||
if title_speed_enabled is not None:
|
||||
conn.execute("UPDATE user_preferences SET title_speed_enabled=?, updated_at=? WHERE user_id=?", (1 if title_speed_enabled else 0, now, user_id))
|
||||
if tracker_favicons_enabled is not None:
|
||||
conn.execute("UPDATE user_preferences SET tracker_favicons_enabled=?, updated_at=? WHERE user_id=?", (1 if tracker_favicons_enabled else 0, now, user_id))
|
||||
if automation_toasts_enabled is not None:
|
||||
# Note: Lets users silence automation-created toast noise without hiding job/history data.
|
||||
conn.execute("UPDATE user_preferences SET automation_toasts_enabled=?, updated_at=? WHERE user_id=?", (1 if automation_toasts_enabled else 0, now, user_id))
|
||||
if smart_queue_toasts_enabled is not None:
|
||||
# Note: Smart Queue toast noise can be disabled independently from automation notifications.
|
||||
conn.execute("UPDATE user_preferences SET smart_queue_toasts_enabled=?, updated_at=? WHERE user_id=?", (1 if smart_queue_toasts_enabled else 0, now, user_id))
|
||||
if interface_scale is not None:
|
||||
scale = int(interface_scale or 100)
|
||||
if scale < 80: scale = 80
|
||||
if scale > 140: scale = 140
|
||||
conn.execute("UPDATE user_preferences SET interface_scale=?, updated_at=? WHERE user_id=?", (scale, now, user_id))
|
||||
if footer_items_json is not None:
|
||||
# Note: Store only JSON objects so footer visibility can be extended without schema churn.
|
||||
value = footer_items_json if isinstance(footer_items_json, str) else json.dumps(footer_items_json)
|
||||
parsed = json.loads(value or "{}")
|
||||
if not isinstance(parsed, dict):
|
||||
parsed = {}
|
||||
conn.execute("UPDATE user_preferences SET footer_items_json=?, updated_at=? WHERE user_id=?", (json.dumps(parsed), now, user_id))
|
||||
if detail_panel_height is not None:
|
||||
try:
|
||||
height = int(detail_panel_height or 255)
|
||||
except (TypeError, ValueError):
|
||||
height = 255
|
||||
if height < 160: height = 160
|
||||
if height > 720: height = 720
|
||||
conn.execute("UPDATE user_preferences SET detail_panel_height=?, updated_at=? WHERE user_id=?", (height, now, user_id))
|
||||
if torrent_sort_json is not None:
|
||||
# Note: Persist only a compact sort object; unknown keys are ignored on the client.
|
||||
value = torrent_sort_json if isinstance(torrent_sort_json, str) else json.dumps(torrent_sort_json)
|
||||
parsed = json.loads(value or "{}")
|
||||
if not isinstance(parsed, dict):
|
||||
parsed = {}
|
||||
try:
|
||||
direction = int(parsed.get("dir") or 1)
|
||||
except (TypeError, ValueError):
|
||||
direction = 1
|
||||
allowed_sort_keys = {"name", "status", "size", "progress", "down_rate", "up_rate", "eta", "seeds", "peers", "ratio", "path", "label", "ratio_group", "down_total", "to_download", "up_total", "created", "priority", "state", "active", "complete", "hashing", "message", "hash"}
|
||||
sort_key = str(parsed.get("key") or "name")
|
||||
if sort_key not in allowed_sort_keys:
|
||||
sort_key = "name"
|
||||
clean = {"key": sort_key, "dir": 1 if direction >= 0 else -1}
|
||||
conn.execute("UPDATE user_preferences SET torrent_sort_json=?, updated_at=? WHERE user_id=?", (json.dumps(clean), now, user_id))
|
||||
if active_filter is not None:
|
||||
value = str(active_filter or "all").strip()
|
||||
if not value or len(value) > 180:
|
||||
value = "all"
|
||||
allowed_static_filters = {"all", "downloading", "seeding", "paused", "checking", "error", "stopped", "moving"}
|
||||
if value not in allowed_static_filters and not value.startswith("label:") and not value.startswith("tracker:"):
|
||||
value = "all"
|
||||
conn.execute("UPDATE user_preferences SET active_filter=?, updated_at=? WHERE user_id=?", (value, now, user_id))
|
||||
if disk_payload is not None:
|
||||
save_disk_monitor_preferences(_active_profile_id_for_user(user_id), disk_payload, user_id)
|
||||
return get_preferences(user_id)
|
||||
Reference in New Issue
Block a user