unit3dprep 1.0.5__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unit3dprep-1.0.5/unit3dprep.egg-info → unit3dprep-1.1.0}/PKG-INFO +2 -2
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/README.md +1 -1
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/pyproject.toml +1 -1
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/i18n.py +3 -3
- unit3dprep-1.1.0/unit3dprep/web/api/reseed.py +142 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/search.py +0 -6
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/app.py +2 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/clients.py +101 -0
- unit3dprep-1.1.0/unit3dprep/web/dist/assets/index-BkzkykEO.js +255 -0
- unit3dprep-1.1.0/unit3dprep/web/dist/assets/index-FFoOmpDN.css +1 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/dist/index.html +2 -2
- unit3dprep-1.1.0/unit3dprep/web/reseed.py +537 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0/unit3dprep.egg-info}/PKG-INFO +2 -2
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep.egg-info/SOURCES.txt +4 -2
- unit3dprep-1.0.5/unit3dprep/web/dist/assets/index-BlAGpoR4.js +0 -255
- unit3dprep-1.0.5/unit3dprep/web/dist/assets/index-DChRHChM.css +0 -1
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/LICENSE +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/MANIFEST.in +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/setup.cfg +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/__init__.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/cli.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/core.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/media.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/upload.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/__init__.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/_env.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/__init__.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/auth.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/fs.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/library.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/logs.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/queue.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/quickupload.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/settings.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/tmdb.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/trackers.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/uploaded.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/version.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/webup.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/api/wizard.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/auth.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/config.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/db.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/duplicate_check.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/lang_cache.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/logbuf.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/tmdb_cache.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/trackers.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/webup_client.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/webup_job_fix.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/webup_logclass.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/webup_orchestrator.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep/web/webup_ws.py +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep.egg-info/dependency_links.txt +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep.egg-info/entry_points.txt +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep.egg-info/requires.txt +0 -0
- {unit3dprep-1.0.5 → unit3dprep-1.1.0}/unit3dprep.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unit3dprep
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Web UI + CLI di pre-flight per tracker Unit3D, companion di Unit3DWebUp (audio ITA, nomenclatura ItaTorrents, hardlink, upload)
|
|
5
5
|
Author: Davide Sidoti
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -723,7 +723,7 @@ Dynamic: license-file
|
|
|
723
723
|

