fix downloaders

This commit is contained in:
jxxghp
2024-07-02 11:00:55 +08:00
parent dfc5872087
commit c8fe6e4284
19 changed files with 311 additions and 140 deletions
+4 -4
View File
@@ -9,14 +9,14 @@ class DashboardChain(ChainBase, metaclass=Singleton):
""" """
各类仪表板统计处理链 各类仪表板统计处理链
""" """
def media_statistic(self) -> Optional[List[schemas.Statistic]]: def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
""" """
媒体数量统计 媒体数量统计
""" """
return self.run_module("media_statistic") return self.run_module("media_statistic", server=server)
def downloader_info(self) -> Optional[List[schemas.DownloaderInfo]]: def downloader_info(self, downloader: str = None) -> Optional[List[schemas.DownloaderInfo]]:
""" """
下载器信息 下载器信息
""" """
return self.run_module("downloader_info") return self.run_module("downloader_info", downloader=downloader)
+8 -5
View File
@@ -1,4 +1,7 @@
from typing import List
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.schemas.system import DownloaderConf
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
@@ -10,11 +13,11 @@ class DownloaderHelper:
def __init__(self): def __init__(self):
self.systemconfig = SystemConfigOper() self.systemconfig = SystemConfigOper()
def get_downloaders(self) -> dict: def get_downloaders(self) -> List[DownloaderConf]:
""" """
获取下载器 获取下载器
""" """
downloader_conf: dict = self.systemconfig.get(SystemConfigKey.Downloaders) downloader_confs: List[dict] = self.systemconfig.get(SystemConfigKey.Downloaders)
if not downloader_conf: if not downloader_confs:
return {} return []
return downloader_conf return [DownloaderConf(**conf) for conf in downloader_confs]
+12 -5
View File
@@ -21,7 +21,6 @@ class EmbyModule(_ModuleBase):
mediaservers = MediaServerHelper().get_mediaservers() mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers: if not mediaservers:
return return
# 读取Emby配置
for server in mediaservers: for server in mediaservers:
if server.type == "emby": if server.type == "emby":
self._servers[server.name] = Emby(**server.config) self._servers[server.name] = Emby(**server.config)
@@ -142,16 +141,24 @@ class EmbyModule(_ModuleBase):
) )
return None return None
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
""" """
媒体数量统计 媒体数量统计
""" """
if server:
server_obj = self.get_server(server)
if not server_obj:
return None
servers = [server_obj]
else:
servers = self._servers.values()
media_statistics = [] media_statistics = []
for server in self._servers.values(): for server in servers:
media_statistic = server.get_medias_count() media_statistic = server.get_medias_count()
if not media_statistics:
continue
media_statistic.user_count = server.get_user_count() media_statistic.user_count = server.get_user_count()
if media_statistic: media_statistics.append(media_statistic)
media_statistics.append(media_statistic)
return media_statistics return media_statistics
def mediaserver_librarys(self, server: str, def mediaserver_librarys(self, server: str,
+4 -1
View File
@@ -15,7 +15,10 @@ from app.utils.http import RequestUtils
class Emby: class Emby:
def __init__(self, host: str = None, play_host: str = None, apikey: str = None): def __init__(self, host: str, apikey: str, play_host: str = None, **kwargs):
if not host or not apikey:
logger.error("Emby服务器配置不完整!")
return
self._host = host self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
+12 -5
View File
@@ -21,7 +21,6 @@ class JellyfinModule(_ModuleBase):
mediaservers = MediaServerHelper().get_mediaservers() mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers: if not mediaservers:
return return
# 读取Jelly配置
for server in mediaservers: for server in mediaservers:
if server.type == "jellyfin": if server.type == "jellyfin":
self._servers[server.name] = Jellyfin(**server.config) self._servers[server.name] = Jellyfin(**server.config)
@@ -140,16 +139,24 @@ class JellyfinModule(_ModuleBase):
) )
return None return None
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
""" """
媒体数量统计 媒体数量统计
""" """
if server:
server_obj = self.get_server(server)
if not server_obj:
return None
servers = [server_obj]
else:
servers = self._servers.values()
media_statistics = [] media_statistics = []
for server in self._servers.values(): for server in servers:
media_statistic = server.get_medias_count() media_statistic = server.get_medias_count()
if not media_statistics:
continue
media_statistic.user_count = server.get_user_count() media_statistic.user_count = server.get_user_count()
if media_statistic: media_statistics.append(media_statistic)
media_statistics.append(media_statistic)
return media_statistics return media_statistics
def mediaserver_librarys(self, server: str = None, def mediaserver_librarys(self, server: str = None,
+4 -1
View File
@@ -12,7 +12,10 @@ from app.utils.http import RequestUtils
class Jellyfin: class Jellyfin:
def __init__(self, host: str = None, play_host: str = None, apikey: str = None): def __init__(self, host: str, apikey: str, play_host: str = None, **kwargs):
if not host or not apikey:
logger.error("Jellyfin服务器配置不完整!!")
return
self._host = host self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
+12 -6
View File
@@ -21,7 +21,6 @@ class PlexModule(_ModuleBase):
mediaservers = MediaServerHelper().get_mediaservers() mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers: if not mediaservers:
return return
# 读取Emby配置
for server in mediaservers: for server in mediaservers:
if server.type == "plex": if server.type == "plex":
self._servers[server.name] = Plex(**server.config) self._servers[server.name] = Plex(**server.config)
@@ -130,16 +129,23 @@ class PlexModule(_ModuleBase):
) )
return None return None
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
""" """
媒体数量统计 媒体数量统计
""" """
if server:
server_obj = self.get_server(server)
if not server_obj:
return None
servers = [server_obj]
else:
servers = self._servers.values()
media_statistics = [] media_statistics = []
for server in self._servers.values(): for server in servers:
media_statistic = server.get_medias_count() media_statistic = server.get_medias_count()
media_statistic.user_count = 1 if not media_statistics:
if media_statistic: continue
media_statistics.append(media_statistic) media_statistics.append(media_statistic)
return media_statistics return media_statistics
def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]: def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]:
+4 -1
View File
@@ -19,7 +19,10 @@ class Plex:
_plex = None _plex = None
_session = None _session = None
def __init__(self, host: str = None, play_host: str = None, token: str = None): def __init__(self, host: str, token: str, play_host: str = None, **kwargs):
if not host or not token:
logger.error("Plex服务器配置不完整!")
return
self._host = host self._host = host
if self._host: if self._host:
self._host = RequestUtils.standardize_base_url(self._host) self._host = RequestUtils.standardize_base_url(self._host)
+96 -52
View File
@@ -1,6 +1,6 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Set, Tuple, Optional, Union, List from typing import Set, Tuple, Optional, Union, List, Dict
from qbittorrentapi import TorrentFilesList from qbittorrentapi import TorrentFilesList
from torrentool.torrent import Torrent from torrentool.torrent import Torrent
@@ -8,6 +8,7 @@ from torrentool.torrent import Torrent
from app import schemas from app import schemas
from app.core.config import settings from app.core.config import settings
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.helper.downloader import DownloaderHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent from app.modules.qbittorrent.qbittorrent import Qbittorrent
@@ -18,15 +19,36 @@ from app.utils.system import SystemUtils
class QbittorrentModule(_ModuleBase): class QbittorrentModule(_ModuleBase):
qbittorrent: Qbittorrent = None _servers: Dict[str, Qbittorrent] = {}
_default_server: Qbittorrent = None
def init_module(self) -> None: def init_module(self) -> None:
self.qbittorrent = Qbittorrent() """
初始化模块
"""
# 读取下载器配置
self._servers = {}
downloaders = DownloaderHelper().get_downloaders()
if not downloaders:
return
for server in downloaders:
if server.type == "qbittorrent":
self._servers[server.name] = Qbittorrent(**server.config)
if server.default:
self._default_server = self._servers[server.name]
@staticmethod @staticmethod
def get_name() -> str: def get_name() -> str:
return "Qbittorrent" return "Qbittorrent"
def get_server(self, name: str = None) -> Optional[Qbittorrent]:
"""
获取服务器,name为空则返回默认服务器
"""
if name:
return self._servers.get(name)
return self._default_server
def stop(self): def stop(self):
pass pass
@@ -34,26 +56,28 @@ class QbittorrentModule(_ModuleBase):
""" """
测试模块连接性 测试模块连接性
""" """
if self.qbittorrent.is_inactive(): for name, server in self._servers.items():
self.qbittorrent.reconnect() if server.is_inactive():
if not self.qbittorrent.transfer_info(): server.reconnect()
return False, "无法获取Qbittorrent状态,请检查参数配置" if not server.transfer_info():
return False, f"无法连接Qbittorrent下载器:{name}"
return True, "" return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]: def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "DOWNLOADER", "qbittorrent" pass
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务,每10分钟调用一次 定时任务,每10分钟调用一次
""" """
# 定时重连 for name, server in self._servers.items():
if self.qbittorrent.is_inactive(): if server.is_inactive():
self.qbittorrent.reconnect() logger.info(f"Qbittorrent下载器 {name} 连接断开,尝试重连 ...")
server.reconnect()
def download(self, content: Union[Path, str], download_dir: Path, cookie: str, def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
episodes: Set[int] = None, category: str = None, episodes: Set[int] = None, category: str = None,
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[Tuple[Optional[str], str]]: downloader: str = None) -> Optional[Tuple[Optional[str], str]]:
""" """
根据种子文件,选择并添加下载任务 根据种子文件,选择并添加下载任务
:param content: 种子文件地址或者磁力链接 :param content: 种子文件地址或者磁力链接
@@ -79,15 +103,17 @@ class QbittorrentModule(_ModuleBase):
logger.error(f"获取种子名称失败:{e}") logger.error(f"获取种子名称失败:{e}")
return "", 0 return "", 0
# 不是默认下载器不处理
if downloader != "qbittorrent":
return None
if not content: if not content:
return None return None, "下载内容为空"
if isinstance(content, Path) and not content.exists(): if isinstance(content, Path) and not content.exists():
logger.error(f"种子文件不存在:{content}")
return None, f"种子文件不存在:{content}" return None, f"种子文件不存在:{content}"
# 获取下载器
server = self.get_server(downloader)
if not server:
return None
# 生成随机Tag # 生成随机Tag
tag = StringUtils.generate_random_str(10) tag = StringUtils.generate_random_str(10)
if settings.TORRENT_TAG: if settings.TORRENT_TAG:
@@ -97,7 +123,7 @@ class QbittorrentModule(_ModuleBase):
# 如果要选择文件则先暂停 # 如果要选择文件则先暂停
is_paused = True if episodes else False is_paused = True if episodes else False
# 添加任务 # 添加任务
state = self.qbittorrent.add_torrent( state = server.add_torrent(
content=content.read_bytes() if isinstance(content, Path) else content, content=content.read_bytes() if isinstance(content, Path) else content,
download_dir=str(download_dir), download_dir=str(download_dir),
is_paused=is_paused, is_paused=is_paused,
@@ -111,7 +137,7 @@ class QbittorrentModule(_ModuleBase):
if not torrent_name: if not torrent_name:
return None, f"添加种子任务失败:无法读取种子文件" return None, f"添加种子任务失败:无法读取种子文件"
# 查询所有下载器的种子 # 查询所有下载器的种子
torrents, error = self.qbittorrent.get_torrents() torrents, error = server.get_torrents()
if error: if error:
return None, "无法连接qbittorrent下载器" return None, "无法连接qbittorrent下载器"
if torrents: if torrents:
@@ -123,21 +149,21 @@ class QbittorrentModule(_ModuleBase):
logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.get('name')}") logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.get('name')}")
# 给种子打上标签 # 给种子打上标签
if "已整理" in torrent_tags: if "已整理" in torrent_tags:
self.qbittorrent.remove_torrents_tag(ids=torrent_hash, tag=['已整理']) server.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
if settings.TORRENT_TAG and settings.TORRENT_TAG not in torrent_tags: if settings.TORRENT_TAG and settings.TORRENT_TAG not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}") logger.info(f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}")
self.qbittorrent.set_torrents_tag(ids=torrent_hash, tags=[settings.TORRENT_TAG]) server.set_torrents_tag(ids=torrent_hash, tags=[settings.TORRENT_TAG])
return torrent_hash, f"下载任务已存在" return torrent_hash, f"下载任务已存在"
return None, f"添加种子任务失败:{content}" return None, f"添加种子任务失败:{content}"
else: else:
# 获取种子Hash # 获取种子Hash
torrent_hash = self.qbittorrent.get_torrent_id_by_tag(tags=tag) torrent_hash = server.get_torrent_id_by_tag(tags=tag)
if not torrent_hash: if not torrent_hash:
return None, f"下载任务添加成功,但获取Qbittorrent任务信息失败:{content}" return None, f"下载任务添加成功,但获取Qbittorrent任务信息失败:{content}"
else: else:
if is_paused: if is_paused:
# 种子文件 # 种子文件
torrent_files = self.qbittorrent.get_files(torrent_hash) torrent_files = server.get_files(torrent_hash)
if not torrent_files: if not torrent_files:
return torrent_hash, "获取种子文件失败,下载任务可能在暂停状态" return torrent_hash, "获取种子文件失败,下载任务可能在暂停状态"
@@ -157,17 +183,17 @@ class QbittorrentModule(_ModuleBase):
sucess_epidised = list(set(sucess_epidised).union(set(meta_info.episode_list))) sucess_epidised = list(set(sucess_epidised).union(set(meta_info.episode_list)))
if sucess_epidised and file_ids: if sucess_epidised and file_ids:
# 选择文件 # 选择文件
self.qbittorrent.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0) server.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
# 开始任务 # 开始任务
if settings.QB_FORCE_RESUME: if settings.QB_FORCE_RESUME:
# 强制继续 # 强制继续
self.qbittorrent.torrents_set_force_start(torrent_hash) server.torrents_set_force_start(torrent_hash)
else: else:
self.qbittorrent.start_torrents(torrent_hash) server.start_torrents(torrent_hash)
return torrent_hash, f"添加下载成功,已选择集数:{sucess_epidised}" return torrent_hash, f"添加下载成功,已选择集数:{sucess_epidised}"
else: else:
if settings.QB_FORCE_RESUME: if settings.QB_FORCE_RESUME:
self.qbittorrent.torrents_set_force_start(torrent_hash) server.torrents_set_force_start(torrent_hash)
return torrent_hash, "添加下载成功" return torrent_hash, "添加下载成功"
def list_torrents(self, status: TorrentStatus = None, def list_torrents(self, status: TorrentStatus = None,
@@ -181,12 +207,15 @@ class QbittorrentModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: 下载器中符合状态的种子列表 :return: 下载器中符合状态的种子列表
""" """
if downloader != "qbittorrent": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
ret_torrents = [] ret_torrents = []
if hashs: if hashs:
# 按Hash获取 # 按Hash获取
torrents, _ = self.qbittorrent.get_torrents(ids=hashs, tags=settings.TORRENT_TAG) torrents, _ = server.get_torrents(ids=hashs, tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
content_path = torrent.get("content_path") content_path = torrent.get("content_path")
if content_path: if content_path:
@@ -202,7 +231,7 @@ class QbittorrentModule(_ModuleBase):
)) ))
elif status == TorrentStatus.TRANSFER: elif status == TorrentStatus.TRANSFER:
# 获取已完成且未整理的 # 获取已完成且未整理的
torrents = self.qbittorrent.get_completed_torrents(tags=settings.TORRENT_TAG) torrents = server.get_completed_torrents(tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
tags = torrent.get("tags") or [] tags = torrent.get("tags") or []
if "已整理" in tags: if "已整理" in tags:
@@ -221,7 +250,7 @@ class QbittorrentModule(_ModuleBase):
)) ))
elif status == TorrentStatus.DOWNLOADING: elif status == TorrentStatus.DOWNLOADING:
# 获取正在下载的任务 # 获取正在下载的任务
torrents = self.qbittorrent.get_downloading_torrents(tags=settings.TORRENT_TAG) torrents = server.get_downloading_torrents(tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
meta = MetaInfo(torrent.get('name')) meta = MetaInfo(torrent.get('name'))
ret_torrents.append(DownloadingTorrent( ret_torrents.append(DownloadingTorrent(
@@ -251,9 +280,10 @@ class QbittorrentModule(_ModuleBase):
:param path: 源目录 :param path: 源目录
:param downloader: 下载器 :param downloader: 下载器
""" """
if downloader != "qbittorrent": server = self.get_server(downloader)
return if not server:
self.qbittorrent.set_torrents_tag(ids=hashs, tags=['已整理']) return None
server.set_torrents_tag(ids=hashs, tags=['已整理'])
# 移动模式删除种子 # 移动模式删除种子
if settings.TRANSFER_TYPE in ["move", "rclone_move"]: if settings.TRANSFER_TYPE in ["move", "rclone_move"]:
if self.remove_torrents(hashs): if self.remove_torrents(hashs):
@@ -274,9 +304,10 @@ class QbittorrentModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "qbittorrent": server = self.get_server(downloader)
if not server:
return None return None
return self.qbittorrent.delete_torrents(delete_file=delete_file, ids=hashs) return server.delete_torrents(delete_file=delete_file, ids=hashs)
def start_torrents(self, hashs: Union[list, str], def start_torrents(self, hashs: Union[list, str],
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]: downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
@@ -286,9 +317,10 @@ class QbittorrentModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "qbittorrent": server = self.get_server(downloader)
if not server:
return None return None
return self.qbittorrent.start_torrents(ids=hashs) return server.start_torrents(ids=hashs)
def stop_torrents(self, hashs: Union[list, str], downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]: def stop_torrents(self, hashs: Union[list, str], downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
""" """
@@ -297,29 +329,41 @@ class QbittorrentModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "qbittorrent": server = self.get_server(downloader)
if not server:
return None return None
return self.qbittorrent.stop_torrents(ids=hashs) return server.stop_torrents(ids=hashs)
def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[TorrentFilesList]: def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[TorrentFilesList]:
""" """
获取种子文件列表 获取种子文件列表
""" """
if downloader != "qbittorrent": server = self.get_server(downloader)
if not server:
return None return None
return self.qbittorrent.get_files(tid=tid) return server.get_files(tid=tid)
def downloader_info(self) -> [schemas.DownloaderInfo]: def downloader_info(self, downloader: str = None) -> Optional[List[schemas.DownloaderInfo]]:
""" """
下载器信息 下载器信息
""" """
if downloader:
server = self.get_server(downloader)
if not server:
return None
servers = [server]
else:
servers = self._servers.values()
# 调用Qbittorrent API查询实时信息 # 调用Qbittorrent API查询实时信息
info = self.qbittorrent.transfer_info() ret_info = []
if not info: for server in servers:
return [schemas.DownloaderInfo()] info = server.transfer_info()
return [schemas.DownloaderInfo( if not info:
download_speed=info.get("dl_info_speed"), continue
upload_speed=info.get("up_info_speed"), ret_info.append(schemas.DownloaderInfo(
download_size=info.get("dl_info_data"), download_speed=info.get("dl_info_speed"),
upload_size=info.get("up_info_data") upload_speed=info.get("up_info_speed"),
)] download_size=info.get("dl_info_data"),
upload_size=info.get("up_info_data")
))
return ret_info
+1 -1
View File
@@ -18,7 +18,7 @@ class Qbittorrent:
qbc: Client = None qbc: Client = None
def __init__(self, host: str = None, port: int = None, username: str = None, password: str = None): def __init__(self, host: str = None, port: int = None, username: str = None, password: str = None, **kwargs):
""" """
若不设置参数,则创建配置文件设置的下载器 若不设置参数,则创建配置文件设置的下载器
""" """
+5 -1
View File
@@ -24,7 +24,11 @@ class Slack:
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}" _ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}"
_channel = "" _channel = ""
def __init__(self, oauth_token: str, app_token: str, channel: str = ""): def __init__(self, oauth_token: str, app_token: str, channel: str = "", **kwargs):
if not oauth_token or not app_token:
logger.error("Slack 配置不完整!")
return
try: try:
slack_app = App(token=oauth_token, slack_app = App(token=oauth_token,
+7 -5
View File
@@ -1,10 +1,9 @@
import json import json
import re import re
from threading import Lock
from typing import Optional, List from typing import Optional, List
from urllib.parse import quote from urllib.parse import quote
from threading import Lock
from app.core.config import settings
from app.core.context import MediaInfo, Context from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
@@ -15,10 +14,13 @@ lock = Lock()
class SynologyChat: class SynologyChat:
def __init__(self): def __init__(self, webhook: str, token: str, **kwargs):
if not webhook or not token:
logger.error("SynologyChat配置不完整!")
return
self._req = RequestUtils(content_type="application/x-www-form-urlencoded") self._req = RequestUtils(content_type="application/x-www-form-urlencoded")
self._webhook_url = settings.SYNOLOGYCHAT_WEBHOOK self._webhook_url = webhook
self._token = settings.SYNOLOGYCHAT_TOKEN self._token = token
if self._webhook_url: if self._webhook_url:
self._domain = StringUtils.get_base_url(self._webhook_url) self._domain = StringUtils.get_base_url(self._webhook_url)
+4 -1
View File
@@ -24,10 +24,13 @@ class Telegram:
_event = Event() _event = Event()
_bot: telebot.TeleBot = None _bot: telebot.TeleBot = None
def __init__(self, token: str = None, chat_id: str = None): def __init__(self, token: str, chat_id: str, **kwargs):
""" """
初始化参数 初始化参数
""" """
if not token or not chat_id:
logger.error("Telegram配置不完整!")
return
# Token # Token
self._telegram_token = token self._telegram_token = token
# Chat Id # Chat Id
+96 -48
View File
@@ -1,6 +1,6 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import Set, Tuple, Optional, Union, List from typing import Set, Tuple, Optional, Union, List, Dict
from torrentool.torrent import Torrent from torrentool.torrent import Torrent
from transmission_rpc import File from transmission_rpc import File
@@ -8,6 +8,7 @@ from transmission_rpc import File
from app import schemas from app import schemas
from app.core.config import settings from app.core.config import settings
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.helper.downloader import DownloaderHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.transmission.transmission import Transmission from app.modules.transmission.transmission import Transmission
@@ -18,14 +19,32 @@ from app.utils.system import SystemUtils
class TransmissionModule(_ModuleBase): class TransmissionModule(_ModuleBase):
transmission: Transmission = None _servers: Dict[str, Transmission] = {}
_default_server: Transmission = None
def init_module(self) -> None: def init_module(self) -> None:
self.transmission = Transmission() # 读取下载器配置
self._servers = {}
downloaders = DownloaderHelper().get_downloaders()
if not downloaders:
return
for server in downloaders:
if server.type == "transmission":
self._servers[server.name] = Transmission(**server.config)
if server.default:
self._default_server = self._servers[server.name]
@staticmethod @staticmethod
def get_name() -> str: def get_name() -> str:
return "Transmission" return "Transmission"
def get_server(self, name: str = None) -> Optional[Transmission]:
"""
获取服务器,name为空则返回默认服务器
"""
if name:
return self._servers.get(name)
return self._default_server
def stop(self): def stop(self):
pass pass
@@ -34,22 +53,27 @@ class TransmissionModule(_ModuleBase):
""" """
测试模块连接性 测试模块连接性
""" """
if self.transmission.is_inactive(): if not self._servers:
self.transmission.reconnect() return False, "未配置Transmission下载器"
if not self.transmission.transfer_info(): for name, server in self._servers.items():
return False, "无法获取Transmission状态,请检查参数配置" if server.is_inactive():
server.reconnect()
if not server.transfer_info():
return False, f"无法连接Transmission下载器:{name}"
return True, "" return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]: def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "DOWNLOADER", "transmission" pass
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务,每10分钟调用一次 定时任务,每10分钟调用一次
""" """
# 定时重连 # 定时重连
if self.transmission.is_inactive(): for name, server in self._servers.items():
self.transmission.reconnect() if server.is_inactive():
logger.info(f"Transmission下载器 {name} 连接断开,尝试重连 ...")
server.reconnect()
def download(self, content: Union[Path, str], download_dir: Path, cookie: str, def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
episodes: Set[int] = None, category: str = None, episodes: Set[int] = None, category: str = None,
@@ -79,15 +103,16 @@ class TransmissionModule(_ModuleBase):
logger.error(f"获取种子名称失败:{e}") logger.error(f"获取种子名称失败:{e}")
return "", 0 return "", 0
# 不是默认下载器不处理
if downloader != "transmission":
return None
if not content: if not content:
return None return None, "下载内容为空"
if isinstance(content, Path) and not content.exists(): if isinstance(content, Path) and not content.exists():
return None, f"种子文件不存在:{content}" return None, f"种子文件不存在:{content}"
# 获取下载器
server = self.get_server(downloader)
if not server:
return None
# 如果要选择文件则先暂停 # 如果要选择文件则先暂停
is_paused = True if episodes else False is_paused = True if episodes else False
# 标签 # 标签
@@ -96,7 +121,7 @@ class TransmissionModule(_ModuleBase):
else: else:
labels = None labels = None
# 添加任务 # 添加任务
torrent = self.transmission.add_torrent( torrent = server.add_torrent(
content=content.read_bytes() if isinstance(content, Path) else content, content=content.read_bytes() if isinstance(content, Path) else content,
download_dir=str(download_dir), download_dir=str(download_dir),
is_paused=is_paused, is_paused=is_paused,
@@ -109,7 +134,7 @@ class TransmissionModule(_ModuleBase):
if not torrent_name: if not torrent_name:
return None, f"添加种子任务失败:无法读取种子文件" return None, f"添加种子任务失败:无法读取种子文件"
# 查询所有下载器的种子 # 查询所有下载器的种子
torrents, error = self.transmission.get_torrents() torrents, error = server.get_torrents()
if error: if error:
return None, "无法连接transmission下载器" return None, "无法连接transmission下载器"
if torrents: if torrents:
@@ -126,17 +151,17 @@ class TransmissionModule(_ModuleBase):
for tag in torrent.labels] if hasattr(torrent, "labels") else [] for tag in torrent.labels] if hasattr(torrent, "labels") else []
if "已整理" in labels: if "已整理" in labels:
labels.remove("已整理") labels.remove("已整理")
self.transmission.set_torrent_tag(ids=torrent_hash, tags=labels) server.set_torrent_tag(ids=torrent_hash, tags=labels)
if settings.TORRENT_TAG and settings.TORRENT_TAG not in labels: if settings.TORRENT_TAG and settings.TORRENT_TAG not in labels:
labels.append(settings.TORRENT_TAG) labels.append(settings.TORRENT_TAG)
self.transmission.set_torrent_tag(ids=torrent_hash, tags=labels) server.set_torrent_tag(ids=torrent_hash, tags=labels)
return torrent_hash, f"下载任务已存在" return torrent_hash, f"下载任务已存在"
return None, f"添加种子任务失败:{content}" return None, f"添加种子任务失败:{content}"
else: else:
torrent_hash = torrent.hashString torrent_hash = torrent.hashString
if is_paused: if is_paused:
# 选择文件 # 选择文件
torrent_files = self.transmission.get_files(torrent_hash) torrent_files = server.get_files(torrent_hash)
if not torrent_files: if not torrent_files:
return torrent_hash, "获取种子文件失败,下载任务可能在暂停状态" return torrent_hash, "获取种子文件失败,下载任务可能在暂停状态"
# 需要的文件信息 # 需要的文件信息
@@ -155,10 +180,10 @@ class TransmissionModule(_ModuleBase):
continue continue
file_ids.append(file_id) file_ids.append(file_id)
# 选择文件 # 选择文件
self.transmission.set_files(torrent_hash, file_ids) server.set_files(torrent_hash, file_ids)
self.transmission.set_unwanted_files(torrent_hash, unwanted_file_ids) server.set_unwanted_files(torrent_hash, unwanted_file_ids)
# 开始任务 # 开始任务
self.transmission.start_torrents(torrent_hash) server.start_torrents(torrent_hash)
return torrent_hash, "添加下载任务成功" return torrent_hash, "添加下载任务成功"
else: else:
return torrent_hash, "添加下载任务成功" return torrent_hash, "添加下载任务成功"
@@ -174,12 +199,14 @@ class TransmissionModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: 下载器中符合状态的种子列表 :return: 下载器中符合状态的种子列表
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
ret_torrents = [] ret_torrents = []
if hashs: if hashs:
# 按Hash获取 # 按Hash获取
torrents, _ = self.transmission.get_torrents(ids=hashs, tags=settings.TORRENT_TAG) torrents, _ = server.get_torrents(ids=hashs, tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
ret_torrents.append(TransferTorrent( ret_torrents.append(TransferTorrent(
title=torrent.name, title=torrent.name,
@@ -190,7 +217,7 @@ class TransmissionModule(_ModuleBase):
)) ))
elif status == TorrentStatus.TRANSFER: elif status == TorrentStatus.TRANSFER:
# 获取已完成且未整理的 # 获取已完成且未整理的
torrents = self.transmission.get_completed_torrents(tags=settings.TORRENT_TAG) torrents = server.get_completed_torrents(tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
# 含"已整理"tag的不处理 # 含"已整理"tag的不处理
if "已整理" in torrent.labels or []: if "已整理" in torrent.labels or []:
@@ -209,7 +236,7 @@ class TransmissionModule(_ModuleBase):
)) ))
elif status == TorrentStatus.DOWNLOADING: elif status == TorrentStatus.DOWNLOADING:
# 获取正在下载的任务 # 获取正在下载的任务
torrents = self.transmission.get_downloading_torrents(tags=settings.TORRENT_TAG) torrents = server.get_downloading_torrents(tags=settings.TORRENT_TAG)
for torrent in torrents or []: for torrent in torrents or []:
meta = MetaInfo(torrent.name) meta = MetaInfo(torrent.name)
dlspeed = torrent.rate_download if hasattr(torrent, "rate_download") else torrent.rateDownload dlspeed = torrent.rate_download if hasattr(torrent, "rate_download") else torrent.rateDownload
@@ -240,16 +267,18 @@ class TransmissionModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: None :return: None
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
# 获取原标签 # 获取原标签
org_tags = self.transmission.get_torrent_tags(ids=hashs) org_tags = server.get_torrent_tags(ids=hashs)
# 种子打上已整理标签 # 种子打上已整理标签
if org_tags: if org_tags:
tags = org_tags + ['已整理'] tags = org_tags + ['已整理']
else: else:
tags = ['已整理'] tags = ['已整理']
self.transmission.set_torrent_tag(ids=hashs, tags=tags) server.set_torrent_tag(ids=hashs, tags=tags)
# 移动模式删除种子 # 移动模式删除种子
if settings.TRANSFER_TYPE in ["move", "rclone_move"]: if settings.TRANSFER_TYPE in ["move", "rclone_move"]:
if self.remove_torrents(hashs): if self.remove_torrents(hashs):
@@ -270,9 +299,11 @@ class TransmissionModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
return self.transmission.delete_torrents(delete_file=delete_file, ids=hashs) return server.delete_torrents(delete_file=delete_file, ids=hashs)
def start_torrents(self, hashs: Union[list, str], def start_torrents(self, hashs: Union[list, str],
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]: downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
@@ -282,9 +313,11 @@ class TransmissionModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
return self.transmission.start_torrents(ids=hashs) return server.start_torrents(ids=hashs)
def stop_torrents(self, hashs: Union[list, str], def stop_torrents(self, hashs: Union[list, str],
downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]: downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[bool]:
@@ -294,28 +327,43 @@ class TransmissionModule(_ModuleBase):
:param downloader: 下载器 :param downloader: 下载器
:return: bool :return: bool
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
return self.transmission.start_torrents(ids=hashs) return server.start_torrents(ids=hashs)
def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[List[File]]: def torrent_files(self, tid: str, downloader: str = settings.DEFAULT_DOWNLOADER) -> Optional[List[File]]:
""" """
获取种子文件列表 获取种子文件列表
""" """
if downloader != "transmission": # 获取下载器
server = self.get_server(downloader)
if not server:
return None return None
return self.transmission.get_files(tid=tid) return server.get_files(tid=tid)
def downloader_info(self) -> [schemas.DownloaderInfo]: def downloader_info(self, downloader: str = None) -> Optional[List[schemas.DownloaderInfo]]:
""" """
下载器信息 下载器信息
""" """
info = self.transmission.transfer_info() if downloader:
if not info: server = self.get_server(downloader)
return [schemas.DownloaderInfo()] if not server:
return [schemas.DownloaderInfo( return None
download_speed=info.download_speed, servers = [server]
upload_speed=info.upload_speed, else:
download_size=info.current_stats.downloaded_bytes, servers = self._servers.values()
upload_size=info.current_stats.uploaded_bytes # 调用Qbittorrent API查询实时信息
)] ret_info = []
for server in servers:
info = server.transfer_info()
if not info:
continue
ret_info.append(schemas.DownloaderInfo(
download_speed=info.download_speed,
upload_speed=info.upload_speed,
download_size=info.current_stats.downloaded_bytes,
upload_size=info.current_stats.uploaded_bytes
))
return ret_info
+4 -1
View File
@@ -22,10 +22,13 @@ class Transmission:
"peersGettingFromUs", "peersSendingToUs", "uploadRatio", "uploadedEver", "downloadedEver", "downloadDir", "peersGettingFromUs", "peersSendingToUs", "uploadRatio", "uploadedEver", "downloadedEver", "downloadDir",
"error", "errorString", "doneDate", "queuePosition", "activityDate", "trackers"] "error", "errorString", "doneDate", "queuePosition", "activityDate", "trackers"]
def __init__(self, host: str = None, port: int = None, username: str = None, password: str = None): def __init__(self, host: str, port: int, username: str = None, password: str = None, **kwargs):
""" """
若不设置参数,则创建配置文件设置的下载器 若不设置参数,则创建配置文件设置的下载器
""" """
if not host or not port:
logger.error("Transmission配置不完整!")
return
self._host = host self._host = host
self._port = port self._port = port
self._username = username self._username = username
+4 -1
View File
@@ -22,10 +22,13 @@ class VoceChat:
# 请求对象 # 请求对象
_client = None _client = None
def __init__(self, host: str = None, apikey: str = None, channel_id: str = None): def __init__(self, host: str, apikey: str, channel_id: str, **kwargs):
""" """
初始化 初始化
""" """
if not host or not apikey or not channel_id:
logger.error("VoceChat配置不完整!")
return
self._host = host self._host = host
if self._host: if self._host:
if not self._host.endswith("/"): if not self._host.endswith("/"):
+4 -1
View File
@@ -37,10 +37,13 @@ class WeChat:
# 企业微信创新菜单URL # 企业微信创新菜单URL
_create_menu_url = "/cgi-bin/menu/create?access_token=%s&agentid=%s" _create_menu_url = "/cgi-bin/menu/create?access_token=%s&agentid=%s"
def __init__(self, corpid: str = None, appsecret: str = None, appid: str = None, proxy: str = None): def __init__(self, corpid: str, appsecret: str, appid: str, proxy: str = None, **kwargs):
""" """
初始化 初始化
""" """
if not corpid or not appsecret or not appid:
logger.error("企业微信配置不完整!")
return
self._corpid = corpid self._corpid = corpid
self._appsecret = appsecret self._appsecret = appsecret
self._appid = appid self._appid = appid
+1 -1
View File
@@ -13,5 +13,5 @@ from .mediaserver import *
from .message import * from .message import *
from .tmdb import * from .tmdb import *
from .transfer import * from .transfer import *
from .file import *
from .filetransfer import * from .filetransfer import *
from .mediaserver import *
+29
View File
@@ -0,0 +1,29 @@
from typing import Optional
from pydantic import BaseModel
class MediaServerConf(BaseModel):
"""
媒体服务器配置
"""
# 名称
name: Optional[str] = None
# 类型 emby/jellyfin/plex
type: Optional[str] = None
# 配置
config: Optional[dict] = {}
class DownloaderConf(BaseModel):
"""
下载器配置
"""
# 名称
name: Optional[str] = None
# 类型 qbittorrent/transmission
type: Optional[str] = None
# 是否默认
default: Optional[bool] = False
# 配置
config: Optional[dict] = {}