big changes in profiles and users
This commit is contained in:
@@ -58,17 +58,21 @@ def recommended_table_columns_json() -> str:
|
||||
return json.dumps(RECOMMENDED_TABLE_COLUMNS, separators=(",", ":"))
|
||||
|
||||
|
||||
def apply_recommended_table_columns(user_id: int | None = None):
|
||||
def apply_recommended_table_columns(user_id: int | None = None, profile_id: int | None = None):
|
||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||
get_preferences(user_id)
|
||||
profile_id = profile_id or _active_profile_id_for_user(user_id)
|
||||
if not profile_id:
|
||||
return get_preferences(user_id)
|
||||
get_preferences(user_id, profile_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),
|
||||
"INSERT INTO profile_preferences(user_id,profile_id,table_columns_json,created_at,updated_at) VALUES(?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,profile_id) DO UPDATE SET table_columns_json=excluded.table_columns_json, updated_at=excluded.updated_at",
|
||||
(user_id, profile_id, value, now, now),
|
||||
)
|
||||
return get_preferences(user_id)
|
||||
return get_preferences(user_id, profile_id)
|
||||
|
||||
def bootstrap_css_url(theme: str | None) -> str:
|
||||
from .frontend_assets import bootstrap_css_path
|
||||
@@ -317,31 +321,137 @@ def save_disk_monitor_preferences(profile_id: int | None, data: dict, user_id: i
|
||||
return clean
|
||||
|
||||
|
||||
PROFILE_PREFERENCE_COLUMNS = {
|
||||
"table_columns_json",
|
||||
"torrent_sort_json",
|
||||
"active_filter",
|
||||
"peers_refresh_seconds",
|
||||
"port_check_enabled",
|
||||
"tracker_favicons_enabled",
|
||||
"reverse_dns_enabled",
|
||||
}
|
||||
|
||||
|
||||
def _seed_profile_preferences(conn, user_id: int, profile_id: int) -> dict:
|
||||
now = utcnow()
|
||||
legacy = conn.execute("SELECT * FROM user_preferences WHERE user_id=?", (user_id,)).fetchone() or {}
|
||||
row = conn.execute("SELECT * FROM profile_preferences WHERE user_id=? AND profile_id=?", (user_id, profile_id)).fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
# Note: First profile preference row is seeded from legacy user-level values so upgrades keep the current layout/filter behavior.
|
||||
conn.execute(
|
||||
"INSERT INTO profile_preferences(user_id,profile_id,table_columns_json,torrent_sort_json,active_filter,peers_refresh_seconds,port_check_enabled,tracker_favicons_enabled,reverse_dns_enabled,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
user_id,
|
||||
profile_id,
|
||||
legacy.get("table_columns_json"),
|
||||
legacy.get("torrent_sort_json"),
|
||||
legacy.get("active_filter") or "all",
|
||||
int(legacy.get("peers_refresh_seconds") or 0),
|
||||
int(legacy.get("port_check_enabled") or 0),
|
||||
int(legacy.get("tracker_favicons_enabled") or 0),
|
||||
int(legacy.get("reverse_dns_enabled") or 0),
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
return dict(conn.execute("SELECT * FROM profile_preferences WHERE user_id=? AND profile_id=?", (user_id, profile_id)).fetchone() or {})
|
||||
|
||||
|
||||
def get_profile_preferences(user_id: int, profile_id: int | None) -> dict:
|
||||
if not profile_id:
|
||||
return {}
|
||||
with connect() as conn:
|
||||
return _seed_profile_preferences(conn, user_id, int(profile_id))
|
||||
|
||||
|
||||
def save_profile_preferences(user_id: int, profile_id: int | None, data: dict) -> None:
|
||||
if not profile_id:
|
||||
return
|
||||
profile_id = int(profile_id)
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
current = _seed_profile_preferences(conn, user_id, profile_id)
|
||||
updates: dict[str, object] = {}
|
||||
if data.get("table_columns_json") is not None:
|
||||
updates["table_columns_json"] = str(data.get("table_columns_json"))
|
||||
if data.get("peers_refresh_seconds") is not None:
|
||||
sec = int(data.get("peers_refresh_seconds") or 0)
|
||||
updates["peers_refresh_seconds"] = sec if sec in {0, 10, 15, 30, 60} else 0
|
||||
if data.get("port_check_enabled") is not None:
|
||||
updates["port_check_enabled"] = 1 if data.get("port_check_enabled") else 0
|
||||
if data.get("tracker_favicons_enabled") is not None:
|
||||
updates["tracker_favicons_enabled"] = 1 if data.get("tracker_favicons_enabled") else 0
|
||||
if data.get("reverse_dns_enabled") is not None:
|
||||
# Note: Reverse DNS is stored per profile because PTR lookups depend on swarm size and profile network latency.
|
||||
updates["reverse_dns_enabled"] = 1 if data.get("reverse_dns_enabled") else 0
|
||||
if data.get("torrent_sort_json") is not None:
|
||||
value = data.get("torrent_sort_json") if isinstance(data.get("torrent_sort_json"), str) else json.dumps(data.get("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"
|
||||
updates["torrent_sort_json"] = json.dumps({"key": sort_key, "dir": 1 if direction >= 0 else -1})
|
||||
if data.get("active_filter") is not None:
|
||||
value = str(data.get("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"
|
||||
updates["active_filter"] = value
|
||||
if not updates:
|
||||
return
|
||||
merged = {**current, **updates}
|
||||
conn.execute(
|
||||
"INSERT INTO profile_preferences(user_id,profile_id,table_columns_json,torrent_sort_json,active_filter,peers_refresh_seconds,port_check_enabled,tracker_favicons_enabled,reverse_dns_enabled,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?) "
|
||||
"ON CONFLICT(user_id,profile_id) DO UPDATE SET table_columns_json=excluded.table_columns_json, torrent_sort_json=excluded.torrent_sort_json, active_filter=excluded.active_filter, peers_refresh_seconds=excluded.peers_refresh_seconds, port_check_enabled=excluded.port_check_enabled, tracker_favicons_enabled=excluded.tracker_favicons_enabled, reverse_dns_enabled=excluded.reverse_dns_enabled, updated_at=excluded.updated_at",
|
||||
(
|
||||
user_id,
|
||||
profile_id,
|
||||
merged.get("table_columns_json"),
|
||||
merged.get("torrent_sort_json"),
|
||||
merged.get("active_filter") or "all",
|
||||
int(merged.get("peers_refresh_seconds") or 0),
|
||||
int(merged.get("port_check_enabled") or 0),
|
||||
int(merged.get("tracker_favicons_enabled") or 0),
|
||||
int(merged.get("reverse_dns_enabled") or 0),
|
||||
merged.get("created_at") or now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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()
|
||||
profile_id = profile_id or _active_profile_id_for_user(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 = dict(pref or {})
|
||||
if profile_id:
|
||||
merged.update(_seed_profile_preferences(conn, user_id, int(profile_id)))
|
||||
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()
|
||||
profile_id = _active_profile_id_for_user(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")
|
||||
reverse_dns_enabled = data.get("reverse_dns_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")
|
||||
@@ -352,8 +462,6 @@ def save_preferences(data: dict, user_id: int | None = None):
|
||||
interface_scale = data.get("interface_scale")
|
||||
compact_torrent_list_enabled = data.get("compact_torrent_list_enabled")
|
||||
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 = {
|
||||
@@ -371,21 +479,8 @@ def save_preferences(data: dict, user_id: int | None = None):
|
||||
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 reverse_dns_enabled is not None:
|
||||
# Note: Reverse DNS is optional because peer PTR lookups can add latency on busy swarms.
|
||||
conn.execute("UPDATE user_preferences SET reverse_dns_enabled=?, updated_at=? WHERE user_id=?", (1 if reverse_dns_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))
|
||||
@@ -415,30 +510,7 @@ def save_preferences(data: dict, user_id: int | None = None):
|
||||
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))
|
||||
save_profile_preferences(user_id, profile_id, data)
|
||||
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)
|
||||
save_disk_monitor_preferences(profile_id, disk_payload, user_id)
|
||||
return get_preferences(user_id, profile_id)
|
||||
|
||||
Reference in New Issue
Block a user