|
|
724
724
|
|
|
725
725
|
[](https://pypi.org/project/unit3dprep/)
|
|
726
|
-
[](https://hub.docker.com/r/hashdeveloper512/unit3dprep)
|
|
727
727
|
|
|
728
728
|
Web UI + CLI di pre-flight per tracker Unit3D, accoppiata via HTTP a [`Unit3DWebUp`](https://pypi.org/project/Unit3DwebUp/) come backend di upload.
|
|
729
729
|
Verifica tracce audio italiane, rinomina secondo la [nomenclatura ItaTorrents](docs/nomenclatura.md), crea hardlink in `~/seedings/` e orchestra il flusso `setenv → scan → maketorrent → upload → seed` su `Unit3DWebUp` con log live via WebSocket/SSE.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
[](https://pypi.org/project/unit3dprep/)
|
|
12
|
-
[](https://hub.docker.com/r/hashdeveloper512/unit3dprep)
|
|
13
13
|
|
|
14
14
|
Web UI + CLI di pre-flight per tracker Unit3D, accoppiata via HTTP a [`Unit3DWebUp`](https://pypi.org/project/Unit3DwebUp/) come backend di upload.
|
|
15
15
|
Verifica tracce audio italiane, rinomina secondo la [nomenclatura ItaTorrents](docs/nomenclatura.md), crea hardlink in `~/seedings/` e orchestra il flusso `setenv → scan → maketorrent → upload → seed` su `Unit3DWebUp` con log live via WebSocket/SSE.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unit3dprep"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Web UI + CLI di pre-flight per tracker Unit3D, companion di Unit3DWebUp (audio ITA, nomenclatura ItaTorrents, hardlink, upload)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -68,9 +68,9 @@ CATALOG: dict[str, dict[str, str]] = {
|
|
|
68
68
|
"it": "Tracker sconosciuto '{tracker}'",
|
|
69
69
|
"en": "Unknown tracker '{tracker}'",
|
|
70
70
|
},
|
|
71
|
-
"err.
|
|
72
|
-
"it": "
|
|
73
|
-
"en": "Reseed
|
|
71
|
+
"err.reseed_session_expired": {
|
|
72
|
+
"it": "Sessione reseed non trovata o scaduta",
|
|
73
|
+
"en": "Reseed session not found or expired",
|
|
74
74
|
},
|
|
75
75
|
"err.record_not_found": {
|
|
76
76
|
"it": "Record non trovato",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Reseed endpoints: discover 0-seed ITT torrents already on disk and re-seed.
|
|
2
|
+
|
|
3
|
+
- `GET /api/reseed/scan` — SSE, batched candidate discovery (library↔ITT)
|
|
4
|
+
- `GET /api/reseed/suggest` — torrent meta + size-matched local files (manual)
|
|
5
|
+
- `POST /api/reseed/start` — create a reseed session, returns a token
|
|
6
|
+
- `GET /api/reseed/{tok}/run`— SSE, run the reseed (download → qBit → hardlink → recheck)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import secrets
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, AsyncGenerator
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
18
|
+
from fastapi.responses import JSONResponse
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
from sse_starlette.sse import EventSourceResponse
|
|
21
|
+
|
|
22
|
+
from ...i18n import get_request_lang, t as _i18n_t
|
|
23
|
+
from ...media import media_root, seedings_root
|
|
24
|
+
from .. import config as web_config
|
|
25
|
+
from ..logbuf import emit as log_emit
|
|
26
|
+
from ..reseed import perform_reseed, stream_reseed_candidates, suggest_local_files
|
|
27
|
+
|
|
28
|
+
router = APIRouter(prefix="/api", tags=["reseed"])
|
|
29
|
+
|
|
30
|
+
_sessions: dict[str, dict[str, Any]] = {}
|
|
31
|
+
_created: dict[str, float] = {}
|
|
32
|
+
_TTL = 3600
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _cleanup() -> None:
|
|
36
|
+
now = time.time()
|
|
37
|
+
for tok in [t for t, ct in _created.items() if now - ct > _TTL]:
|
|
38
|
+
_sessions.pop(tok, None)
|
|
39
|
+
_created.pop(tok, None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _create(state: dict[str, Any]) -> str:
|
|
43
|
+
_cleanup()
|
|
44
|
+
tok = secrets.token_urlsafe(24)
|
|
45
|
+
_sessions[tok] = state
|
|
46
|
+
_created[tok] = time.time()
|
|
47
|
+
return tok
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_source(p: str, lang: str | None = None) -> Path:
|
|
51
|
+
resolved = Path(p).resolve()
|
|
52
|
+
allowed = [media_root().resolve(), seedings_root().resolve()]
|
|
53
|
+
if not any(str(resolved).startswith(str(a)) for a in allowed):
|
|
54
|
+
raise HTTPException(403, _i18n_t("err.path_outside", lang))
|
|
55
|
+
if not resolved.exists():
|
|
56
|
+
raise HTTPException(404, _i18n_t("err.path_not_found_at", lang, path=str(resolved)))
|
|
57
|
+
return resolved
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Discovery
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.get("/reseed/scan")
|
|
66
|
+
async def reseed_scan(category: str, offset: int = 0, limit: int = 20, max_seeders: int = 0):
|
|
67
|
+
cfg = web_config.load()
|
|
68
|
+
safe_limit = max(1, min(int(limit or 20), 100))
|
|
69
|
+
safe_offset = max(0, int(offset or 0))
|
|
70
|
+
safe_max_seeders = max(0, min(int(max_seeders or 0), 100))
|
|
71
|
+
|
|
72
|
+
async def generate() -> AsyncGenerator[dict, None]:
|
|
73
|
+
async for kind, data in stream_reseed_candidates(
|
|
74
|
+
cfg, category, offset=safe_offset, limit=safe_limit,
|
|
75
|
+
max_seeders=safe_max_seeders,
|
|
76
|
+
):
|
|
77
|
+
yield {"event": kind, "data": json.dumps(data)}
|
|
78
|
+
|
|
79
|
+
return EventSourceResponse(generate())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/reseed/suggest")
|
|
83
|
+
async def reseed_suggest(torrent_id: int):
|
|
84
|
+
cfg = web_config.load()
|
|
85
|
+
return JSONResponse(await suggest_local_files(cfg, torrent_id))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Reseed run
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StartBody(BaseModel):
|
|
94
|
+
tracker: str = "ITT"
|
|
95
|
+
torrent_id: int
|
|
96
|
+
source_path: str
|
|
97
|
+
category: str = ""
|
|
98
|
+
kind: str = ""
|
|
99
|
+
title: str = ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.post("/reseed/start")
|
|
103
|
+
async def reseed_start(request: Request, body: StartBody):
|
|
104
|
+
lang = get_request_lang(request)
|
|
105
|
+
src = _validate_source(body.source_path, lang)
|
|
106
|
+
state = {
|
|
107
|
+
"tracker": (body.tracker or "ITT").upper(),
|
|
108
|
+
"torrent_id": int(body.torrent_id),
|
|
109
|
+
"source_path": str(src),
|
|
110
|
+
"category": body.category,
|
|
111
|
+
"kind": body.kind,
|
|
112
|
+
"title": body.title,
|
|
113
|
+
}
|
|
114
|
+
tok = _create(state)
|
|
115
|
+
return JSONResponse({"token": tok})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.get("/reseed/{tok}/run")
|
|
119
|
+
async def reseed_run(tok: str, request: Request):
|
|
120
|
+
lang = get_request_lang(request)
|
|
121
|
+
state = _sessions.get(tok)
|
|
122
|
+
if state is None:
|
|
123
|
+
raise HTTPException(404, _i18n_t("err.reseed_session_expired", lang))
|
|
124
|
+
cfg = web_config.load()
|
|
125
|
+
|
|
126
|
+
async def generate() -> AsyncGenerator[dict, None]:
|
|
127
|
+
async for ev in perform_reseed(
|
|
128
|
+
cfg,
|
|
129
|
+
tracker=state["tracker"],
|
|
130
|
+
torrent_id=state["torrent_id"],
|
|
131
|
+
source_path=state["source_path"],
|
|
132
|
+
category=state.get("category", ""),
|
|
133
|
+
kind=state.get("kind", ""),
|
|
134
|
+
title=state.get("title", ""),
|
|
135
|
+
):
|
|
136
|
+
if ev["event"] == "log":
|
|
137
|
+
log_emit("info", ev["data"], "reseed", source="reseed")
|
|
138
|
+
elif ev["event"] == "error":
|
|
139
|
+
log_emit("error", ev["data"], "reseed", source="reseed")
|
|
140
|
+
yield ev
|
|
141
|
+
|
|
142
|
+
return EventSourceResponse(generate())
|
|
@@ -32,9 +32,3 @@ async def search(request: Request, q: str, tracker: str = "ITT"):
|
|
|
32
32
|
"tracker": tracker.upper(),
|
|
33
33
|
"results": [r.to_dict() for r in results],
|
|
34
34
|
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@router.post("/reseed/{tracker}/{torrent_id}")
|
|
38
|
-
async def reseed(request: Request, tracker: str, torrent_id: int):
|
|
39
|
-
# Placeholder: trigger a unit3dup reseed run for a known torrent id.
|
|
40
|
-
raise HTTPException(501, t("err.reseed_not_implemented", get_request_lang(request)))
|
|
@@ -23,6 +23,7 @@ from .api import (
|
|
|
23
23
|
logs as logs_api,
|
|
24
24
|
quickupload as quickupload_api,
|
|
25
25
|
queue as queue_api,
|
|
26
|
+
reseed as reseed_api,
|
|
26
27
|
search as search_api,
|
|
27
28
|
settings as settings_api,
|
|
28
29
|
tmdb as tmdb_api,
|
|
@@ -124,6 +125,7 @@ for r in (
|
|
|
124
125
|
logs_api.router,
|
|
125
126
|
queue_api.router,
|
|
126
127
|
quickupload_api.router,
|
|
128
|
+
reseed_api.router,
|
|
127
129
|
search_api.router,
|
|
128
130
|
settings_api.router,
|
|
129
131
|
tmdb_api.router,
|
|
@@ -40,6 +40,33 @@ class TorrentClient(ABC):
|
|
|
40
40
|
@abstractmethod
|
|
41
41
|
async def remove(self, torrent_hash: str, delete_files: bool = False) -> None: ...
|
|
42
42
|
|
|
43
|
+
async def add_torrent(
|
|
44
|
+
self,
|
|
45
|
+
torrent_bytes: bytes,
|
|
46
|
+
*,
|
|
47
|
+
save_path: str,
|
|
48
|
+
paused: bool = True,
|
|
49
|
+
skip_checking: bool = False,
|
|
50
|
+
category: str | None = None,
|
|
51
|
+
tags: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
raise NotImplementedError(f"{self.name} client does not support add_torrent")
|
|
54
|
+
|
|
55
|
+
async def torrent_files(self, torrent_hash: str) -> list[dict]:
|
|
56
|
+
raise NotImplementedError(f"{self.name} client does not support torrent_files")
|
|
57
|
+
|
|
58
|
+
async def info_one(self, torrent_hash: str) -> dict | None:
|
|
59
|
+
raise NotImplementedError(f"{self.name} client does not support info_one")
|
|
60
|
+
|
|
61
|
+
async def recheck(self, torrent_hash: str) -> None:
|
|
62
|
+
raise NotImplementedError(f"{self.name} client does not support recheck")
|
|
63
|
+
|
|
64
|
+
async def resume(self, torrent_hash: str) -> None:
|
|
65
|
+
raise NotImplementedError(f"{self.name} client does not support resume")
|
|
66
|
+
|
|
67
|
+
async def pause(self, torrent_hash: str) -> None:
|
|
68
|
+
raise NotImplementedError(f"{self.name} client does not support pause")
|
|
69
|
+
|
|
43
70
|
|
|
44
71
|
# ---------------------------------------------------------------------------
|
|
45
72
|
# qBittorrent Web API (v2)
|
|
@@ -120,6 +147,80 @@ class QBittorrentClient(TorrentClient):
|
|
|
120
147
|
r2 = await cli.post("/api/v2/torrents/resume", data={"hashes": torrent_hash})
|
|
121
148
|
r2.raise_for_status()
|
|
122
149
|
|
|
150
|
+
async def add_torrent(
|
|
151
|
+
self,
|
|
152
|
+
torrent_bytes: bytes,
|
|
153
|
+
*,
|
|
154
|
+
save_path: str,
|
|
155
|
+
paused: bool = True,
|
|
156
|
+
skip_checking: bool = False,
|
|
157
|
+
category: str | None = None,
|
|
158
|
+
tags: str | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Add a .torrent (raw bytes) pointing at `save_path`.
|
|
161
|
+
|
|
162
|
+
For reseed the caller adds **paused** (so it never announces before the
|
|
163
|
+
content is in place), hardlinks the matching files into the layout qBit
|
|
164
|
+
reports via `torrent_files()`, then triggers an explicit `recheck()`.
|
|
165
|
+
Keep `skip_checking` False so the initial progress reflects reality
|
|
166
|
+
(0% until the content is hardlinked) rather than a spurious 100%. qBit's
|
|
167
|
+
add endpoint does not return the infohash; callers diff `list()`
|
|
168
|
+
before/after.
|
|
169
|
+
"""
|
|
170
|
+
cli = await self._http()
|
|
171
|
+
data: dict[str, str] = {
|
|
172
|
+
"savepath": save_path,
|
|
173
|
+
"paused": "true" if paused else "false",
|
|
174
|
+
"skip_checking": "true" if skip_checking else "false",
|
|
175
|
+
"autoTMM": "false",
|
|
176
|
+
}
|
|
177
|
+
if category:
|
|
178
|
+
data["category"] = category
|
|
179
|
+
if tags:
|
|
180
|
+
data["tags"] = tags
|
|
181
|
+
files = {"torrents": ("reseed.torrent", torrent_bytes, "application/x-bittorrent")}
|
|
182
|
+
r = await cli.post("/api/v2/torrents/add", data=data, files=files)
|
|
183
|
+
r.raise_for_status()
|
|
184
|
+
if r.text.strip().lower() == "fails.":
|
|
185
|
+
raise RuntimeError("qBittorrent rejected the .torrent file")
|
|
186
|
+
|
|
187
|
+
async def torrent_files(self, torrent_hash: str) -> list[dict]:
|
|
188
|
+
"""Files qBit expects for a torrent: ``[{name, size, ...}]``.
|
|
189
|
+
|
|
190
|
+
``name`` is the path relative to the torrent root — the source of
|
|
191
|
+
truth for where to hardlink the local content under the save path.
|
|
192
|
+
"""
|
|
193
|
+
cli = await self._http()
|
|
194
|
+
r = await cli.get("/api/v2/torrents/files", params={"hash": torrent_hash})
|
|
195
|
+
r.raise_for_status()
|
|
196
|
+
data = r.json()
|
|
197
|
+
return data if isinstance(data, list) else []
|
|
198
|
+
|
|
199
|
+
async def info_one(self, torrent_hash: str) -> dict | None:
|
|
200
|
+
"""Raw qBit torrent info for a single hash (raw `state` + `progress`)."""
|
|
201
|
+
cli = await self._http()
|
|
202
|
+
r = await cli.get("/api/v2/torrents/info", params={"hashes": torrent_hash})
|
|
203
|
+
r.raise_for_status()
|
|
204
|
+
data = r.json()
|
|
205
|
+
if isinstance(data, list) and data:
|
|
206
|
+
return data[0]
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
async def recheck(self, torrent_hash: str) -> None:
|
|
210
|
+
cli = await self._http()
|
|
211
|
+
r = await cli.post("/api/v2/torrents/recheck", data={"hashes": torrent_hash})
|
|
212
|
+
r.raise_for_status()
|
|
213
|
+
|
|
214
|
+
async def resume(self, torrent_hash: str) -> None:
|
|
215
|
+
cli = await self._http()
|
|
216
|
+
r = await cli.post("/api/v2/torrents/resume", data={"hashes": torrent_hash})
|
|
217
|
+
r.raise_for_status()
|
|
218
|
+
|
|
219
|
+
async def pause(self, torrent_hash: str) -> None:
|
|
220
|
+
cli = await self._http()
|
|
221
|
+
r = await cli.post("/api/v2/torrents/pause", data={"hashes": torrent_hash})
|
|
222
|
+
r.raise_for_status()
|
|
223
|
+
|
|
123
224
|
async def remove(self, torrent_hash: str, delete_files: bool = False) -> None:
|
|
124
225
|
cli = await self._http()
|
|
125
226
|
r = await cli.post(
|