profile id support in api requests
This commit is contained in:
+1609
-96
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ from flask import Blueprint, jsonify, request, abort, send_file, redirect, Respo
|
|||||||
from ..config import DB_PATH, JOBS_RETENTION_DAYS, SMART_QUEUE_HISTORY_RETENTION_DAYS, LOG_RETENTION_DAYS, WORKERS, PYTORRENT_TMP_DIR
|
from ..config import DB_PATH, JOBS_RETENTION_DAYS, SMART_QUEUE_HISTORY_RETENTION_DAYS, LOG_RETENTION_DAYS, WORKERS, PYTORRENT_TMP_DIR
|
||||||
from ..db import connect, utcnow
|
from ..db import connect, utcnow
|
||||||
from ..services.auth import current_user_id as default_user_id, current_user, list_users, save_user, delete_user, login_user, logout_user, enabled as auth_enabled, require_profile_write, require_admin, is_admin
|
from ..services.auth import current_user_id as default_user_id, current_user, list_users, save_user, delete_user, login_user, logout_user, enabled as auth_enabled, require_profile_write, require_admin, is_admin
|
||||||
from ..services import preferences, rtorrent, torrent_stats, speed_peaks, tracker_cache, rss as rss_service, ratio_rules, backup as backup_service, download_planner, operation_logs, poller_control, database_maintenance
|
from ..services import auth, preferences, rtorrent, torrent_stats, speed_peaks, tracker_cache, rss as rss_service, ratio_rules, backup as backup_service, download_planner, operation_logs, poller_control, database_maintenance
|
||||||
from ..services.torrent_cache import torrent_cache
|
from ..services.torrent_cache import torrent_cache
|
||||||
from ..services.torrent_summary import cached_summary
|
from ..services.torrent_summary import cached_summary
|
||||||
from ..services.workers import enqueue, list_jobs, cancel_job, retry_job, force_job, clear_jobs, emergency_clear_jobs
|
from ..services.workers import enqueue, list_jobs, cancel_job, retry_job, force_job, clear_jobs, emergency_clear_jobs
|
||||||
@@ -39,6 +39,78 @@ from .auth_api import register_auth_routes
|
|||||||
register_auth_routes(bp)
|
register_auth_routes(bp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _request_profile_selector() -> tuple[int | None, str]:
|
||||||
|
"""Return the optional profile selector supplied by external API clients."""
|
||||||
|
payload = {}
|
||||||
|
if request.method in {"POST", "PUT", "PATCH", "DELETE"}:
|
||||||
|
try:
|
||||||
|
payload = request.get_json(silent=True) or {}
|
||||||
|
except Exception:
|
||||||
|
payload = {}
|
||||||
|
profile_id = request.args.get("profile_id") or request.form.get("profile_id") or payload.get("profile_id") or request.headers.get("X-PyTorrent-Profile-Id")
|
||||||
|
profile_name = request.args.get("profile_name") or request.form.get("profile_name") or payload.get("profile_name") or request.headers.get("X-PyTorrent-Profile-Name") or ""
|
||||||
|
try:
|
||||||
|
return (int(profile_id), "") if profile_id not in (None, "") else (None, str(profile_name or "").strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError("profile_id must be an integer")
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_by_name(profile_name: str, user_id: int | None = None):
|
||||||
|
name = str(profile_name or "").strip()
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
user_id = 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 WHERE lower(name)=lower(?) ORDER BY is_default DESC, id LIMIT 1",
|
||||||
|
(name,),
|
||||||
|
).fetchone()
|
||||||
|
if not visible:
|
||||||
|
return None
|
||||||
|
placeholders = ",".join("?" for _ in visible)
|
||||||
|
return conn.execute(
|
||||||
|
f"SELECT * FROM rtorrent_profiles WHERE id IN ({placeholders}) AND lower(name)=lower(?) ORDER BY is_default DESC, id LIMIT 1",
|
||||||
|
(*tuple(visible), name),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def request_profile(require_write: bool = False):
|
||||||
|
"""Resolve API profile context from profile_id/profile_name, then active profile for compatibility."""
|
||||||
|
try:
|
||||||
|
profile_id, profile_name = _request_profile_selector()
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
|
user_id = default_user_id()
|
||||||
|
profile = None
|
||||||
|
if profile_id:
|
||||||
|
profile = preferences.get_profile(int(profile_id), user_id)
|
||||||
|
elif profile_name:
|
||||||
|
profile = _profile_by_name(profile_name, user_id)
|
||||||
|
else:
|
||||||
|
profile = preferences.active_profile(user_id)
|
||||||
|
if not profile and auth.can_access_profile(1, user_id):
|
||||||
|
profile = preferences.get_profile(1, user_id)
|
||||||
|
if not profile and (profile_id or profile_name):
|
||||||
|
abort(404)
|
||||||
|
if not profile:
|
||||||
|
return None
|
||||||
|
pid = int(profile["id"])
|
||||||
|
if require_write and not auth.can_write_profile(pid, user_id):
|
||||||
|
abort(403)
|
||||||
|
if not require_write and not auth.can_access_profile(pid, user_id):
|
||||||
|
abort(403)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def request_profile_id(require_write: bool = False) -> int | None:
|
||||||
|
profile = request_profile(require_write=require_write)
|
||||||
|
return int(profile["id"]) if profile else None
|
||||||
|
|
||||||
|
|
||||||
def _job_profile_id(job_id: str) -> int | None:
|
def _job_profile_id(job_id: str) -> int | None:
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
row = conn.execute("SELECT profile_id FROM jobs WHERE id=?", (job_id,)).fetchone()
|
row = conn.execute("SELECT profile_id FROM jobs WHERE id=?", (job_id,)).fetchone()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def _automation_user_id() -> int:
|
|||||||
@bp.get('/automations')
|
@bp.get('/automations')
|
||||||
def automations_get():
|
def automations_get():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({'rules': [], 'history': [], 'error': 'No profile'})
|
return ok({'rules': [], 'history': [], 'error': 'No profile'})
|
||||||
try:
|
try:
|
||||||
@@ -26,7 +26,7 @@ def automations_get():
|
|||||||
@bp.get('/automations/export')
|
@bp.get('/automations/export')
|
||||||
def automations_export():
|
def automations_export():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -39,7 +39,7 @@ def automations_export():
|
|||||||
@bp.post('/automations/import')
|
@bp.post('/automations/import')
|
||||||
def automations_import():
|
def automations_import():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -55,7 +55,7 @@ def automations_import():
|
|||||||
@bp.post('/automations')
|
@bp.post('/automations')
|
||||||
def automations_save():
|
def automations_save():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -69,7 +69,7 @@ def automations_save():
|
|||||||
@bp.delete('/automations/<int:rule_id>')
|
@bp.delete('/automations/<int:rule_id>')
|
||||||
def automations_delete(rule_id: int):
|
def automations_delete(rule_id: int):
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -83,7 +83,7 @@ def automations_delete(rule_id: int):
|
|||||||
@bp.post('/automations/<int:rule_id>/run')
|
@bp.post('/automations/<int:rule_id>/run')
|
||||||
def automations_run_rule(rule_id: int):
|
def automations_run_rule(rule_id: int):
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -100,7 +100,7 @@ def automations_run_rule(rule_id: int):
|
|||||||
@bp.post('/automations/check')
|
@bp.post('/automations/check')
|
||||||
def automations_check():
|
def automations_check():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -117,7 +117,7 @@ def automations_check():
|
|||||||
@bp.delete('/automations/history')
|
@bp.delete('/automations/history')
|
||||||
def automations_history_clear():
|
def automations_history_clear():
|
||||||
from ..services import automation_rules
|
from ..services import automation_rules
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from ._shared import *
|
|||||||
from ..services import auth
|
from ..services import auth
|
||||||
|
|
||||||
|
|
||||||
def _active_profile_id() -> int | None:
|
def _active_profile_id(require_write: bool = False) -> int | None:
|
||||||
profile = preferences.active_profile()
|
profile = request_profile(require_write=require_write)
|
||||||
return int(profile["id"]) if profile else None
|
return int(profile["id"]) if profile else None
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ def backup_list():
|
|||||||
@bp.post("/backup/profile")
|
@bp.post("/backup/profile")
|
||||||
def backup_create_profile():
|
def backup_create_profile():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
pid = _active_profile_id()
|
pid = _active_profile_id(require_write=True)
|
||||||
if not pid:
|
if not pid:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -84,7 +84,7 @@ def profile_backup_settings_get():
|
|||||||
@bp.post("/backup/profile/settings")
|
@bp.post("/backup/profile/settings")
|
||||||
def profile_backup_settings_save():
|
def profile_backup_settings_save():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
pid = _active_profile_id()
|
pid = _active_profile_id(require_write=True)
|
||||||
if not pid:
|
if not pid:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -104,7 +104,7 @@ def backup_preview(backup_id: int):
|
|||||||
@bp.post("/backup/<int:backup_id>/restore")
|
@bp.post("/backup/<int:backup_id>/restore")
|
||||||
def backup_restore(backup_id: int):
|
def backup_restore(backup_id: int):
|
||||||
try:
|
try:
|
||||||
pid = _active_profile_id()
|
pid = _active_profile_id(require_write=True)
|
||||||
return ok({"result": backup_service.restore_backup(backup_id, default_user_id(), profile_id=pid)})
|
return ok({"result": backup_service.restore_backup(backup_id, default_user_id(), profile_id=pid)})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"ok": False, "error": str(exc)}), 403 if isinstance(exc, PermissionError) else 400
|
return jsonify({"ok": False, "error": str(exc)}), 403 if isinstance(exc, PermissionError) else 400
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from ..services import operation_logs
|
|||||||
|
|
||||||
|
|
||||||
def _active_profile_or_400():
|
def _active_profile_or_400():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return None
|
return None
|
||||||
return profile
|
return profile
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def ok(payload=None):
|
|||||||
|
|
||||||
|
|
||||||
def _profile_or_error():
|
def _profile_or_error():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return None, (jsonify({"ok": False, "error": "No profile"}), 400)
|
return None, (jsonify({"ok": False, "error": "No profile"}), 400)
|
||||||
return profile, None
|
return profile, None
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ from ..services import auth
|
|||||||
|
|
||||||
@bp.get("/profiles")
|
@bp.get("/profiles")
|
||||||
def profiles_list():
|
def profiles_list():
|
||||||
return ok({"profiles": preferences.list_profiles(), "active": preferences.active_profile()})
|
profiles = []
|
||||||
|
for row in preferences.list_profiles():
|
||||||
|
item = dict(row)
|
||||||
|
settings = backup_service.get_auto_backup_settings(default_user_id(), "profile", int(item.get("id") or 0))
|
||||||
|
item["profile_backup_enabled"] = bool(settings.get("enabled"))
|
||||||
|
item["profile_backup_interval_hours"] = settings.get("interval_hours")
|
||||||
|
item["profile_backup_retention_days"] = settings.get("retention_days")
|
||||||
|
profiles.append(item)
|
||||||
|
return ok({"profiles": profiles, "active": preferences.active_profile()})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -89,25 +97,25 @@ def profiles_import():
|
|||||||
|
|
||||||
@bp.get("/preferences")
|
@bp.get("/preferences")
|
||||||
def prefs_get():
|
def prefs_get():
|
||||||
return ok({"preferences": preferences.get_preferences()})
|
return ok({"preferences": preferences.get_preferences(profile_id=request_profile_id())})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/preferences")
|
@bp.post("/preferences")
|
||||||
def prefs_save():
|
def prefs_save():
|
||||||
return ok({"preferences": preferences.save_preferences(request.json or {})})
|
return ok({"preferences": preferences.save_preferences(request.json or {}, profile_id=request_profile_id(require_write=True))})
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/preferences/table-columns/recommended")
|
@bp.post("/preferences/table-columns/recommended")
|
||||||
def prefs_table_columns_recommended():
|
def prefs_table_columns_recommended():
|
||||||
# Note: Applies the backend-owned recommended desktop and mobile column layout.
|
# Note: Applies the backend-owned recommended desktop and mobile column layout.
|
||||||
return ok({"preferences": preferences.apply_recommended_table_columns()})
|
return ok({"preferences": preferences.apply_recommended_table_columns(profile_id=request_profile_id(require_write=True))})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/labels")
|
@bp.get("/labels")
|
||||||
def labels_list():
|
def labels_list():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
pid = profile["id"] if profile else None
|
pid = profile["id"] if profile else None
|
||||||
if not pid:
|
if not pid:
|
||||||
return ok({"labels": []})
|
return ok({"labels": []})
|
||||||
@@ -128,7 +136,7 @@ def labels_list():
|
|||||||
|
|
||||||
@bp.post("/labels")
|
@bp.post("/labels")
|
||||||
def labels_save():
|
def labels_save():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -150,7 +158,7 @@ def labels_save():
|
|||||||
|
|
||||||
@bp.delete("/labels/<int:label_id>")
|
@bp.delete("/labels/<int:label_id>")
|
||||||
def labels_delete(label_id: int):
|
def labels_delete(label_id: int):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
pid = profile["id"] if profile else None
|
pid = profile["id"] if profile else None
|
||||||
if not pid or not auth.can_write_profile(int(pid), default_user_id()):
|
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
|
return jsonify({"ok": False, "error": "No write access to profile"}), 403
|
||||||
@@ -162,7 +170,7 @@ def labels_delete(label_id: int):
|
|||||||
|
|
||||||
@bp.get("/ratio-groups")
|
@bp.get("/ratio-groups")
|
||||||
def ratio_groups_list():
|
def ratio_groups_list():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
pid = profile["id"] if profile else None
|
pid = profile["id"] if profile else None
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
@@ -182,7 +190,7 @@ def ratio_groups_list():
|
|||||||
|
|
||||||
@bp.post("/ratio-groups")
|
@bp.post("/ratio-groups")
|
||||||
def ratio_groups_save():
|
def ratio_groups_save():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -212,7 +220,7 @@ def ratio_groups_save():
|
|||||||
|
|
||||||
@bp.post("/ratio-groups/check")
|
@bp.post("/ratio-groups/check")
|
||||||
def ratio_groups_check():
|
def ratio_groups_check():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
return ok({"result": ratio_rules.check(profile, default_user_id())})
|
return ok({"result": ratio_rules.check(profile, default_user_id())})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from ._shared import *
|
|||||||
|
|
||||||
|
|
||||||
def _active_profile_or_400():
|
def _active_profile_or_400():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return None
|
return None
|
||||||
return profile
|
return profile
|
||||||
@@ -117,7 +117,7 @@ def rss_rule_test():
|
|||||||
|
|
||||||
@bp.post("/rss/check")
|
@bp.post("/rss/check")
|
||||||
def rss_check():
|
def rss_check():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
return ok(rss_service.check(profile, only_due=False))
|
return ok(rss_service.check(profile, only_due=False))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from ._shared import *
|
|||||||
@bp.get('/smart-queue')
|
@bp.get('/smart-queue')
|
||||||
def smart_queue_get():
|
def smart_queue_get():
|
||||||
from ..services import smart_queue
|
from ..services import smart_queue
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({'settings': {}, 'exclusions': [], 'error': 'No profile'})
|
return ok({'settings': {}, 'exclusions': [], 'error': 'No profile'})
|
||||||
try:
|
try:
|
||||||
@@ -23,7 +23,7 @@ def smart_queue_get():
|
|||||||
@bp.post('/smart-queue')
|
@bp.post('/smart-queue')
|
||||||
def smart_queue_save():
|
def smart_queue_save():
|
||||||
from ..services import smart_queue
|
from ..services import smart_queue
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({'settings': {}, 'error': 'No profile'})
|
return ok({'settings': {}, 'error': 'No profile'})
|
||||||
try:
|
try:
|
||||||
@@ -37,7 +37,7 @@ def smart_queue_save():
|
|||||||
|
|
||||||
@bp.post('/smart-queue/check')
|
@bp.post('/smart-queue/check')
|
||||||
def smart_queue_check():
|
def smart_queue_check():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({'result': {'ok': False, 'error': 'No profile'}})
|
return ok({'result': {'ok': False, 'error': 'No profile'}})
|
||||||
if str(request.args.get('sync') or '').lower() in {'1', 'true', 'yes'}:
|
if str(request.args.get('sync') or '').lower() in {'1', 'true', 'yes'}:
|
||||||
@@ -66,7 +66,7 @@ def smart_queue_check():
|
|||||||
@bp.post('/smart-queue/exclusion')
|
@bp.post('/smart-queue/exclusion')
|
||||||
def smart_queue_exclusion():
|
def smart_queue_exclusion():
|
||||||
from ..services import smart_queue
|
from ..services import smart_queue
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -79,7 +79,7 @@ def smart_queue_exclusion():
|
|||||||
@bp.delete('/smart-queue/history')
|
@bp.delete('/smart-queue/history')
|
||||||
def smart_queue_history_clear():
|
def smart_queue_history_clear():
|
||||||
from ..services import smart_queue
|
from ..services import smart_queue
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
|
|||||||
+19
-19
@@ -7,7 +7,7 @@ from ..services.frontend_assets import static_hash
|
|||||||
|
|
||||||
@bp.get("/system/disk")
|
@bp.get("/system/disk")
|
||||||
def system_disk():
|
def system_disk():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"})
|
return jsonify({"ok": False, "error": "No profile"})
|
||||||
try:
|
try:
|
||||||
@@ -19,7 +19,7 @@ def system_disk():
|
|||||||
|
|
||||||
@bp.get("/system/status")
|
@bp.get("/system/status")
|
||||||
def system_status():
|
def system_status():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"})
|
return jsonify({"ok": False, "error": "No profile"})
|
||||||
try:
|
try:
|
||||||
@@ -80,7 +80,7 @@ def health_check_nagios():
|
|||||||
@bp.get("/app/status")
|
@bp.get("/app/status")
|
||||||
def app_status():
|
def app_status():
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
proc = psutil.Process(os.getpid())
|
proc = psutil.Process(os.getpid())
|
||||||
try:
|
try:
|
||||||
jobs = list_jobs(10, 0)
|
jobs = list_jobs(10, 0)
|
||||||
@@ -178,7 +178,7 @@ def cleanup_status():
|
|||||||
|
|
||||||
@bp.post("/cleanup/cache")
|
@bp.post("/cleanup/cache")
|
||||||
def cleanup_profile_cache():
|
def cleanup_profile_cache():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
profile_id = int(profile["id"])
|
profile_id = int(profile["id"])
|
||||||
@@ -225,7 +225,7 @@ def cleanup_database_vacuum():
|
|||||||
|
|
||||||
@bp.post("/cleanup/smart-queue")
|
@bp.post("/cleanup/smart-queue")
|
||||||
def cleanup_smart_queue():
|
def cleanup_smart_queue():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
profile_id = int(profile["id"])
|
profile_id = int(profile["id"])
|
||||||
@@ -243,7 +243,7 @@ def cleanup_smart_queue():
|
|||||||
|
|
||||||
@bp.post("/cleanup/operation-logs")
|
@bp.post("/cleanup/operation-logs")
|
||||||
def cleanup_operation_logs():
|
def cleanup_operation_logs():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
# Note: Operation log cleanup removes only profile-scoped log entries; torrents, jobs and settings stay intact.
|
# Note: Operation log cleanup removes only profile-scoped log entries; torrents, jobs and settings stay intact.
|
||||||
@@ -254,7 +254,7 @@ def cleanup_operation_logs():
|
|||||||
|
|
||||||
@bp.post("/cleanup/planner")
|
@bp.post("/cleanup/planner")
|
||||||
def cleanup_planner():
|
def cleanup_planner():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
# Note: Planner cleanup removes only the active profile action history, not saved Planner settings.
|
# Note: Planner cleanup removes only the active profile action history, not saved Planner settings.
|
||||||
@@ -264,7 +264,7 @@ def cleanup_planner():
|
|||||||
|
|
||||||
@bp.post("/cleanup/automations")
|
@bp.post("/cleanup/automations")
|
||||||
def cleanup_automations():
|
def cleanup_automations():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
profile_id = int(profile["id"])
|
profile_id = int(profile["id"])
|
||||||
@@ -284,7 +284,7 @@ def cleanup_automations():
|
|||||||
|
|
||||||
@bp.post("/cleanup/poller-diagnostics")
|
@bp.post("/cleanup/poller-diagnostics")
|
||||||
def cleanup_poller_diagnostics():
|
def cleanup_poller_diagnostics():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
profile_id = int(profile["id"])
|
profile_id = int(profile["id"])
|
||||||
@@ -295,7 +295,7 @@ def cleanup_poller_diagnostics():
|
|||||||
@bp.post("/cleanup/all")
|
@bp.post("/cleanup/all")
|
||||||
def cleanup_all():
|
def cleanup_all():
|
||||||
deleted_jobs = clear_jobs()
|
deleted_jobs = clear_jobs()
|
||||||
active_profile = preferences.active_profile()
|
active_profile = request_profile()
|
||||||
active_profile_id = int(active_profile["id"]) if active_profile else 0
|
active_profile_id = int(active_profile["id"]) if active_profile else 0
|
||||||
deleted_logs = operation_logs.clear(active_profile_id) if active_profile_id else 0
|
deleted_logs = operation_logs.clear(active_profile_id) if active_profile_id else 0
|
||||||
deleted_planner = download_planner.clear_history(active_profile_id) if active_profile_id else 0
|
deleted_planner = download_planner.clear_history(active_profile_id) if active_profile_id else 0
|
||||||
@@ -371,7 +371,7 @@ def _annotate_path_directories(profile: dict, payload: dict) -> dict:
|
|||||||
|
|
||||||
@bp.get("/path/default")
|
@bp.get("/path/default")
|
||||||
def path_default():
|
def path_default():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -383,7 +383,7 @@ def path_default():
|
|||||||
|
|
||||||
@bp.get("/path/browse")
|
@bp.get("/path/browse")
|
||||||
def path_browse():
|
def path_browse():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
base = request.args.get("path") or ""
|
base = request.args.get("path") or ""
|
||||||
@@ -395,7 +395,7 @@ def path_browse():
|
|||||||
|
|
||||||
@bp.post("/path/directories")
|
@bp.post("/path/directories")
|
||||||
def path_directory_create():
|
def path_directory_create():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
require_profile_write(profile.get("id"))
|
require_profile_write(profile.get("id"))
|
||||||
@@ -410,7 +410,7 @@ def path_directory_create():
|
|||||||
|
|
||||||
@bp.post("/path/directories/rename")
|
@bp.post("/path/directories/rename")
|
||||||
def path_directory_rename():
|
def path_directory_rename():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
require_profile_write(profile.get("id"))
|
require_profile_write(profile.get("id"))
|
||||||
@@ -429,7 +429,7 @@ def path_directory_rename():
|
|||||||
|
|
||||||
@bp.get('/rtorrent-config')
|
@bp.get('/rtorrent-config')
|
||||||
def rtorrent_config_get():
|
def rtorrent_config_get():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -440,7 +440,7 @@ def rtorrent_config_get():
|
|||||||
|
|
||||||
@bp.post('/rtorrent-config')
|
@bp.post('/rtorrent-config')
|
||||||
def rtorrent_config_save():
|
def rtorrent_config_save():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -457,7 +457,7 @@ def rtorrent_config_save():
|
|||||||
|
|
||||||
@bp.post('/rtorrent-config/reset')
|
@bp.post('/rtorrent-config/reset')
|
||||||
def rtorrent_config_reset():
|
def rtorrent_config_reset():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -468,7 +468,7 @@ def rtorrent_config_reset():
|
|||||||
|
|
||||||
@bp.post('/rtorrent-config/generate')
|
@bp.post('/rtorrent-config/generate')
|
||||||
def rtorrent_config_generate():
|
def rtorrent_config_generate():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
return jsonify({'ok': False, 'error': 'No profile'}), 400
|
||||||
try:
|
try:
|
||||||
@@ -481,7 +481,7 @@ def rtorrent_config_generate():
|
|||||||
@bp.get('/traffic/history')
|
@bp.get('/traffic/history')
|
||||||
def traffic_history_get():
|
def traffic_history_get():
|
||||||
from ..services import traffic_history
|
from ..services import traffic_history
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({'history': {'range': request.args.get('range') or '7d', 'bucket': 'day', 'rows': []}})
|
return ok({'history': {'range': request.args.get('range') or '7d', 'bucket': 'day', 'rows': []}})
|
||||||
range_name = request.args.get('range') or '7d'
|
range_name = request.args.get('range') or '7d'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from ..services.reverse_dns import attach_reverse_dns
|
|||||||
|
|
||||||
@bp.get("/torrents")
|
@bp.get("/torrents")
|
||||||
def torrents():
|
def torrents():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({"torrents": [], "summary": cached_summary(0, []), "error": "No rTorrent profile"})
|
return ok({"torrents": [], "summary": cached_summary(0, []), "error": "No rTorrent profile"})
|
||||||
rows = torrent_cache.snapshot(profile["id"])
|
rows = torrent_cache.snapshot(profile["id"])
|
||||||
@@ -23,7 +23,7 @@ def torrents():
|
|||||||
|
|
||||||
@bp.get("/trackers/summary")
|
@bp.get("/trackers/summary")
|
||||||
def trackers_summary():
|
def trackers_summary():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
|
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
|
||||||
try:
|
try:
|
||||||
@@ -78,7 +78,7 @@ def tracker_favicon_query():
|
|||||||
|
|
||||||
@bp.get("/torrent-stats")
|
@bp.get("/torrent-stats")
|
||||||
def torrent_stats_get():
|
def torrent_stats_get():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return ok({"stats": {}, "error": "No profile"})
|
return ok({"stats": {}, "error": "No profile"})
|
||||||
force = str(request.args.get("force") or "").lower() in {"1", "true", "yes"}
|
force = str(request.args.get("force") or "").lower() in {"1", "true", "yes"}
|
||||||
@@ -92,7 +92,7 @@ def torrent_stats_get():
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/files")
|
@bp.get("/torrents/<torrent_hash>/files")
|
||||||
def torrent_files(torrent_hash: str):
|
def torrent_files(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
return ok({"files": rtorrent.torrent_files(profile, torrent_hash)})
|
return ok({"files": rtorrent.torrent_files(profile, torrent_hash)})
|
||||||
@@ -101,7 +101,7 @@ def torrent_files(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/files/<int:file_index>/mediainfo")
|
@bp.get("/torrents/<torrent_hash>/files/<int:file_index>/mediainfo")
|
||||||
def torrent_file_media_info(torrent_hash: str, file_index: int):
|
def torrent_file_media_info(torrent_hash: str, file_index: int):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -124,7 +124,7 @@ def torrent_file_media_info(torrent_hash: str, file_index: int):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/files/priority")
|
@bp.post("/torrents/<torrent_hash>/files/priority")
|
||||||
def torrent_file_priority(torrent_hash: str):
|
def torrent_file_priority(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -139,7 +139,7 @@ def torrent_file_priority(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/files/tree")
|
@bp.get("/torrents/<torrent_hash>/files/tree")
|
||||||
def torrent_file_tree(torrent_hash: str):
|
def torrent_file_tree(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
return ok({"tree": rtorrent.torrent_file_tree(profile, torrent_hash)})
|
return ok({"tree": rtorrent.torrent_file_tree(profile, torrent_hash)})
|
||||||
@@ -148,7 +148,7 @@ def torrent_file_tree(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/files/folder-priority")
|
@bp.post("/torrents/<torrent_hash>/files/folder-priority")
|
||||||
def torrent_folder_priority(torrent_hash: str):
|
def torrent_folder_priority(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -214,7 +214,7 @@ def _send_staged_file(profile: dict, path: str, download_name: str, local: bool
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/files/<int:file_index>/download-link")
|
@bp.post("/torrents/<torrent_hash>/files/<int:file_index>/download-link")
|
||||||
def torrent_file_download_link(torrent_hash: str, file_index: int):
|
def torrent_file_download_link(torrent_hash: str, file_index: int):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -238,7 +238,7 @@ def torrent_file_download_link_from_body(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/files/download.zip/link")
|
@bp.post("/torrents/<torrent_hash>/files/download.zip/link")
|
||||||
def torrent_files_download_zip_link(torrent_hash: str):
|
def torrent_files_download_zip_link(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -254,7 +254,7 @@ def torrent_files_download_zip_link(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/torrent-file/link")
|
@bp.get("/torrents/<torrent_hash>/torrent-file/link")
|
||||||
def torrent_file_export_link(torrent_hash: str):
|
def torrent_file_export_link(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -267,7 +267,7 @@ def torrent_file_export_link(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/torrent-files.zip/link")
|
@bp.post("/torrents/torrent-files.zip/link")
|
||||||
def torrent_files_export_zip_link():
|
def torrent_files_export_zip_link():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -284,7 +284,7 @@ def torrent_files_export_zip_link():
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/files/<int:file_index>/download")
|
@bp.get("/torrents/<torrent_hash>/files/<int:file_index>/download")
|
||||||
def torrent_file_download(torrent_hash: str, file_index: int):
|
def torrent_file_download(torrent_hash: str, file_index: int):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -377,7 +377,7 @@ def _stream_torrent_files_zip(profile: dict, items: list[dict]):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/files/download.zip")
|
@bp.post("/torrents/<torrent_hash>/files/download.zip")
|
||||||
def torrent_files_download_zip(torrent_hash: str):
|
def torrent_files_download_zip(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -393,7 +393,7 @@ def torrent_files_download_zip(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/torrent-file")
|
@bp.get("/torrents/<torrent_hash>/torrent-file")
|
||||||
def torrent_file_export(torrent_hash: str):
|
def torrent_file_export(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -406,7 +406,7 @@ def torrent_file_export(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/torrent-files.zip")
|
@bp.post("/torrents/torrent-files.zip")
|
||||||
def torrent_files_export_zip():
|
def torrent_files_export_zip():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -455,7 +455,7 @@ def torrent_files_export_zip():
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/chunks")
|
@bp.get("/torrents/<torrent_hash>/chunks")
|
||||||
def torrent_chunks(torrent_hash: str):
|
def torrent_chunks(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -467,7 +467,7 @@ def torrent_chunks(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/chunks/<action_name>")
|
@bp.post("/torrents/<torrent_hash>/chunks/<action_name>")
|
||||||
def torrent_chunk_action(torrent_hash: str, action_name: str):
|
def torrent_chunk_action(torrent_hash: str, action_name: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -480,7 +480,7 @@ def torrent_chunk_action(torrent_hash: str, action_name: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/peers")
|
@bp.get("/torrents/<torrent_hash>/peers")
|
||||||
def torrent_peers(torrent_hash: str):
|
def torrent_peers(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
peers = rtorrent.torrent_peers(profile, torrent_hash)
|
peers = rtorrent.torrent_peers(profile, torrent_hash)
|
||||||
@@ -496,7 +496,7 @@ def torrent_peers(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.get("/torrents/<torrent_hash>/trackers")
|
@bp.get("/torrents/<torrent_hash>/trackers")
|
||||||
def torrent_trackers(torrent_hash: str):
|
def torrent_trackers(torrent_hash: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
return ok({"trackers": rtorrent.torrent_trackers(profile, torrent_hash)})
|
return ok({"trackers": rtorrent.torrent_trackers(profile, torrent_hash)})
|
||||||
@@ -505,7 +505,7 @@ def torrent_trackers(torrent_hash: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/<torrent_hash>/trackers/<action_name>")
|
@bp.post("/torrents/<torrent_hash>/trackers/<action_name>")
|
||||||
def torrent_tracker_action(torrent_hash: str, action_name: str):
|
def torrent_tracker_action(torrent_hash: str, action_name: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
try:
|
try:
|
||||||
@@ -518,7 +518,7 @@ def torrent_tracker_action(torrent_hash: str, action_name: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/<action_name>")
|
@bp.post("/torrents/<action_name>")
|
||||||
def torrent_action(action_name: str):
|
def torrent_action(action_name: str):
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -547,7 +547,7 @@ def torrent_action(action_name: str):
|
|||||||
|
|
||||||
@bp.post("/torrents/create")
|
@bp.post("/torrents/create")
|
||||||
def torrent_create():
|
def torrent_create():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
form = request.form if request.content_type and request.content_type.startswith("multipart/form-data") else (request.get_json(silent=True) or {})
|
form = request.form if request.content_type and request.content_type.startswith("multipart/form-data") else (request.get_json(silent=True) or {})
|
||||||
@@ -577,7 +577,7 @@ def torrent_create():
|
|||||||
|
|
||||||
@bp.post("/torrents/add")
|
@bp.post("/torrents/add")
|
||||||
def torrent_add():
|
def torrent_add():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
job_ids = []
|
job_ids = []
|
||||||
@@ -634,7 +634,7 @@ def torrent_add():
|
|||||||
|
|
||||||
@bp.post("/torrents/preview")
|
@bp.post("/torrents/preview")
|
||||||
def torrent_preview():
|
def torrent_preview():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
existing_hashes = set()
|
existing_hashes = set()
|
||||||
if profile:
|
if profile:
|
||||||
try:
|
try:
|
||||||
@@ -664,7 +664,7 @@ def torrent_preview():
|
|||||||
|
|
||||||
@bp.post("/speed/limits")
|
@bp.post("/speed/limits")
|
||||||
def speed_limits():
|
def speed_limits():
|
||||||
profile = preferences.active_profile()
|
profile = request_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
|||||||
@@ -728,12 +728,45 @@ def install_guards(app) -> None:
|
|||||||
def _request_profile_id() -> int | None:
|
def _request_profile_id() -> int | None:
|
||||||
if request.view_args and request.view_args.get("profile_id"):
|
if request.view_args and request.view_args.get("profile_id"):
|
||||||
return int(request.view_args["profile_id"])
|
return int(request.view_args["profile_id"])
|
||||||
|
payload = {}
|
||||||
try:
|
try:
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
if payload.get("profile_id"):
|
|
||||||
return int(payload.get("profile_id"))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
payload = {}
|
||||||
|
raw_id = (
|
||||||
|
request.args.get("profile_id")
|
||||||
|
or request.form.get("profile_id")
|
||||||
|
or payload.get("profile_id")
|
||||||
|
or request.headers.get("X-PyTorrent-Profile-Id")
|
||||||
|
)
|
||||||
|
if raw_id not in (None, ""):
|
||||||
|
try:
|
||||||
|
return int(raw_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
raw_name = (
|
||||||
|
request.args.get("profile_name")
|
||||||
|
or request.form.get("profile_name")
|
||||||
|
or payload.get("profile_name")
|
||||||
|
or request.headers.get("X-PyTorrent-Profile-Name")
|
||||||
|
)
|
||||||
|
if raw_name:
|
||||||
|
from . import preferences
|
||||||
|
visible = visible_profile_ids(current_user_id())
|
||||||
|
with connect() as conn:
|
||||||
|
if visible is None:
|
||||||
|
row = conn.execute("SELECT id FROM rtorrent_profiles WHERE lower(name)=lower(?) ORDER BY is_default DESC, id LIMIT 1", (str(raw_name).strip(),)).fetchone()
|
||||||
|
elif visible:
|
||||||
|
placeholders = ",".join("?" for _ in visible)
|
||||||
|
row = conn.execute(
|
||||||
|
f"SELECT id FROM rtorrent_profiles WHERE id IN ({placeholders}) AND lower(name)=lower(?) ORDER BY is_default DESC, id LIMIT 1",
|
||||||
|
(*tuple(visible), str(raw_name).strip()),
|
||||||
|
).fetchone()
|
||||||
|
else:
|
||||||
|
row = None
|
||||||
|
return int(row["id"]) if row else None
|
||||||
from . import preferences
|
from . import preferences
|
||||||
profile = preferences.active_profile()
|
profile = preferences.active_profile()
|
||||||
return int(profile["id"]) if profile else None
|
if profile:
|
||||||
|
return int(profile["id"])
|
||||||
|
return 1 if can_access_profile(1) else None
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ def create_app_backup(name: str, user_id: int | None = None, automatic: bool = F
|
|||||||
|
|
||||||
def create_profile_backup(name: str, profile_id: int, user_id: int | None = None, automatic: bool = False) -> dict:
|
def create_profile_backup(name: str, profile_id: int, user_id: int | None = None, automatic: bool = False) -> dict:
|
||||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||||
if not auth.can_access_profile(profile_id, user_id):
|
if not auth.can_write_profile(profile_id, user_id):
|
||||||
raise PermissionError("No access to profile")
|
raise PermissionError("No write access to profile")
|
||||||
payload = {"version": 2, "backup_type": "profile", "source_profile_id": int(profile_id), "created_at": utcnow(), "automatic": bool(automatic), "tables": {}}
|
payload = {"version": 2, "backup_type": "profile", "source_profile_id": int(profile_id), "created_at": utcnow(), "automatic": bool(automatic), "tables": {}}
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
for table in PROFILE_BACKUP_TABLES:
|
for table in PROFILE_BACKUP_TABLES:
|
||||||
|
|||||||
@@ -491,9 +491,9 @@ def get_preferences(user_id: int | None = None, profile_id: int | None = None):
|
|||||||
merged.update(get_disk_monitor_preferences(profile_id, user_id))
|
merged.update(get_disk_monitor_preferences(profile_id, user_id))
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
def save_preferences(data: dict, user_id: int | None = None):
|
def save_preferences(data: dict, user_id: int | None = None, profile_id: int | None = None):
|
||||||
user_id = user_id or auth.current_user_id() or default_user_id()
|
user_id = user_id or auth.current_user_id() or default_user_id()
|
||||||
profile_id = _active_profile_id_for_user(user_id)
|
profile_id = profile_id or _active_profile_id_for_user(user_id)
|
||||||
allowed_theme = data.get("theme") if data.get("theme") in {"light", "dark"} else None
|
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
|
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
|
font_family = data.get("font_family") if data.get("font_family") in FONT_FAMILIES else None
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
export const profileListSource = " function markActiveProfileRow(id){\n // Note: Keeps the active rTorrent profile frame in sync immediately after switching, before diagnostics refresh finishes.\n const activeId=String(id||'');\n document.querySelectorAll('#profileList .profile-row').forEach(row=>{\n const isActive=String(row.dataset.profileId||'')===activeId;\n row.classList.toggle('active', isActive);\n row.setAttribute('aria-current', isActive ? 'true' : 'false');\n const badge=row.querySelector('[data-active-profile-badge]');\n if(badge) badge.classList.toggle('d-none', !isActive);\n });\n }\n function profileDiagnosticStatusClass(status){\n // Note: rTorrent profile badges reuse Bootstrap colors and the same normal/slow/error idea as the poller panel.\n const value=String(status||'unknown').toLowerCase();\n if(value==='normal' || value==='online') return 'success';\n if(value==='slow' || value==='slowdown') return 'warning';\n if(value==='error' || value==='recovery') return 'danger';\n return 'secondary';\n }\n function profileDiagnosticStatusLabel(status){\n const value=String(status||'unknown').toLowerCase();\n return value==='online' ? 'normal' : value;\n }\n async function refreshProfiles(){ const j=await (await fetch('/api/profiles')).json(); profileCache=new Map((j.profiles||[]).map(p=>[String(p.id),p])); const active=String(j.active?.id ?? activeProfileId ?? ''); const rows=j.profiles||[]; const statusMap=new Map(); try{ const d=await (await fetch('/api/profiles/diagnostics')).json(); (d.diagnostics||[]).forEach(x=>statusMap.set(String(x.profile_id), x)); }catch(e){} $('profileList').innerHTML=rows.map(p=>{ const d=statusMap.get(String(p.id))||{}; const st=profileDiagnosticStatusLabel(d.status || 'unknown'); const cls=profileDiagnosticStatusClass(st); const response=d.response_time_ms?` · ${esc(d.response_time_ms)} ms`:''; const threshold=d.slow_threshold_ms?` · slow > ${esc(d.slow_threshold_ms)} ms`:''; const isActive=String(p.id)===active; return `<div class=\"profile-row ${isActive?'active':''}\" data-profile-id=\"${esc(p.id)}\" aria-current=\"${isActive?'true':'false'}\"><b>${esc(p.name)} <span data-active-profile-badge class='badge text-bg-primary ms-1 ${isActive?'':'d-none'}'>active</span> ${p.is_remote?\"<span class='badge text-bg-secondary ms-1'>remote</span>\":''} <span class=\"badge text-bg-${cls}\">${esc(st)}</span></b><span>${esc(p.scgi_url)} · heavy ${esc(p.max_parallel_jobs||5)} · light ${esc(p.light_parallel_jobs||4)} · poll ${esc(p.polling_min_interval_seconds||'-')}s${response}${threshold}</span><div class=\"profile-actions\"><button class=\"btn btn-xs btn-outline-primary\" data-use-profile=\"${p.id}\"><i class=\"fa-solid fa-plug-circle-check\"></i> use</button><button class=\"btn btn-xs btn-outline-info\" data-test-saved-profile=\"${p.id}\" title=\"Diagnostics\"><i class=\"fa-solid fa-stethoscope\"></i></button><button class=\"btn btn-xs btn-outline-secondary\" data-edit-profile=\"${p.id}\" title=\"Edit\"><i class=\"fa-solid fa-pen-to-square\"></i></button><button class=\"btn btn-xs btn-outline-danger\" data-del-profile=\"${p.id}\" title=\"Delete\"><i class=\"fa-solid fa-trash-can\"></i> Remove</button></div></div>`; }).join('')||'No profiles.'; }\n";
|
export const profileListSource = " function markActiveProfileRow(id){\n // Note: Keeps the active rTorrent profile frame in sync immediately after switching, before diagnostics refresh finishes.\n const activeId=String(id||'');\n document.querySelectorAll('#profileList .profile-row').forEach(row=>{\n const isActive=String(row.dataset.profileId||'')===activeId;\n row.classList.toggle('active', isActive);\n row.setAttribute('aria-current', isActive ? 'true' : 'false');\n const badge=row.querySelector('[data-active-profile-badge]');\n if(badge) badge.classList.toggle('d-none', !isActive);\n });\n }\n function profileDiagnosticStatusClass(status){\n // Note: rTorrent profile badges reuse Bootstrap colors and the same normal/slow/error idea as the poller panel.\n const value=String(status||'unknown').toLowerCase();\n if(value==='normal' || value==='online') return 'success';\n if(value==='slow' || value==='slowdown') return 'warning';\n if(value==='error' || value==='recovery') return 'danger';\n return 'secondary';\n }\n function profileDiagnosticStatusLabel(status){\n const value=String(status||'unknown').toLowerCase();\n return value==='online' ? 'normal' : value;\n }\n async function refreshProfiles(){ const j=await (await fetch('/api/profiles')).json(); profileCache=new Map((j.profiles||[]).map(p=>[String(p.id),p])); const active=String(j.active?.id ?? activeProfileId ?? ''); const rows=j.profiles||[]; const statusMap=new Map(); try{ const d=await (await fetch('/api/profiles/diagnostics')).json(); (d.diagnostics||[]).forEach(x=>statusMap.set(String(x.profile_id), x)); }catch(e){} $('profileList').innerHTML=rows.map(p=>{ const d=statusMap.get(String(p.id))||{}; const st=profileDiagnosticStatusLabel(d.status || 'unknown'); const cls=profileDiagnosticStatusClass(st); const response=d.response_time_ms?` · ${esc(d.response_time_ms)} ms`:''; const threshold=d.slow_threshold_ms?` · slow > ${esc(d.slow_threshold_ms)} ms`:''; const isActive=String(p.id)===active; const backupBadge=p.profile_backup_enabled?` <span class=\"badge text-bg-info ms-1\">profile backup on</span>`:''; return `<div class=\"profile-row ${isActive?'active':''}\" data-profile-id=\"${esc(p.id)}\" aria-current=\"${isActive?'true':'false'}\"><b><span class=\"profile-id-badge\">#${esc(p.id)}</span> ${esc(p.name)} <span data-active-profile-badge class='badge text-bg-primary ms-1 ${isActive?'':'d-none'}'>active</span> ${p.is_remote?\"<span class='badge text-bg-secondary ms-1'>remote</span>\":''}${backupBadge} <span class=\"badge text-bg-${cls}\">${esc(st)}</span></b><span>ID ${esc(p.id)} · ${esc(p.scgi_url)} · heavy ${esc(p.max_parallel_jobs||5)} · light ${esc(p.light_parallel_jobs||4)} · poll ${esc(p.polling_min_interval_seconds||'-')}s${response}${threshold}</span><div class=\"profile-actions\"><button class=\"btn btn-xs btn-outline-primary\" data-use-profile=\"${p.id}\"><i class=\"fa-solid fa-plug-circle-check\"></i> use</button><button class=\"btn btn-xs btn-outline-info\" data-test-saved-profile=\"${p.id}\" title=\"Diagnostics\"><i class=\"fa-solid fa-stethoscope\"></i></button><button class=\"btn btn-xs btn-outline-secondary\" data-edit-profile=\"${p.id}\" title=\"Edit\"><i class=\"fa-solid fa-pen-to-square\"></i></button><button class=\"btn btn-xs btn-outline-danger\" data-del-profile=\"${p.id}\" title=\"Delete\"><i class=\"fa-solid fa-trash-can\"></i> Remove</button></div></div>`; }).join('')||'No profiles.'; }\n";
|
||||||
|
|||||||
@@ -5884,3 +5884,15 @@ body.compact-torrent-list .mobile-progress .torrent-progress {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
gap: 0.45rem 0.85rem;
|
gap: 0.45rem 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-id-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
border: 1px solid var(--bs-border-color);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<label class="form-label" for="profileSelect">Configured rTorrents</label>
|
<label class="form-label" for="profileSelect">Configured rTorrents</label>
|
||||||
<select id="profileSelect" class="form-select profile-select">
|
<select id="profileSelect" class="form-select profile-select">
|
||||||
{% for p in profiles %}<option value="{{ p.id }}" {% if active_profile and active_profile.id == p.id %}selected{% endif %}>{{ p.name }}</option>{% endfor %}
|
{% for p in profiles %}<option value="{{ p.id }}" {% if active_profile and active_profile.id == p.id %}selected{% endif %}>#{{ p.id }} — {{ p.name }}</option>{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">Changing rTorrent reloads the live torrent snapshot.</div>
|
<div class="form-text">Changing rTorrent reloads the live torrent snapshot.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user