fix planner
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from importlib import import_module
|
||||
|
||||
API_ROUTE_MODULES = (
|
||||
"torrents",
|
||||
"profiles",
|
||||
"rss",
|
||||
"automations",
|
||||
"smart_queue",
|
||||
"system",
|
||||
"backup",
|
||||
"operation_logs",
|
||||
"planner",
|
||||
)
|
||||
|
||||
|
||||
def load_api_route_modules() -> None:
|
||||
"""Import API route modules so their shared blueprint decorators are registered."""
|
||||
for module_name in API_ROUTE_MODULES:
|
||||
import_module(f"{__name__}.{module_name}")
|
||||
+16
-19
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import platform
|
||||
@@ -19,7 +18,6 @@ import threading
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
from flask import Blueprint, jsonify, request, abort, send_file, redirect, Response, stream_with_context, url_for
|
||||
# Note: url_for is exported through this shared module for API routes that build temporary in-app links.
|
||||
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
|
||||
@@ -34,23 +32,33 @@ bp = Blueprint("api", __name__, url_prefix="/api")
|
||||
|
||||
MOVE_BULK_MAX_HASHES = 100
|
||||
|
||||
|
||||
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."""
|
||||
"""Return the optional rTorrent 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 ""
|
||||
|
||||
profile_id = (
|
||||
request.args.get("profile_id")
|
||||
or request.form.get("profile_id")
|
||||
or payload.get("rtorrent_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("rtorrent_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):
|
||||
@@ -123,13 +131,9 @@ def ok(payload=None):
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
|
||||
from ..services.port_check import port_check_status
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _safe_len(callable_obj) -> int | None:
|
||||
try:
|
||||
return len(callable_obj())
|
||||
@@ -261,13 +265,11 @@ def enrich_bulk_payload(profile: dict, action_name: str, data: dict) -> dict:
|
||||
|
||||
|
||||
def _chunk_hashes(hashes: list[str], size: int = MOVE_BULK_MAX_HASHES) -> list[list[str]]:
|
||||
# Note: Splits very large torrent selections into predictable chunks so each queued job stays small and recoverable.
|
||||
safe_size = max(1, int(size or MOVE_BULK_MAX_HASHES))
|
||||
return [hashes[index:index + safe_size] for index in range(0, len(hashes), safe_size)]
|
||||
|
||||
|
||||
def enqueue_bulk_parts(profile: dict, action_name: str, data: dict) -> list[dict]:
|
||||
# Note: One shared helper splits large move/remove operations into small ordered parts without changing other actions.
|
||||
base_payload = enrich_bulk_payload(profile, action_name, data)
|
||||
hashes = base_payload.get("hashes") or []
|
||||
chunks = _chunk_hashes(hashes)
|
||||
@@ -297,17 +299,14 @@ def enqueue_bulk_parts(profile: dict, action_name: str, data: dict) -> list[dict
|
||||
|
||||
|
||||
def enqueue_move_bulk_parts(profile: dict, data: dict) -> list[dict]:
|
||||
# Note: Keep the old public move helper while using the same partitioning logic.
|
||||
return enqueue_bulk_parts(profile, "move", data)
|
||||
|
||||
|
||||
def enqueue_remove_bulk_parts(profile: dict, data: dict) -> list[dict]:
|
||||
# Note: Remove/rm uses the same partitioning as move, which lowers rTorrent load.
|
||||
return enqueue_bulk_parts(profile, "remove", data)
|
||||
|
||||
|
||||
def _user_disk_status(profile: dict) -> dict:
|
||||
# Note: Disk usage is user-preference aware, so it is read separately from the shared Socket.IO poller.
|
||||
prefs = preferences.get_disk_monitor_preferences(profile.get("id") if profile else None)
|
||||
try:
|
||||
paths = json.loads((prefs or {}).get("disk_monitor_paths_json") or "[]") if prefs else []
|
||||
@@ -321,6 +320,4 @@ def _user_disk_status(profile: dict) -> dict:
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Note: Route modules import shared helpers with wildcard imports; include private helper names intentionally.
|
||||
__all__ = [name for name in globals() if not name.startswith('__')]
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import bp
|
||||
from . import load_api_route_modules
|
||||
|
||||
# Note: Route modules are imported for their decorators; this keeps the public API unchanged.
|
||||
from . import torrents as _torrents_routes
|
||||
from . import profiles as _profiles_routes
|
||||
from . import rss as _rss_routes
|
||||
from . import automations as _automations_routes
|
||||
from . import smart_queue as _smart_queue_routes
|
||||
from . import system as _system_routes
|
||||
from . import backup as _backup_routes
|
||||
from . import operation_logs as _operation_logs_routes
|
||||
load_api_route_modules()
|
||||
|
||||
__all__ = ["bp"]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import abort, jsonify, request
|
||||
|
||||
from ..services.auth import current_user, list_users, save_user, delete_user, login_user, logout_user, enabled as auth_enabled, provider as auth_provider, uses_external_provider, external_auth_summary, list_api_tokens, create_api_token, revoke_api_token
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from ..services import auth
|
||||
|
||||
@@ -53,7 +52,6 @@ def backup_create_app():
|
||||
|
||||
@bp.post("/backup")
|
||||
def backup_create():
|
||||
# Note: Legacy endpoint now creates a profile backup so non-admin users cannot capture other users' settings.
|
||||
return backup_create_profile()
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ from ..services.preferences import get_preferences, list_profiles, active_profil
|
||||
from ..services import auth, pdf_preview_links, rtorrent
|
||||
from ..config import PYTORRENT_TMP_DIR, SMART_QUEUE_LABEL, SMART_QUEUE_STALLED_LABEL
|
||||
from ..services.frontend_assets import asset_path
|
||||
|
||||
# for favicon
|
||||
from flask import current_app, send_from_directory
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
@@ -24,8 +22,6 @@ def _asset_url(key: str) -> str:
|
||||
return path if path.startswith("http") else url_for("static", filename=path)
|
||||
|
||||
|
||||
|
||||
|
||||
def _attachment_headers(download_name: str, content_type: str = "application/octet-stream", disposition: str = "attachment") -> dict:
|
||||
safe = Path(download_name or "download.bin").name or "download.bin"
|
||||
safe_disposition = "inline" if disposition == "inline" else "attachment"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from ..services import operation_logs
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask import jsonify, request
|
||||
|
||||
from ._shared import request_profile
|
||||
from ..services import preferences, download_planner, poller_control
|
||||
from ._shared import bp, request_profile
|
||||
from ..services import download_planner, poller_control
|
||||
from ..services.auth import current_user_id
|
||||
|
||||
bp = Blueprint("planner_api", __name__, url_prefix="/api")
|
||||
|
||||
|
||||
def ok(payload=None):
|
||||
data = {"ok": True}
|
||||
if payload:
|
||||
@@ -33,7 +30,7 @@ def download_planner_get():
|
||||
|
||||
@bp.post("/download-planner")
|
||||
def download_planner_save():
|
||||
# Note: Planner settings are saved through one canonical endpoint to avoid hidden frontend/backend fallbacks.
|
||||
# Note: Planner settings are saved through one canonical endpoint to keep the frontend/backend contract explicit.
|
||||
profile, error = _profile_or_error()
|
||||
if error:
|
||||
return error
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from ..services.rtorrent.diagnostics import profile_diagnostics
|
||||
from ..services import auth
|
||||
@@ -26,7 +25,6 @@ def profiles_create():
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
|
||||
|
||||
|
||||
@bp.put("/profiles/<int:profile_id>")
|
||||
def profiles_update(profile_id: int):
|
||||
try:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
|
||||
|
||||
def _active_profile_or_400():
|
||||
profile = request_profile()
|
||||
if not profile:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
|
||||
|
||||
@bp.get('/smart-queue')
|
||||
def smart_queue_get():
|
||||
from ..services import smart_queue
|
||||
@@ -19,7 +19,6 @@ def smart_queue_get():
|
||||
return jsonify({'ok': False, 'error': str(exc), 'settings': {}, 'exclusions': []})
|
||||
|
||||
|
||||
|
||||
@bp.post('/smart-queue')
|
||||
def smart_queue_save():
|
||||
from ..services import smart_queue
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
import posixpath
|
||||
from ..services import operation_logs
|
||||
@@ -27,7 +26,6 @@ def system_status():
|
||||
status["disk"] = _user_disk_status(profile)
|
||||
if bool(profile.get("is_remote")):
|
||||
try:
|
||||
# Note: Remote profiles must report CPU/RAM from the rTorrent host, not hide the footer stats.
|
||||
usage = rtorrent.remote_system_usage(profile)
|
||||
status.update(usage)
|
||||
status["usage_available"] = True
|
||||
@@ -40,7 +38,6 @@ def system_status():
|
||||
status["ram"] = psutil.virtual_memory().percent
|
||||
status["usage_source"] = "local"
|
||||
status["usage_available"] = True
|
||||
# Note: REST status returns the latest records without waiting for the next Socket.IO message.
|
||||
status["speed_peaks"] = speed_peaks.record(profile["id"], status.get("down_rate", 0), status.get("up_rate", 0))
|
||||
return ok({"status": status})
|
||||
except Exception as exc:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from ..services import profile_speed_limits
|
||||
from ..services import pdf_preview_links, torrent_creator
|
||||
@@ -20,7 +19,6 @@ def torrents():
|
||||
|
||||
|
||||
|
||||
|
||||
@bp.get("/trackers/summary")
|
||||
def trackers_summary():
|
||||
profile = request_profile()
|
||||
|
||||
Reference in New Issue
Block a user