diff --git a/.env.example b/.env.example index 7090dcf..1d078d8 100644 --- a/.env.example +++ b/.env.example @@ -41,12 +41,19 @@ MONITORINK_CODEX_TOKEN_FILE=/hermes/auth.json # Liste des clés actives, puis UN bloc par tracker. v1 : type "unit3d_nuxt" (ex. c411). # Le ratio n'est PAS lisible au token API -> login requis (username/password du compte). # Laisser MONITORINK_TRACKERS vide pour masquer la section. -#MONITORINK_TRACKERS=c411 +#MONITORINK_TRACKERS=c411,torr9 #MONITORINK_TRACKER_C411_LABEL=c411 #MONITORINK_TRACKER_C411_TYPE=unit3d_nuxt #MONITORINK_TRACKER_C411_URL=https://c411.org #MONITORINK_TRACKER_C411_USER=TonUsername #MONITORINK_TRACKER_C411_PASS=TonMotDePasse +# torr9 : type "torr9" (API JWT dédiée). URL = sous-domaine API. Le passkey ne sert +# qu'au RSS, pas au ratio -> identifiant/mot de passe requis. +#MONITORINK_TRACKER_TORR9_LABEL=torr9 +#MONITORINK_TRACKER_TORR9_TYPE=torr9 +#MONITORINK_TRACKER_TORR9_URL=https://api.torr9.net +#MONITORINK_TRACKER_TORR9_USER=TonUsername +#MONITORINK_TRACKER_TORR9_PASS=TonMotDePasse #MONITORINK_TRACKER_TTL=1800 # Home Assistant (optionnel) — laisser vide pour désactiver diff --git a/backend/integrations/trackers.py b/backend/integrations/trackers.py index 0a7d559..85a5f4e 100644 --- a/backend/integrations/trackers.py +++ b/backend/integrations/trackers.py @@ -119,7 +119,45 @@ async def _fetch_unit3d(spec: TrackerSpec) -> TrackerStat: ) -_FETCHERS = {"unit3d_nuxt": _fetch_unit3d} +async def _fetch_torr9(spec: TrackerSpec) -> TrackerStat: + """torr9 : API Go dédiée (`api.torr9.net`). Login JWT puis `/api/v1/users/me`. + Le profil n'a pas de champ `ratio` ; on le calcule comme le frontend : + (total_uploaded_bytes + bonus_uploaded) / (total_downloaded_bytes + bonus_downloaded). + NB : le passkey du compte ne sert qu'aux flux RSS, pas à ce profil.""" + if not (spec.base_url and spec.username and spec.password): + return TrackerStat(spec.key, spec.label, ok=False, error="non configuré") + + async with httpx.AsyncClient( + timeout=20, follow_redirects=True, headers={"User-Agent": _UA}, + ) as client: + r = await client.post( + f"{spec.base_url}/api/v1/auth/login", + json={"username": spec.username, "password": spec.password}, + ) + try: + d = r.json() + except ValueError: + raise _AuthError(f"login HTTP {r.status_code}") + token = d.get("token") + if not token: + raise _AuthError(str(d.get("error") or d.get("message") or "login refusé")) + me = await client.get( + f"{spec.base_url}/api/v1/users/me", + headers={"Authorization": f"Bearer {token}"}, + ) + if me.status_code != 200: + raise _AuthError(f"users/me HTTP {me.status_code}") + u = me.json() + + up = int(u.get("total_uploaded_bytes", 0) or 0) + int(u.get("bonus_uploaded", 0) or 0) + down = int(u.get("total_downloaded_bytes", 0) or 0) + int(u.get("bonus_downloaded", 0) or 0) + return TrackerStat( + spec.key, spec.label, ok=True, + ratio=(up / down if down else 0.0), up_bytes=up, down_bytes=down, + ) + + +_FETCHERS = {"unit3d_nuxt": _fetch_unit3d, "torr9": _fetch_torr9} async def _fetch_one(spec: TrackerSpec) -> TrackerStat: