Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
95 changes: 95 additions & 0 deletions backend/src/module/downloader/client/tr_downloader.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions backend/src/module/downloader/download_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
25 changes: 22 additions & 3 deletions backend/src/module/models/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from os.path import expandvars
from typing import Literal
from typing import Literal, Union

from pydantic import BaseModel, Field

Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down