from __future__ import annotations from flask import Blueprint, render_template, jsonify, Response, request, redirect, url_for, abort from ..services.preferences import get_preferences, list_profiles, active_profile, BOOTSTRAP_THEMES, FONT_FAMILIES, bootstrap_css_url from ..services import auth bp = Blueprint("main", __name__) @bp.route("/login", methods=["GET", "POST"]) def login(): # Note: When optional authentication is disabled, /login is intentionally unavailable. if not auth.enabled(): abort(404) error = "" if request.method == "POST": user = auth.login_user(request.form.get("username", ""), request.form.get("password", "")) if user: return redirect(request.args.get("next") or url_for("main.index")) error = "Invalid username or password" return render_template("login.html", error=error) @bp.get("/logout") def logout(): auth.logout_user() if not auth.enabled(): return redirect(url_for("main.index")) return redirect(url_for("main.login")) @bp.get("/") def index(): prefs = get_preferences() return render_template( "index.html", prefs=prefs, profiles=list_profiles(), active_profile=active_profile(), bootstrap_themes=BOOTSTRAP_THEMES, font_families=FONT_FAMILIES, bootstrap_css_url=bootstrap_css_url((prefs or {}).get("bootstrap_theme")), auth_enabled=auth.enabled(), current_user=auth.current_user(), ) @bp.get("/docs") def docs(): html = """pyTorrent API Docs
""" return Response(html, mimetype="text/html") @bp.get("/api/openapi.json") def openapi(): paths = { "/api/profiles": { "get": {"summary": "List rTorrent profiles", "responses": {"200": {"description": "Profiles"}}}, "post": {"summary": "Create rTorrent profile", "requestBody": {"required": True, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "scgi_url": {"type": "string"}, "timeout_seconds": {"type": "integer"}, "max_parallel_jobs": {"type": "integer", "default": 5, "description": "Maximum queued jobs that may run at once for this rTorrent. Move/remove jobs keep request order."}, "is_remote": {"type": "boolean", "description": "When true, CPU/RAM host usage is hidden; public IP checks try remote rTorrent commands when supported."}}}}}}, "responses": {"200": {"description": "Created"}}} }, "/api/profiles/{profile_id}": { "put": {"summary": "Update rTorrent profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "requestBody": {"required": True, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "scgi_url": {"type": "string"}, "timeout_seconds": {"type": "integer"}, "max_parallel_jobs": {"type": "integer", "default": 5, "description": "Maximum queued jobs that may run at once for this rTorrent. Move/remove jobs keep request order."}, "is_remote": {"type": "boolean", "description": "When true, CPU/RAM host usage is hidden; public IP checks try remote rTorrent commands when supported."}}}}}}, "responses": {"200": {"description": "Updated"}}}, "delete": {"summary": "Delete rTorrent profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Deleted"}}} }, "/api/profiles/{profile_id}/activate": {"post": {"summary": "Activate profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Activated"}}}}, "/api/preferences": { "get": {"summary": "Get preferences", "responses": {"200": {"description": "Preferences including theme, bootstrap_theme and font_family"}}}, "post": { "summary": "Save preferences", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": { "theme": {"type": "string", "enum": ["light", "dark"]}, "bootstrap_theme": {"type": "string", "enum": list(BOOTSTRAP_THEMES.keys())}, "font_family": {"type": "string", "enum": list(FONT_FAMILIES.keys())}, "table_columns_json": {"type": "string"}, "peers_refresh_seconds": {"type": "integer", "enum": [0, 10, 15, 30, 60]}, "port_check_enabled": {"type": "boolean"}, }}}}}, "responses": {"200": {"description": "Saved"}}, }, }, "/api/torrents": {"get": {"summary": "Get cached torrent snapshot", "responses": {"200": {"description": "Torrent list"}}}}, "/api/torrents/{action_name}": {"post": {"summary": "Queue torrent action", "description": "For move, path is the target directory; move_data=true physically moves data on the rTorrent host using a detached shell move with status polling, force-overwrites an existing destination, tolerates rTorrent execute timeouts around mkdir/start/polling, handles retries after a partially completed move, avoids SCGI timeout on long mv operations, and recheck defaults to move_data. Large move selections are split into ordered bulk parts of up to 100 hashes. Move and remove jobs are ordered per profile, so a later remove waits for earlier move/remove jobs to finish.", "parameters": [{"name": "action_name", "in": "path", "required": True, "schema": {"type": "string", "enum": ["start", "pause", "stop", "resume", "recheck", "remove", "move", "set_label", "set_ratio_group"]}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"hashes": {"type": "array", "items": {"type": "string"}}, "path": {"type": "string", "description": "Target directory for move"}, "move_data": {"type": "boolean", "description": "Physically move data before setting torrent directory"}, "recheck": {"type": "boolean", "description": "Run hash check after physical move; defaults to move_data"}, "label": {"type": "string"}, "ratio_group": {"type": "string"}, "remove_data": {"type": "boolean"}}}}}}, "responses": {"200": {"description": "Job queued"}}}}, "/api/torrents/add": {"post": {"summary": "Add magnet links or torrent files", "requestBody": {"content": {"multipart/form-data": {"schema": {"type": "object", "properties": {"uris": {"type": "string"}, "directory": {"type": "string"}, "label": {"type": "string"}, "start": {"type": "boolean"}, "files": {"type": "array", "items": {"type": "string", "format": "binary"}}}}}, "application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "Jobs queued"}}}}, "/api/torrents/{torrent_hash}/files": {"get": {"summary": "Torrent files", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": {"description": "Files"}}}}, "/api/torrents/{torrent_hash}/peers": {"get": {"summary": "Torrent peers with GeoIP", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": {"description": "Peers"}}}}, "/api/torrents/{torrent_hash}/trackers": {"get": {"summary": "Torrent trackers", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": {"description": "Trackers"}}}}, "/api/speed/limits": {"post": {"summary": "Queue global speed limit change", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"down": {"type": "integer", "description": "Bytes per second, 0 unlimited"}, "up": {"type": "integer", "description": "Bytes per second, 0 unlimited"}}}}}}, "responses": {"200": {"description": "Job queued"}}}}, "/api/system/status": {"get": {"summary": "rTorrent/system status", "description": "For remote profiles CPU/RAM host usage is not returned and usage_available is false.", "responses": {"200": {"description": "Status"}}}}, "/api/port-check": {"get": {"summary": "Read cached incoming port check status", "responses": {"200": {"description": "Port check status including status, port, public_ip, source, cached, checked_at and checked_at_epoch"}}}, "post": {"summary": "Run incoming port check immediately, bypassing cache", "responses": {"200": {"description": "Fresh port check status including checked_at and checked_at_epoch"}}}}, "/api/jobs": {"get": {"summary": "List job queue history", "parameters": [{"name": "limit", "in": "query", "schema": {"type": "integer", "default": 50}}, {"name": "offset", "in": "query", "schema": {"type": "integer", "default": 0}}], "responses": {"200": {"description": "Jobs"}}}}, "/api/jobs/clear": {"post": {"summary": "Clear finished job history", "description": "Deletes jobs that are not pending or running.", "responses": {"200": {"description": "Deleted count"}}}}, "/api/jobs/{job_id}/cancel": {"post": {"summary": "Cancel pending or failed job", "parameters": [{"name": "job_id", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": {"description": "Cancelled"}}}}, "/api/jobs/{job_id}/retry": {"post": {"summary": "Retry failed or cancelled job", "parameters": [{"name": "job_id", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": {"description": "Retried"}}}}, "/api/path/browse": {"get": {"summary": "Browse server directories", "parameters": [{"name": "path", "in": "query", "schema": {"type": "string"}}], "responses": {"200": {"description": "Directory listing"}}}}, "/api/labels": {"get": {"summary": "List labels", "responses": {"200": {"description": "Labels"}}}, "post": {"summary": "Create label", "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "Labels"}}}}, "/api/ratio-groups": {"get": {"summary": "List ratio groups", "responses": {"200": {"description": "Ratio groups"}}}, "post": {"summary": "Create or update ratio group", "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "Ratio groups"}}}}, "/api/rss": {"get": {"summary": "List RSS feeds and rules", "responses": {"200": {"description": "RSS config"}}}}, "/api/rss/feeds": {"post": {"summary": "Add RSS feed", "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "RSS config"}}}}, "/api/rss/rules": {"post": {"summary": "Add RSS rule", "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": {"description": "RSS config"}}}}, "/api/rss/check": {"post": {"summary": "Manually check RSS feeds", "responses": {"200": {"description": "Queued matches"}}}}, "/api/smart-queue": {"get": {"summary": "Get Smart Queue settings, exceptions and history", "parameters": [{"name": "history_limit", "in": "query", "schema": {"type": "integer", "default": 10, "minimum": 1, "maximum": 100}, "description": "Number of Smart Queue history rows to return"}], "responses": {"200": {"description": "Smart Queue config with history and history_total"}}}, "post": {"summary": "Save Smart Queue settings", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "max_active_downloads": {"type": "integer"}, "stalled_seconds": {"type": "integer"}, "min_speed_bytes": {"type": "integer"}, "min_seeds": {"type": "integer"}}}}}}, "responses": {"200": {"description": "Saved"}}}}, "/api/smart-queue/check": {"post": {"summary": "Run Smart Queue immediately", "responses": {"200": {"description": "Smart Queue action result"}}}}, "/api/smart-queue/exclusion": {"post": {"summary": "Add or remove a torrent Smart Queue exception", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"hash": {"type": "string"}, "excluded": {"type": "boolean"}, "reason": {"type": "string"}}}}}}, "responses": {"200": {"description": "Exception list"}}}}, "/api/traffic/history": {"get": {"summary": "Transfer history for charts", "parameters": [{"name": "range", "in": "query", "schema": {"type": "string", "enum": ["15m", "1h", "3h", "6h", "24h", "7d", "30d", "90d"]}}], "responses": {"200": {"description": "Aggregated traffic history"}}}} } paths.update({ "/api/auth/login": {"post": {"summary": "Log in with username and password when authentication is enabled", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"username": {"type": "string"}, "password": {"type": "string", "format": "password"}}, "required": ["username", "password"]}}}}, "responses": {"200": {"description": "Logged in"}, "401": {"description": "Invalid credentials"}, "404": {"description": "Authentication disabled"}}}}, "/api/auth/me": {"get": {"summary": "Return current authenticated user", "responses": {"200": {"description": "Current user"}, "404": {"description": "Authentication disabled"}}}}, "/api/auth/logout": {"post": {"summary": "Log out current user", "responses": {"200": {"description": "Logged out"}, "404": {"description": "Authentication disabled"}}}}, "/api/auth/users": {"get": {"summary": "List users, admin only", "responses": {"200": {"description": "Users"}, "403": {"description": "Admin only"}}}, "post": {"summary": "Create user, admin only", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthUserInput"}}}}, "responses": {"200": {"description": "User created"}, "403": {"description": "Admin only"}}}}, "/api/auth/users/{user_id}": {"put": {"summary": "Update user, admin only", "parameters": [{"name": "user_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthUserInput"}}}}, "responses": {"200": {"description": "User updated"}, "403": {"description": "Admin only"}}}, "delete": {"summary": "Delete user, admin only", "parameters": [{"name": "user_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "User deleted"}, "403": {"description": "Admin only"}}}}, "/api/profiles/{profile_id}": {"delete": {"summary": "Delete rTorrent profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Deleted"}}}}, "/api/torrent-stats": {"get": {"summary": "Torrent statistics and cached file metadata", "parameters": [{"name": "force", "in": "query", "schema": {"type": "boolean", "default": False}}], "responses": {"200": {"description": "Torrent statistics"}}}}, "/api/path/default": {"get": {"summary": "Read active rTorrent default download path", "responses": {"200": {"description": "Default path"}}}}, "/api/torrents/{torrent_hash}/files/priority": {"post": {"summary": "Set file priorities", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"files": {"type": "array", "items": {"type": "object", "properties": {"index": {"type": "integer"}, "priority": {"type": "integer", "enum": [0, 1, 2]}}}}}}}}}, "responses": {"200": {"description": "Updated priorities"}, "207": {"description": "Partial update"}}}}, "/api/torrents/{torrent_hash}/peers/action": {"post": {"summary": "Run peer action", "parameters": [{"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"peer_index": {"type": "integer"}, "action": {"type": "string", "enum": ["disconnect", "kick", "snub", "unsnub", "ban"]}}}}}}, "responses": {"200": {"description": "Peer action result"}}}}, "/api/labels/{label_id}": {"delete": {"summary": "Delete saved label", "parameters": [{"name": "label_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Labels"}}}}, "/api/rtorrent-config": {"get": {"summary": "Read supported rTorrent config fields", "responses": {"200": {"description": "Config fields"}}}, "post": {"summary": "Save supported rTorrent config fields", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"values": {"type": "object"}}}}}}, "responses": {"200": {"description": "Save result"}}}}, "/api/rtorrent-config/generate": {"post": {"summary": "Generate rTorrent config text from provided values", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"values": {"type": "object"}}}}}}, "responses": {"200": {"description": "Generated config text"}}}}, "/api/automations": {"get": {"summary": "List automation rules and history", "responses": {"200": {"description": "Rules and history"}}}, "post": {"summary": "Create or update automation rule", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "enabled": {"type": "boolean"}, "cooldown_minutes": {"type": "integer"}, "conditions": {"type": "array"}, "effects": {"type": "array"}}}}}}, "responses": {"200": {"description": "Rule saved"}}}}, "/api/automations/{rule_id}": {"delete": {"summary": "Delete automation rule", "parameters": [{"name": "rule_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Deleted"}}}}, "/api/automations/check": {"post": {"summary": "Run automation rules immediately", "responses": {"200": {"description": "Automation result"}}}} }) components = { "schemas": { "ApiOk": { "type": "object", "properties": {"ok": {"type": "boolean"}}, "required": ["ok"], }, "AuthUserInput": { "type": "object", "properties": { "username": {"type": "string"}, "password": {"type": "string", "format": "password", "description": "Optional on update"}, "role": {"type": "string", "enum": ["admin", "user"]}, "is_active": {"type": "boolean"}, "permissions": {"type": "array", "items": {"type": "object", "properties": {"profile_id": {"type": "integer", "description": "0 means all profiles"}, "access_level": {"type": "string", "enum": ["ro", "full"]}}}}, }, "required": ["username"], }, "Profile": { "type": "object", "additionalProperties": True, "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, "scgi_url": {"type": "string"}, "timeout_seconds": {"type": "integer"}, "max_parallel_jobs": {"type": "integer"}, }, }, "Torrent": { "type": "object", "additionalProperties": True, "properties": { "hash": {"type": "string"}, "name": {"type": "string"}, "path": {"type": "string"}, "status": {"type": "string"}, "size": {"type": "integer", "format": "int64"}, "completed_bytes": {"type": "integer", "format": "int64"}, "down_total": {"type": "integer", "format": "int64"}, "up_total": {"type": "integer", "format": "int64"}, "complete": {"type": "boolean"}, "state": {"type": "boolean"}, "paused": {"type": "boolean"}, "hashing": {"type": "integer"}, "message": {"type": "string"}, }, }, "TorrentFilterSummary": { "type": "object", "properties": { "count": {"type": "integer", "description": "Number of torrents in this filter."}, "size": {"type": "integer", "format": "int64", "description": "Total torrent payload size in bytes."}, "disk_bytes": {"type": "integer", "format": "int64", "description": "Completed bytes reported by rTorrent; used as the displayed Data value."}, "completed_bytes": {"type": "integer", "format": "int64", "description": "Completed bytes reported by rTorrent."}, "remaining_bytes": {"type": "integer", "format": "int64", "description": "size - completed_bytes, never below zero."}, "progress_percent": {"type": "number", "format": "float", "description": "Completed percentage for this filter."}, "remaining_percent": {"type": "number", "format": "float", "description": "Remaining percentage for this filter."}, "down_total": {"type": "integer", "format": "int64", "deprecated": True, "description": "Backward compatibility field; not used by the filters UI."}, "up_total": {"type": "integer", "format": "int64", "deprecated": True, "description": "Backward compatibility field; not used by the filters UI."}, }, "required": ["count", "size", "disk_bytes", "completed_bytes", "remaining_bytes", "progress_percent", "remaining_percent"], }, "TorrentSummaryFilters": { "type": "object", "properties": { "all": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "downloading": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "seeding": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "paused": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "checking": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "error": {"$ref": "#/components/schemas/TorrentFilterSummary"}, "stopped": {"$ref": "#/components/schemas/TorrentFilterSummary"}, }, "required": ["all", "downloading", "seeding", "paused", "checking", "error", "stopped"], }, "TorrentSummary": { "type": "object", "properties": { "filters": {"$ref": "#/components/schemas/TorrentSummaryFilters"}, "cache_ttl_seconds": {"type": "integer", "description": "Summary cache TTL in seconds."}, "generated_at_epoch": {"type": "number", "format": "double", "description": "Unix timestamp when summary was generated."}, "cached": {"type": "boolean", "description": "True when returned from cache."}, }, "required": ["filters", "cache_ttl_seconds", "generated_at_epoch", "cached"], }, "TorrentListResponse": { "allOf": [ {"$ref": "#/components/schemas/ApiOk"}, {"type": "object", "properties": { "profile_id": {"type": "integer"}, "torrents": {"type": "array", "items": {"$ref": "#/components/schemas/Torrent"}}, "summary": {"$ref": "#/components/schemas/TorrentSummary"}, "error": {"type": "string", "nullable": True}, }, "required": ["torrents", "summary"]}, ], }, "CleanupSummary": { "type": "object", "properties": { "jobs_total": {"type": "integer"}, "jobs_clearable": {"type": "integer"}, "smart_queue_history_total": {"type": "integer"}, "retention_days": {"type": "object", "properties": {"jobs": {"type": "integer"}, "smart_queue_history": {"type": "integer"}}}, "database": {"type": "object", "properties": {"path": {"type": "string"}, "size": {"type": "integer", "format": "int64"}, "size_h": {"type": "string"}, "error": {"type": "string"}}}, }, "required": ["jobs_total", "jobs_clearable", "smart_queue_history_total", "retention_days", "database"], }, "CleanupResponse": { "allOf": [ {"$ref": "#/components/schemas/ApiOk"}, {"type": "object", "properties": {"cleanup": {"$ref": "#/components/schemas/CleanupSummary"}, "deleted": {"oneOf": [{"type": "integer"}, {"type": "object"}]}}}, ], }, "PortCheckStatus": { "type": "object", "additionalProperties": True, "properties": { "status": {"type": "string", "enum": ["open", "closed", "unknown", "disabled", "error"]}, "enabled": {"type": "boolean"}, "port": {"type": "integer"}, "public_ip": {"type": "string"}, "source": {"type": "string"}, "cached": {"type": "boolean"}, "checked_at": {"type": "string", "format": "date-time"}, "checked_at_epoch": {"type": "number", "format": "double"}, "error": {"type": "string"}, }, }, "AppStatus": { "type": "object", "properties": { "pytorrent": {"type": "object", "additionalProperties": True}, "cleanup": {"$ref": "#/components/schemas/CleanupSummary"}, "profile": {"$ref": "#/components/schemas/Profile"}, "scgi": {"type": "object", "nullable": True, "additionalProperties": True}, "port_check": {"$ref": "#/components/schemas/PortCheckStatus"}, "api_ms": {"type": "number", "format": "float"}, }, "required": ["pytorrent", "cleanup", "scgi", "port_check", "api_ms"], }, "AppStatusResponse": { "allOf": [ {"$ref": "#/components/schemas/ApiOk"}, {"type": "object", "properties": {"status": {"$ref": "#/components/schemas/AppStatus"}}, "required": ["status"]}, ], }, "JobQueuedResponse": { "allOf": [ {"$ref": "#/components/schemas/ApiOk"}, {"type": "object", "properties": {"job_id": {"type": "string"}, "job_ids": {"type": "array", "items": {"type": "string"}}, "hash_count": {"type": "integer"}, "bulk": {"type": "boolean"}}}, ], }, "TrackerActionResponse": { "allOf": [ {"$ref": "#/components/schemas/ApiOk"}, {"type": "object", "properties": {"result": {"type": "object", "additionalProperties": True}, "message": {"type": "string"}}}, ], }, } } def response_ref(schema_name: str, description: str = "OK") -> dict: return {"description": description, "content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{schema_name}"}}}} paths["/api/torrents"]["get"]["responses"]["200"] = response_ref("TorrentListResponse", "Torrent list with cached filter summary") paths["/api/torrents/{action_name}"]["post"]["responses"]["200"] = response_ref("JobQueuedResponse", "Job queued") paths["/api/torrents/add"]["post"]["responses"]["200"] = response_ref("JobQueuedResponse", "Jobs queued") paths.update({ "/api/torrents/{torrent_hash}/trackers/{action_name}": { "post": { "summary": "Run tracker action", "parameters": [ {"name": "torrent_hash", "in": "path", "required": True, "schema": {"type": "string"}}, {"name": "action_name", "in": "path", "required": True, "schema": {"type": "string"}}, ], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}}}}, "responses": {"200": response_ref("TrackerActionResponse", "Tracker action result")}, } }, "/api/app/status": { "get": {"summary": "pyTorrent application status", "responses": {"200": response_ref("AppStatusResponse", "Application status")}} }, "/api/cleanup/summary": { "get": {"summary": "Cleanup summary", "responses": {"200": response_ref("CleanupResponse", "Cleanup summary")}} }, "/api/cleanup/jobs": { "post": {"summary": "Clear finished job history", "responses": {"200": response_ref("CleanupResponse", "Cleanup result")}} }, "/api/cleanup/smart-queue": { "post": {"summary": "Clear Smart Queue history", "responses": {"200": response_ref("CleanupResponse", "Cleanup result")}} }, "/api/cleanup/all": { "post": {"summary": "Clear all cleanup-supported history", "responses": {"200": response_ref("CleanupResponse", "Cleanup result")}} }, }) components.setdefault("securitySchemes", {})["sessionCookie"] = {"type": "apiKey", "in": "cookie", "name": "session"} for path, methods in paths.items(): if path != "/api/auth/login": for operation in methods.values(): operation.setdefault("security", [{"sessionCookie": []}]) return jsonify({"openapi": "3.0.3", "info": {"title": "pyTorrent API", "version": "0.0.1"}, "paths": paths, "components": components})