profile id support in api requests

This commit is contained in:
Mateusz Gruszczyński
2026-06-16 19:46:16 +02:00
parent c796a740d1
commit a73aeb5544
18 changed files with 1823 additions and 185 deletions
+73 -1
View File
@@ -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 ..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 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_summary import cached_summary
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)
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:
with connect() as conn:
row = conn.execute("SELECT profile_id FROM jobs WHERE id=?", (job_id,)).fetchone()
+8 -8
View File
@@ -10,7 +10,7 @@ def _automation_user_id() -> int:
@bp.get('/automations')
def automations_get():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({'rules': [], 'history': [], 'error': 'No profile'})
try:
@@ -26,7 +26,7 @@ def automations_get():
@bp.get('/automations/export')
def automations_export():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -39,7 +39,7 @@ def automations_export():
@bp.post('/automations/import')
def automations_import():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -55,7 +55,7 @@ def automations_import():
@bp.post('/automations')
def automations_save():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -69,7 +69,7 @@ def automations_save():
@bp.delete('/automations/<int:rule_id>')
def automations_delete(rule_id: int):
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -83,7 +83,7 @@ def automations_delete(rule_id: int):
@bp.post('/automations/<int:rule_id>/run')
def automations_run_rule(rule_id: int):
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -100,7 +100,7 @@ def automations_run_rule(rule_id: int):
@bp.post('/automations/check')
def automations_check():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -117,7 +117,7 @@ def automations_check():
@bp.delete('/automations/history')
def automations_history_clear():
from ..services import automation_rules
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
+5 -5
View File
@@ -4,8 +4,8 @@ from ._shared import *
from ..services import auth
def _active_profile_id() -> int | None:
profile = preferences.active_profile()
def _active_profile_id(require_write: bool = False) -> int | None:
profile = request_profile(require_write=require_write)
return int(profile["id"]) if profile else None
@@ -27,7 +27,7 @@ def backup_list():
@bp.post("/backup/profile")
def backup_create_profile():
data = request.get_json(silent=True) or {}
pid = _active_profile_id()
pid = _active_profile_id(require_write=True)
if not pid:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -84,7 +84,7 @@ def profile_backup_settings_get():
@bp.post("/backup/profile/settings")
def profile_backup_settings_save():
data = request.get_json(silent=True) or {}
pid = _active_profile_id()
pid = _active_profile_id(require_write=True)
if not pid:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -104,7 +104,7 @@ def backup_preview(backup_id: int):
@bp.post("/backup/<int:backup_id>/restore")
def backup_restore(backup_id: int):
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)})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 403 if isinstance(exc, PermissionError) else 400
+1 -1
View File
@@ -5,7 +5,7 @@ from ..services import operation_logs
def _active_profile_or_400():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return None
return profile
+1 -1
View File
@@ -16,7 +16,7 @@ def ok(payload=None):
def _profile_or_error():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return None, (jsonify({"ok": False, "error": "No profile"}), 400)
return profile, None
+18 -10
View File
@@ -6,7 +6,15 @@ from ..services import auth
@bp.get("/profiles")
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")
def prefs_get():
return ok({"preferences": preferences.get_preferences()})
return ok({"preferences": preferences.get_preferences(profile_id=request_profile_id())})
@bp.post("/preferences")
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")
def prefs_table_columns_recommended():
# 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")
def labels_list():
profile = preferences.active_profile()
profile = request_profile()
pid = profile["id"] if profile else None
if not pid:
return ok({"labels": []})
@@ -128,7 +136,7 @@ def labels_list():
@bp.post("/labels")
def labels_save():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
@@ -150,7 +158,7 @@ def labels_save():
@bp.delete("/labels/<int:label_id>")
def labels_delete(label_id: int):
profile = preferences.active_profile()
profile = request_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
@@ -162,7 +170,7 @@ def labels_delete(label_id: int):
@bp.get("/ratio-groups")
def ratio_groups_list():
profile = preferences.active_profile()
profile = request_profile()
pid = profile["id"] if profile else None
with connect() as conn:
rows = conn.execute(
@@ -182,7 +190,7 @@ def ratio_groups_list():
@bp.post("/ratio-groups")
def ratio_groups_save():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
@@ -212,7 +220,7 @@ def ratio_groups_save():
@bp.post("/ratio-groups/check")
def ratio_groups_check():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
return ok({"result": ratio_rules.check(profile, default_user_id())})
+2 -2
View File
@@ -4,7 +4,7 @@ from ._shared import *
def _active_profile_or_400():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return None
return profile
@@ -117,7 +117,7 @@ def rss_rule_test():
@bp.post("/rss/check")
def rss_check():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
return ok(rss_service.check(profile, only_due=False))
+5 -5
View File
@@ -5,7 +5,7 @@ from ._shared import *
@bp.get('/smart-queue')
def smart_queue_get():
from ..services import smart_queue
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({'settings': {}, 'exclusions': [], 'error': 'No profile'})
try:
@@ -23,7 +23,7 @@ def smart_queue_get():
@bp.post('/smart-queue')
def smart_queue_save():
from ..services import smart_queue
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({'settings': {}, 'error': 'No profile'})
try:
@@ -37,7 +37,7 @@ def smart_queue_save():
@bp.post('/smart-queue/check')
def smart_queue_check():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({'result': {'ok': False, 'error': 'No profile'}})
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')
def smart_queue_exclusion():
from ..services import smart_queue
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
data = request.get_json(silent=True) or {}
@@ -79,7 +79,7 @@ def smart_queue_exclusion():
@bp.delete('/smart-queue/history')
def smart_queue_history_clear():
from ..services import smart_queue
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
+19 -19
View File
@@ -7,7 +7,7 @@ from ..services.frontend_assets import static_hash
@bp.get("/system/disk")
def system_disk():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"})
try:
@@ -19,7 +19,7 @@ def system_disk():
@bp.get("/system/status")
def system_status():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"})
try:
@@ -80,7 +80,7 @@ def health_check_nagios():
@bp.get("/app/status")
def app_status():
started = time.perf_counter()
profile = preferences.active_profile()
profile = request_profile()
proc = psutil.Process(os.getpid())
try:
jobs = list_jobs(10, 0)
@@ -178,7 +178,7 @@ def cleanup_status():
@bp.post("/cleanup/cache")
def cleanup_profile_cache():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
profile_id = int(profile["id"])
@@ -225,7 +225,7 @@ def cleanup_database_vacuum():
@bp.post("/cleanup/smart-queue")
def cleanup_smart_queue():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
profile_id = int(profile["id"])
@@ -243,7 +243,7 @@ def cleanup_smart_queue():
@bp.post("/cleanup/operation-logs")
def cleanup_operation_logs():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
# 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")
def cleanup_planner():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
# 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")
def cleanup_automations():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
profile_id = int(profile["id"])
@@ -284,7 +284,7 @@ def cleanup_automations():
@bp.post("/cleanup/poller-diagnostics")
def cleanup_poller_diagnostics():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
profile_id = int(profile["id"])
@@ -295,7 +295,7 @@ def cleanup_poller_diagnostics():
@bp.post("/cleanup/all")
def cleanup_all():
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
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
@@ -371,7 +371,7 @@ def _annotate_path_directories(profile: dict, payload: dict) -> dict:
@bp.get("/path/default")
def path_default():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -383,7 +383,7 @@ def path_default():
@bp.get("/path/browse")
def path_browse():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
base = request.args.get("path") or ""
@@ -395,7 +395,7 @@ def path_browse():
@bp.post("/path/directories")
def path_directory_create():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
require_profile_write(profile.get("id"))
@@ -410,7 +410,7 @@ def path_directory_create():
@bp.post("/path/directories/rename")
def path_directory_rename():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
require_profile_write(profile.get("id"))
@@ -429,7 +429,7 @@ def path_directory_rename():
@bp.get('/rtorrent-config')
def rtorrent_config_get():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -440,7 +440,7 @@ def rtorrent_config_get():
@bp.post('/rtorrent-config')
def rtorrent_config_save():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -457,7 +457,7 @@ def rtorrent_config_save():
@bp.post('/rtorrent-config/reset')
def rtorrent_config_reset():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -468,7 +468,7 @@ def rtorrent_config_reset():
@bp.post('/rtorrent-config/generate')
def rtorrent_config_generate():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({'ok': False, 'error': 'No profile'}), 400
try:
@@ -481,7 +481,7 @@ def rtorrent_config_generate():
@bp.get('/traffic/history')
def traffic_history_get():
from ..services import traffic_history
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({'history': {'range': request.args.get('range') or '7d', 'bucket': 'day', 'rows': []}})
range_name = request.args.get('range') or '7d'
+26 -26
View File
@@ -7,7 +7,7 @@ from ..services.reverse_dns import attach_reverse_dns
@bp.get("/torrents")
def torrents():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({"torrents": [], "summary": cached_summary(0, []), "error": "No rTorrent profile"})
rows = torrent_cache.snapshot(profile["id"])
@@ -23,7 +23,7 @@ def torrents():
@bp.get("/trackers/summary")
def trackers_summary():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({"summary": {"hashes": {}, "trackers": [], "errors": [], "scanned": 0, "pending": 0}, "error": "No profile"})
try:
@@ -78,7 +78,7 @@ def tracker_favicon_query():
@bp.get("/torrent-stats")
def torrent_stats_get():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return ok({"stats": {}, "error": "No profile"})
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")
def torrent_files(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_media_info(torrent_hash: str, file_index: int):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -124,7 +124,7 @@ def torrent_file_media_info(torrent_hash: str, file_index: int):
@bp.post("/torrents/<torrent_hash>/files/priority")
def torrent_file_priority(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_tree(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_folder_priority(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_download_link(torrent_hash: str, file_index: int):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -238,7 +238,7 @@ def torrent_file_download_link_from_body(torrent_hash: str):
@bp.post("/torrents/<torrent_hash>/files/download.zip/link")
def torrent_files_download_zip_link(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_export_link(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -267,7 +267,7 @@ def torrent_file_export_link(torrent_hash: str):
@bp.post("/torrents/torrent-files.zip/link")
def torrent_files_export_zip_link():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_download(torrent_hash: str, file_index: int):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -377,7 +377,7 @@ def _stream_torrent_files_zip(profile: dict, items: list[dict]):
@bp.post("/torrents/<torrent_hash>/files/download.zip")
def torrent_files_download_zip(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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")
def torrent_file_export(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -406,7 +406,7 @@ def torrent_file_export(torrent_hash: str):
@bp.post("/torrents/torrent-files.zip")
def torrent_files_export_zip():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
@@ -455,7 +455,7 @@ def torrent_files_export_zip():
@bp.get("/torrents/<torrent_hash>/chunks")
def torrent_chunks(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -467,7 +467,7 @@ def torrent_chunks(torrent_hash: str):
@bp.post("/torrents/<torrent_hash>/chunks/<action_name>")
def torrent_chunk_action(torrent_hash: str, action_name: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -480,7 +480,7 @@ def torrent_chunk_action(torrent_hash: str, action_name: str):
@bp.get("/torrents/<torrent_hash>/peers")
def torrent_peers(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
peers = rtorrent.torrent_peers(profile, torrent_hash)
@@ -496,7 +496,7 @@ def torrent_peers(torrent_hash: str):
@bp.get("/torrents/<torrent_hash>/trackers")
def torrent_trackers(torrent_hash: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
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>")
def torrent_tracker_action(torrent_hash: str, action_name: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
try:
@@ -518,7 +518,7 @@ def torrent_tracker_action(torrent_hash: str, action_name: str):
@bp.post("/torrents/<action_name>")
def torrent_action(action_name: str):
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}
@@ -547,7 +547,7 @@ def torrent_action(action_name: str):
@bp.post("/torrents/create")
def torrent_create():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
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 {})
@@ -577,7 +577,7 @@ def torrent_create():
@bp.post("/torrents/add")
def torrent_add():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
job_ids = []
@@ -634,7 +634,7 @@ def torrent_add():
@bp.post("/torrents/preview")
def torrent_preview():
profile = preferences.active_profile()
profile = request_profile()
existing_hashes = set()
if profile:
try:
@@ -664,7 +664,7 @@ def torrent_preview():
@bp.post("/speed/limits")
def speed_limits():
profile = preferences.active_profile()
profile = request_profile()
if not profile:
return jsonify({"ok": False, "error": "No profile"}), 400
data = request.get_json(silent=True) or {}