diff --git a/backend/requirements.txt b/backend/requirements.txt index 02ed21578..92cb06a70 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -27,3 +27,4 @@ sqlmodel==0.0.8 sse-starlette==1.6.5 semver==3.0.1 openai==0.28.1 +transmission-rpc==7.0.3 \ No newline at end of file diff --git a/backend/src/module/downloader/client/tr_downloader.py b/backend/src/module/downloader/client/tr_downloader.py index e69de29bb..457634e39 100644 --- a/backend/src/module/downloader/client/tr_downloader.py +++ b/backend/src/module/downloader/client/tr_downloader.py @@ -0,0 +1,95 @@ +import logging +import time +import re + +from transmission_rpc import Client, TransmissionError +from transmission_rpc.error import ( + TransmissionConnectError, + TransmissionAuthError, +) + +logger = logging.getLogger(__name__) + + +class TrDownloader: + def __init__(self, host: str, username: str, password: str, ssl: bool): + host, port = self.parse_host(host) + self._client = self.connect(host, port, username, password) + self.host = host + self.username = username + self.connect(host, port, username, password) + + def parse_host(self, host_str: str): + regex = re.compile(r'(?:(?:http|https):\/\/)?([\w\d\.-]+:[\d]+)') + host_str = regex.search(host_str).group(1) + host, port = host_str.split(":") + + try: + return host, int(port) + except ValueError: + logger.warning("Cannot parse port, use default port 9091") + return host_str, 9091 + except Exception as e: + logger.error(f"Unknown error: {e}, use default port 9091") + return host_str, 9091 + + def auth(self): + try: + self._client.get_session() + return True + except TransmissionError: + return False + + def logout(self): + pass + + def connect(self, host: str, port: int, username: str, password: str, retry=3): + times = 0 + while times < retry: + try: + return Client(host=host, port=port, username=username, password=password) + except TransmissionAuthError: + logger.error( + f"Can't login Transmission Server {self.host} by {self.username}, retry in {5} seconds." + ) + time.sleep(5) + times += 1 + except TransmissionConnectError: + logger.error("Cannot connect to TransmissionServer") + logger.info("Please check the IP and port in settings") + time.sleep(10) + times += 1 + except Exception as e: + logger.error(f"Unknown error: {e}") + break + raise Exception("Cannot connect to TransmissionServer") + + def add_torrent(self, torrent: str, download_dir: str, labels: str): + res = self._client.add_torrent(torrent=torrent, download_dir=download_dir, labels=labels, paused=False) + return res.error == 0 + + def add_torrents(self, torrent_urls, torrent_files, save_path, category): + res = True + for torrent in torrent_urls: + res = res and self.add_torrent(torrent, save_path, labels=category) + + for torrent in torrent_files: + res = res and self.add_torrent(torrent, save_path, labels=category) + return res + + def torrents_delete(self, hashes): + return self._client.remove_torrent(hashes, delete_data=True) + + def torrents_rename_file(self, torrent_hash, old_path, new_path) -> bool: + # old path just use to be compatible with download_client.py + torrent = self._client.get_torrent(torrent_hash) + + try: + self._client.rename_torrent_path(torrent_hash, location=new_path, name=torrent.name) + return True + except TransmissionError: + logger.debug(f"Error: {torrent.download_dir} >> {new_path}") + return False + + def move_torrent(self, hashes, new_location): + self._client.move_torrent_data(hashes, new_location) diff --git a/backend/src/module/downloader/download_client.py b/backend/src/module/downloader/download_client.py index d01d4fa3c..6b918c593 100644 --- a/backend/src/module/downloader/download_client.py +++ b/backend/src/module/downloader/download_client.py @@ -27,6 +27,10 @@ def __getClient(): from .client.qb_downloader import QbDownloader return QbDownloader(host, username, password, ssl) + if type == "transmission": + from .client.tr_downloader import TrDownloader + + return TrDownloader(host, username, password, ssl) else: logger.error(f"[Downloader] Unsupported downloader type: {type}") raise Exception(f"Unsupported downloader type: {type}") diff --git a/backend/src/module/models/config.py b/backend/src/module/models/config.py index 49044fa3d..111ab7eca 100644 --- a/backend/src/module/models/config.py +++ b/backend/src/module/models/config.py @@ -1,5 +1,5 @@ from os.path import expandvars -from typing import Literal +from typing import Literal, Union from pydantic import BaseModel, Field @@ -10,7 +10,15 @@ class Program(BaseModel): webui_port: int = Field(7892, description="WebUI port") -class Downloader(BaseModel): +class BaseDownloader(BaseModel): + host_: str + username_: str + password_: str + path: str + ssl: bool + + +class QbDownloader(BaseDownloader): type: str = Field("qbittorrent", description="Downloader type") host_: str = Field("172.17.0.1:8080", alias="host", description="Downloader host") username_: str = Field("admin", alias="username", description="Downloader username") @@ -33,6 +41,17 @@ def password(self): return expandvars(self.password_) +class TrDownloader(BaseDownloader): + type: str = Field("transmission", description="Downloader type") + host_: str = Field("172.17.0.1:9091", alias="host", description="Downloader host") + username_: str = Field("admin", alias="username", description="Downloader username") + password_: str = Field( + "admin", alias="password", description="Downloader password" + ) + path: str = Field("/downloads/Bangumi", description="Downloader path") + ssl: bool = Field(False, description="Downloader ssl") # USELESS for transmission + + class RSSParser(BaseModel): enable: bool = Field(True, description="Enable RSS parser") filter: list[str] = Field(["720", r"\d+-\d"], description="Filter") @@ -105,7 +124,7 @@ class ExperimentalOpenAI(BaseModel): class Config(BaseModel): program: Program = Program() - downloader: Downloader = Downloader() + downloader: Union[QbDownloader, TrDownloader] = QbDownloader() rss_parser: RSSParser = RSSParser() bangumi_manage: BangumiManage = BangumiManage() log: Log = Log()