Ctrl K
ComunidadGuías…

app/qbcontrol.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Contrôle qBittorrent : /pause / /resume manuels pour Telegram.

Côté API uniquement : le conteneur telegram n'a ni la clé SSH ni la base —
il ne fait que relayer l'affichage (POST /api/qb/{verb}). L'exécution passe
par le canal d'actions verrouillé (SSH forced-command `vektor-actions` sur
le PVE, whitelist fermée, exécute déjà sur le CT 107 — le suffixe _103 du
nom est historique). Le registre OUI est partagé avec le chat naturel :
« mets les téléchargements en pause » → OUI suit exactement le même chemin.

Garde-fou : /resume prévient si une lecture Jellyfin est en cours —
l'action reste possible (c'est l'utilisateur qui décide), mais elle n'est
plus jamais silencieuse (leçon du 26/09 : resume all pendant un film).
"""
from __future__ import annotations

import os
import re

import httpx

from . import actions
from .tools import seeds_report

JF_TOKEN = os.environ.get("VEKTOR_JELLYFIN_TOKEN", "")
JF_URL = os.environ.get("VEKTOR_JELLYFIN_URL", "http://192.168.1.75:8096")


async def qb_state() -> tuple[int, int]:
    """(total, paused) via le rapport seeds existant (lecture seule)."""
    report = await seeds_report()
    m = re.search(r"(\d+)\s*torrents", report)
    total = int(m.group(1)) if m else 0
    p = re.search(r"(\d+)\s*en pause", report)
    paused = int(p.group(1)) if p else 0
    return total, paused


def _extract_playing(sessions) -> int | None:
    """Compte les lectures effectives ; None si l'entrée est inexploitable."""
    if not isinstance(sessions, list):
        return None
    playing = 0
    for session in sessions:
        play_state = session.get("PlayState") or {}
        if not session.get("NowPlayingItem") or not play_state.get("PlayMethod"):
            continue
        if play_state.get("IsPaused"):
            continue
        playing += 1
    return playing


async def jellyfin_playing_count() -> int | None:
    """Nombre de lectures Jellyfin actives ; None si le check échoue.

    Un échec n'est JAMAIS traité comme une absence de lecture (leçon du
    26/09) : il force l'avertissement prudent."""
    if not JF_TOKEN:
        return None
    try:
        async with httpx.AsyncClient(timeout=8) as client:
            response = await client.get(
                f"{JF_URL}/Sessions?activeWithinSeconds=120",
                headers={"Authorization": f'MediaBrowser Token="{JF_TOKEN}"'},
            )
    except httpx.HTTPError:
        return None
    if response.status_code != 200:
        return None
    try:
        sessions = response.json()
    except ValueError:
        return None
    return _extract_playing(sessions)


async def proposal_for(verb: str, user_key: str) -> str:
    """Texte de proposition (avertissement inclus) + enregistrement OUI.

    Utilise le registre d'actions partagé : un « OUI » envoyé ensuite dans
    la même conversation exécute qb_{verb}_103 — même chemin que les
    actions détectées dans le langage naturel."""
    warning = ""
    if verb == "resume":
        playing = await jellyfin_playing_count()
        if playing is None:
            warning = (
                "⚠️ Impossible de vérifier si une lecture Jellyfin est en cours "
                "(check en échec). Vérifie avant de confirmer.\n\n"
            )
        elif playing > 0:
            warning = (
                f"⚠️ {playing} lecture(s) Jellyfin détectée(s) : reprendre les "
                "torrents va leur disputer la bande passante.\n\n"
            )
    human = {
        "pause": "pause de TOUS les téléchargements qBittorrent",
        "resume": "reprise de TOUS les téléchargements qBittorrent",
    }[verb]
    actions._PENDING[user_key] = (f"qb_{verb}_103", actions.time.monotonic())
    return warning + (
        f"⚠️ Action d'écriture demandée : {human}.\n"
        "Confirme en répondant exactement **OUI** (expire dans 2 minutes)."
    )


async def execute(verb: str) -> str:
    """pause | resume via le canal d'actions verrouillé. Retour lisible."""
    action = f"qb_{verb}_103"
    result = await actions._execute(action)
    total, paused = await qb_state()
    if verb == "pause":
        return f"{result}\n\nÉtat qBit : {paused}/{total} en pause."
    return f"{result}\n\nÉtat qBit : {total - paused}/{total} actifs."