You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/core/torrent_clients/base.py

111 lines
4.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""Torrent client adapter contract.
``TorrentClientAdapter`` is a structural Protocol — any class with
these method signatures is treated as a valid adapter. The download
plugin layer (built in a later commit) dispatches generically against
this surface so it doesn't have to know whether the user picked
qBittorrent, Transmission, or Deluge.
The contract intentionally hides protocol-specific details:
- qBittorrent uses cookie auth + multipart form uploads.
- Transmission uses an X-Transmission-Session-Id header + JSON RPC.
- Deluge 2.x uses /json with a session cookie.
All three converge on the same eight verbs below.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Protocol, runtime_checkable
@dataclass
class TorrentStatus:
"""Adapter-uniform view of one torrent's live state.
Field semantics:
- ``state`` is one of: ``queued`` | ``downloading`` | ``seeding`` |
``paused`` | ``stalled`` | ``error`` | ``completed``. Each
adapter maps its native state names to this set.
- ``progress`` is 0.01.0.
- ``save_path`` is where files land on the torrent client's host.
For remote clients this is a path on the *remote* machine.
- ``files`` is the list of relative paths inside the torrent. Empty
until the client has finished fetching the metadata.
"""
id: str # torrent hash (qBit, Deluge) or numeric id (Transmission)
name: str
state: str
progress: float
size: int # total size in bytes
downloaded: int # bytes downloaded so far
download_speed: int # bytes/sec
upload_speed: int # bytes/sec
seeders: int = 0
peers: int = 0
eta: Optional[int] = None # seconds, None if unknown
save_path: Optional[str] = None
files: Optional[List[str]] = None
error: Optional[str] = None
@runtime_checkable
class TorrentClientAdapter(Protocol):
"""Structural contract every torrent-client adapter implements."""
def is_configured(self) -> bool:
"""True when the adapter has a URL and any credentials it
needs. Reads from config_manager — never raises on missing
config, just returns False so the orchestrator can dim the
torrent download source in the UI."""
...
async def check_connection(self) -> bool:
"""Probe the client over the network. Logs in if required."""
...
async def add_torrent(
self,
url_or_magnet: str,
category: str = "soulsync",
save_path: Optional[str] = None,
) -> Optional[str]:
"""Hand the torrent client a HTTP/HTTPS URL pointing to a
``.torrent`` file or a ``magnet:`` URI. Returns the torrent's
client-side identifier (info-hash for qBit / Deluge, numeric
id for Transmission) or ``None`` on failure."""
...
async def add_torrent_file(
self,
file_bytes: bytes,
category: str = "soulsync",
save_path: Optional[str] = None,
) -> Optional[str]:
"""Upload a raw ``.torrent`` payload. Same return as
``add_torrent``. Used when the indexer doesn't expose a
direct download URL and SoulSync had to fetch the file
itself first."""
...
async def get_status(self, torrent_id: str) -> Optional[TorrentStatus]:
"""Return live status for one torrent, or ``None`` if the
client doesn't know about it."""
...
async def get_all(self) -> List[TorrentStatus]:
"""Return live status for every torrent the client currently
tracks. Used by the global download list."""
...
async def remove(self, torrent_id: str, delete_files: bool = False) -> bool:
"""Remove the torrent from the client. ``delete_files=True``
also deletes the downloaded data on disk."""
...
async def pause(self, torrent_id: str) -> bool: ...
async def resume(self, torrent_id: str) -> bool: ...