from __future__ import annotations
from flask import Blueprint, render_template, jsonify, Response
from ..services.preferences import get_preferences, list_profiles, active_profile, BOOTSTRAP_THEMES, FONT_FAMILIES, bootstrap_css_url
bp = Blueprint("main", __name__)
@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")),
)
@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/profiles/{profile_id}": {"delete": {"summary": "Delete rTorrent profile", "parameters": [{"name": "profile_id", "in": "path", "required": True, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Deleted"}}}},
"/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"],
},
"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")}}
},
})
return jsonify({"openapi": "3.0.3", "info": {"title": "pyTorrent API", "version": "0.2.0"}, "paths": paths, "components": components})