from __future__ import annotations from ._shared import * from ..services.rtorrent.diagnostics import profile_diagnostics from ..services import auth @bp.get("/profiles") def profiles_list(): return ok({"profiles": preferences.list_profiles(), "active": preferences.active_profile()}) @bp.post("/profiles") def profiles_create(): try: return ok({"profile": preferences.save_profile(request.json or {})}) except Exception as exc: return jsonify({"ok": False, "error": str(exc)}), 400 @bp.put("/profiles/") def profiles_update(profile_id: int): try: return ok({"profile": preferences.update_profile(profile_id, request.json or {})}) except Exception as exc: return jsonify({"ok": False, "error": str(exc)}), 400 @bp.delete("/profiles/") def profiles_delete(profile_id: int): preferences.delete_profile(profile_id) return ok({"profiles": preferences.list_profiles(), "active": preferences.active_profile()}) @bp.post("/profiles//activate") def profiles_activate(profile_id: int): try: return ok({"profile": preferences.activate_profile(profile_id)}) except Exception as exc: return jsonify({"ok": False, "error": str(exc)}), 404 @bp.post("/profiles/test") def profiles_test_unsaved(): data = request.get_json(silent=True) or {} profile = { "id": data.get("id"), "name": data.get("name") or "test", "scgi_url": data.get("scgi_url") or "", "timeout_seconds": data.get("timeout_seconds") or 5, } return ok({"diagnostics": profile_diagnostics(profile)}) @bp.get("/profiles//diagnostics") def profiles_diagnostics(profile_id: int): profile = preferences.get_profile(profile_id) if not profile: return jsonify({"ok": False, "error": "Profile not found"}), 404 return ok({"diagnostics": profile_diagnostics(profile)}) @bp.get("/profiles/diagnostics") def profiles_diagnostics_all(): rows = preferences.list_profiles() diagnostics = [] for profile in rows: diagnostics.append(profile_diagnostics(profile)) return ok({"diagnostics": diagnostics}) @bp.get("/profiles/export") def profiles_export(): return ok(preferences.export_profiles()) @bp.post("/profiles/import") def profiles_import(): try: rows = preferences.import_profiles(request.get_json(silent=True) or {}) return ok({"profiles": rows}) except Exception as exc: return jsonify({"ok": False, "error": str(exc)}), 400 @bp.get("/preferences") def prefs_get(): return ok({"preferences": preferences.get_preferences()}) @bp.post("/preferences") def prefs_save(): return ok({"preferences": preferences.save_preferences(request.json or {})}) @bp.post("/preferences/table-columns/recommended") def prefs_table_columns_recommended(): # Note: Applies the backend-owned recommended desktop and mobile column layout. return ok({"preferences": preferences.apply_recommended_table_columns()}) @bp.get("/labels") def labels_list(): profile = preferences.active_profile() pid = profile["id"] if profile else None if not pid: return ok({"labels": []}) with connect() as conn: rows = conn.execute( """ SELECT l.*, COALESCE(u.display_name,u.username,u.email,'user ' || l.user_id) AS owner_name FROM labels l LEFT JOIN users u ON u.id=l.user_id WHERE l.profile_id=? ORDER BY l.name COLLATE NOCASE, l.id """, (pid,), ).fetchall() return ok({"labels": rows}) @bp.post("/labels") def labels_save(): profile = preferences.active_profile() if not profile: return jsonify({"ok": False, "error": "No profile"}), 400 data = request.get_json(silent=True) or {} name = str(data.get("name") or "").strip() if not name: return jsonify({"ok": False, "error": "Missing label name"}), 400 if not auth.can_write_profile(int(profile["id"]), default_user_id()): return jsonify({"ok": False, "error": "No write access to profile"}), 403 now = utcnow() with connect() as conn: existing = conn.execute("SELECT id FROM labels WHERE profile_id=? AND lower(name)=lower(?) ORDER BY id LIMIT 1", (profile["id"], name)).fetchone() if existing: conn.execute("UPDATE labels SET color=?, updated_at=? WHERE id=?", (data.get("color") or "#64748b", now, existing["id"])) else: conn.execute("INSERT INTO labels(user_id,profile_id,name,color,created_at,updated_at) VALUES(?,?,?,?,?,?)", (default_user_id(), profile["id"], name, data.get("color") or "#64748b", now, now)) return labels_list() @bp.delete("/labels/") def labels_delete(label_id: int): profile = preferences.active_profile() pid = profile["id"] if profile else None if not pid or not auth.can_write_profile(int(pid), default_user_id()): return jsonify({"ok": False, "error": "No write access to profile"}), 403 with connect() as conn: conn.execute("DELETE FROM labels WHERE id=? AND profile_id=?", (label_id, pid)) return labels_list() @bp.get("/ratio-groups") def ratio_groups_list(): profile = preferences.active_profile() pid = profile["id"] if profile else None with connect() as conn: rows = conn.execute( """ SELECT g.*, COALESCE(u.display_name,u.username,u.email,'user ' || g.user_id) AS owner_name FROM ratio_groups g LEFT JOIN users u ON u.id=g.user_id WHERE g.profile_id=? ORDER BY g.name COLLATE NOCASE, g.id """, (pid or 0,), ).fetchall() if pid else [] history = conn.execute("SELECT * FROM ratio_history WHERE profile_id=? ORDER BY id DESC LIMIT 50", (pid or 0,)).fetchall() if pid else [] return ok({"groups": rows, "history": history}) @bp.post("/ratio-groups") def ratio_groups_save(): profile = preferences.active_profile() if not profile: return jsonify({"ok": False, "error": "No profile"}), 400 data = request.get_json(silent=True) or {} name = str(data.get("name") or "").strip() if not name: return jsonify({"ok": False, "error": "Missing group name"}), 400 if not auth.can_write_profile(int(profile["id"]), default_user_id()): return jsonify({"ok": False, "error": "No write access to profile"}), 403 now = utcnow() with connect() as conn: existing = conn.execute("SELECT id,user_id FROM ratio_groups WHERE profile_id=? AND lower(name)=lower(?) ORDER BY id LIMIT 1", (profile["id"], name)).fetchone() values = (float(data.get("min_ratio") or 1), float(data.get("max_ratio") or 2), int(data.get("seed_time_minutes") or 0), int(data.get("min_seed_time_minutes") or 0), 1 if data.get("ignore_private", True) else 0, 1 if data.get("ignore_active_upload", True) else 0, int(data.get("active_upload_min_bytes") or 1024), data.get("move_path") or "", data.get("set_label") or "", data.get("action") or "stop", 1 if data.get("enabled", True) else 0, now) if existing: conn.execute( """UPDATE ratio_groups SET min_ratio=?,max_ratio=?,seed_time_minutes=?,min_seed_time_minutes=?,ignore_private=?,ignore_active_upload=?,active_upload_min_bytes=?,move_path=?,set_label=?,action=?,enabled=?,updated_at=? WHERE id=? AND profile_id=?""", (*values, existing["id"], profile["id"]), ) else: conn.execute( """INSERT INTO ratio_groups(user_id,profile_id,name,min_ratio,max_ratio,seed_time_minutes,min_seed_time_minutes,ignore_private,ignore_active_upload,active_upload_min_bytes,move_path,set_label,action,enabled,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", (default_user_id(), profile["id"], name, *values[:-1], now, now), ) return ratio_groups_list() @bp.post("/ratio-groups/check") def ratio_groups_check(): profile = preferences.active_profile() if not profile: return jsonify({"ok": False, "error": "No profile"}), 400 return ok({"result": ratio_rules.check(profile, default_user_id())})