From 1e151c55f303cef459226392c323e3ba6c9a714e Mon Sep 17 00:00:00 2001 From: Allen Date: Tue, 14 May 2024 16:59:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=99=A8=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=E6=96=B0=E5=A2=9E=E4=BB=AA=E8=A1=A8=E6=9D=BF=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E7=A7=8D=E5=AD=90=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- plugins/downloaderhelper/README.md | 11 + plugins/downloaderhelper/__init__.py | 525 +++++++++++++++++++++++++- plugins/downloaderhelper/convertor.py | 156 ++++++++ plugins/downloaderhelper/module.py | 40 ++ 5 files changed, 725 insertions(+), 10 deletions(-) create mode 100644 plugins/downloaderhelper/convertor.py diff --git a/package.json b/package.json index 1580b10..43994b5 100644 --- a/package.json +++ b/package.json @@ -616,11 +616,12 @@ "name": "下载器助手", "description": "自动做种、站点标签、自动删种。", "labels": "下载管理", - "version": "1.7", + "version": "1.8", "icon": "DownloaderHelper.png", "author": "hotlcc", "level": 2, "history": { + "v1.8": "新增仪表板活动种子组件,qb完美支持,tr尚未测试,有问题提Issue并@hotlcc", "v1.7": "优化了表单界面和一些逻辑。", "v1.6": "修复事件触发tr打标问题;表单界面优化。" } diff --git a/plugins/downloaderhelper/README.md b/plugins/downloaderhelper/README.md index a41b700..e171836 100644 --- a/plugins/downloaderhelper/README.md +++ b/plugins/downloaderhelper/README.md @@ -44,6 +44,7 @@ |排除种子标签|多个标签通过英文逗号分割,具备配置的任意标签的种子不会进行自动做种、站点标签、自动删种操作。| |站点标签前缀|站点标签的前缀,缺省时不添加前缀。| |配置Tracker映射|该开关无实际业务意义,仅用于触发展开配置Tracker映射窗口。| +|配置仪表板活动种子组件|该开关无实际业务意义,仅用于触发展开配置仪表板活动种子组件窗口。| |Tracker映射|站点标签的原理是根据tracker的域名去匹配站点,但是有的PT站的tracker域名和站点域名不一致,导致匹配不到站点,因此需要对这些特殊站点的tracker做映射;每行一个映射,格式是 `tracker域名:站点域名`,tracker域名可以是完整域名或者主域名。| ##### 2.1.2、下载器子任务配置项 @@ -55,6 +56,16 @@ |站点标签|是否启用站点标签功能,启用后还需要配合【定时周期】或者【监听下载事件】才可以在后台定时或者事件驱动执行。| |自动删种|是否启用自动删种功能,启用后还需要配合【定时周期】或者【监听源文件事件】才可以在后台定时或者事件驱动执行。| +##### 2.1.3、仪表板活动种子组件配置项 + +|配置项|说明| +|---|---| +|启用仪表板组件|是否启用仪表板组件。| +|组件尺寸|选择仪表板组件尺寸,即组件栅格化宽度。| +|刷新间隔(秒)|组件刷新时间间隔,单位为秒,缺省时不刷新。**请合理配置,间隔太短可能会导致下载器假死。**| +|目标下载器|选择要展示的目标下载器。| +|展示的字段|选择要展示的字段,展示顺序以选择的顺序为准。| + #### 2.2、Q&A (待补充) diff --git a/plugins/downloaderhelper/__init__.py b/plugins/downloaderhelper/__init__.py index c802180..67c3899 100644 --- a/plugins/downloaderhelper/__init__.py +++ b/plugins/downloaderhelper/__init__.py @@ -3,14 +3,14 @@ import re import urllib from datetime import datetime, timedelta from threading import Event as ThreadEvent, RLock -from typing import Any, List, Dict, Tuple, Optional, Set +from typing import Any, List, Dict, Tuple, Optional, Set, Union from urllib.parse import urlparse import pytz from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger -from qbittorrentapi import TorrentDictionary -from transmission_rpc import Torrent +from qbittorrentapi import TorrentDictionary, TorrentState +from transmission_rpc.torrent import Torrent, Status as TorrentStatus from app.core.config import settings from app.core.event import eventmanager, Event @@ -19,7 +19,7 @@ from app.log import logger from app.modules.qbittorrent.qbittorrent import Qbittorrent from app.modules.transmission.transmission import Transmission from app.plugins import _PluginBase -from app.plugins.downloaderhelper.module import TaskContext, TaskResult, Downloader +from app.plugins.downloaderhelper.module import TaskContext, TaskResult, Downloader, TorrentField, TorrentFieldMap from app.schemas.types import EventType from app.utils.string import StringUtils @@ -32,7 +32,7 @@ class DownloaderHelper(_PluginBase): # 插件图标 plugin_icon = "DownloaderHelper.png" # 插件版本 - plugin_version = "1.7" + plugin_version = "1.8" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -63,7 +63,22 @@ class DownloaderHelper(_PluginBase): # 插件缺省配置 __config_default: Dict[str, Any] = { 'site_name_priority': True, - 'tag_prefix': '站点/' + 'tag_prefix': '站点/', + 'dashboard_widget_size': 12, + 'dashboard_widget_target_downloader': 'default', + 'dashboard_widget_display_fields': [ + TorrentField.NAME.name, + TorrentField.SELECT_SIZE.name, + TorrentField.COMPLETED.name, + TorrentField.STATE.name, + TorrentField.DOWNLOAD_SPEED.name, + TorrentField.UPLOAD_SPEED.name, + TorrentField.REMAINING_TIME.name, + TorrentField.RATIO.name, + TorrentField.TAGS.name, + TorrentField.ADD_TIME.name, + TorrentField.UPLOADED.name, + ] } # 插件用户配置 __config: Dict[str, Any] = {} @@ -84,6 +99,8 @@ class DownloaderHelper(_PluginBase): """ 初始化插件 """ + # 修正配置 + config = self.__fix_config(config=config) # 加载插件配置 self.__config = config # 解析tracker映射 @@ -117,9 +134,15 @@ class DownloaderHelper(_PluginBase): 获取插件状态 """ state = True if self.__get_config_item(config_key='enable') and ( - self.__get_config_item(config_key='cron') or self.__check_enable_listen() - ) and self.__check_enable_any_task() \ - else False + ( + ( + self.__get_config_item(config_key='cron') + or self.__check_enable_listen() + ) + and self.__check_enable_any_task() + ) + or self.__check_enable_dashboard_widget() + ) else False return state @staticmethod @@ -243,6 +266,11 @@ class DownloaderHelper(_PluginBase): }] }] } for d in Downloader if d] + # 下载器字段选项 + downloader_field_options = [{ + 'title': field.name_, + 'value': field.name + } for field in TorrentField if field] # 返回form return [{ 'component': 'VForm', @@ -399,6 +427,20 @@ class DownloaderHelper(_PluginBase): 'hint': '点击展开Tracker映射配置窗口。' } }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSwitch', + 'props': { + 'model': '_config_dashboard_dialog_closed', + 'label': '配置仪表板活动种子组件', + 'hint': '点击展开仪表板组件配置窗口。' + } + }] }] }, { 'component': 'VDialog', @@ -441,6 +483,116 @@ class DownloaderHelper(_PluginBase): }] }] }] + }, { + 'component': 'VDialog', + 'props': { + 'model': '_config_dashboard_dialog_closed', + 'max-width': '40rem' + }, + 'content': [{ + 'component': 'VCard', + 'props': { + 'title': '配置仪表板活动种子组件', + 'style': { + 'padding': '0 20px 20px 20px' + } + }, + 'content': [{ + 'component': 'VDialogCloseBtn', + 'props': { + 'model': '_config_dashboard_dialog_closed' + } + }, { + 'component': 'VRow', + 'content': [{ + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSwitch', + 'props': { + 'model': 'enable_dashboard_widget', + 'label': '启用仪表板组件', + 'hint': '是否启用仪表板组件。' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSelect', + 'props': { + 'model': 'dashboard_widget_size', + 'label': '组件尺寸', + 'items': [ + {'title': '100%', 'value': 12}, + {'title': '2/3', 'value': 8}, + {'title': '50%', 'value': 6}, + {'title': '1/3', 'value': 4} + ], + 'hint': '选择仪表板组件尺寸。' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VTextField', + 'props': { + 'model': 'dashboard_widget_refresh', + 'label': '刷新间隔(秒)', + 'placeholder': '5', + 'type': 'number', + 'hint': '组件刷新时间间隔,单位为秒,缺省时不刷新。请合理配置,间隔太短可能会导致下载器假死。' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 + }, + 'content': [{ + 'component': 'VSelect', + 'props': { + 'model': 'dashboard_widget_target_downloader', + 'label': '目标下载器', + 'items': [ + {'title': '系统默认下载器', 'value': 'default'}, + {'title': Downloader.QB.name_, 'value': Downloader.QB.id}, + {'title': Downloader.TR.name_, 'value': Downloader.TR.id} + ], + 'hint': '选择要展示的目标下载器。' + } + }] + }, { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'xxl': 12, 'xl': 12, 'lg': 12, 'md': 12, 'sm': 12, 'xs': 12 + }, + 'content': [{ + 'component': 'VSelect', + 'props': { + 'model': 'dashboard_widget_display_fields', + 'label': '展示的字段', + 'multiple': True, + 'chips': True, + 'items': downloader_field_options, + 'hint': '选择要展示的字段,展示顺序以选择的顺序为准。' + } + }] + }] + }] + }] }, { 'component': 'VRow', 'content': [{ @@ -499,6 +651,41 @@ class DownloaderHelper(_PluginBase): def get_page(self) -> List[dict]: pass + def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、全局配置参考: + { + "refresh": 10 // 自动刷新时间,单位秒 + } + 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + """ + if not self.get_state() or not self.__check_enable_dashboard_widget(): + return None + dashboard_widget_size = self.__get_config_item('dashboard_widget_size') + # 列配置 + cols = { + 'cols': 12, + 'xxl': dashboard_widget_size, + 'xl': dashboard_widget_size, + 'lg': dashboard_widget_size, + 'md': dashboard_widget_size, + 'sm': 12, + 'xs': 12 + } + # 全局配置 + attrs = { + 'refresh': self.__get_config_item('dashboard_widget_refresh'), + 'subtitle': '活动种子' + } + # 页面元素 + elements = self.__get_dashboard_elememts() + return cols, attrs, elements + def stop_service(self): """ 退出插件 @@ -603,6 +790,21 @@ class DownloaderHelper(_PluginBase): except Exception as e: logger.error(f"插件服务调度器停止异常: {str(e)}", exc_info=True) + def __fix_config(self, config: dict) -> dict: + """ + 修正配置 + """ + if not config: + config = {} + dashboard_widget_size = config.get('dashboard_widget_size') + config['dashboard_widget_size'] = int(dashboard_widget_size) if dashboard_widget_size else None + dashboard_widget_refresh = config.get('dashboard_widget_refresh') + config['dashboard_widget_refresh'] = int(dashboard_widget_refresh) if dashboard_widget_refresh else None + dashboard_widget_display_fields = config.get('dashboard_widget_display_fields') + config['dashboard_widget_display_fields'] = list(filter(lambda field: TorrentFieldMap.get(field), dashboard_widget_display_fields)) if dashboard_widget_display_fields else [] + self.update_config(config=config) + return config + def __get_config_item(self, config_key: str, use_default: bool = True) -> Any: """ 获取插件配置项 @@ -706,6 +908,13 @@ class DownloaderHelper(_PluginBase): return True if self.__check_enable_qb_task() \ or self.__check_enable_tr_task() else False + def __check_enable_dashboard_widget(self) -> bool: + """ + 判断是否启用了仪表板组件 + :return: 是否启用了仪表板组件 + """ + return True if self.__get_config_item('enable_dashboard_widget') else False + @classmethod def __parse_tracker_for_qbittorrent(cls, torrent: TorrentDictionary) -> Optional[str]: """ @@ -1531,6 +1740,304 @@ class DownloaderHelper(_PluginBase): logger.info(f"'[TR]单个删种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") return True + @staticmethod + def __ensure_torrent_fields(fields: List[Union[str, TorrentField]]) -> List[TorrentField]: + """ + 确保种子字段类型 + """ + result = [] + if not fields: + return result + for field in fields: + if not field: + continue + if isinstance(field, str): + field = TorrentFieldMap.get(field) + if not field: + continue + if isinstance(field, TorrentField): + result.append(field) + return result + + def __build_dashboard_widget_table_head_content(self, fields: List[Union[str, TorrentField]] = None) -> list: + """ + 构造仪表板组件表头内容 + """ + if not fields: + fields = self.__get_config_item('dashboard_widget_display_fields') + fields = self.__ensure_torrent_fields(fields=fields) + if not fields: + return [] + return [{ + 'component': 'th', + 'props': { + 'class': 'text-start ps-4' + }, + 'text': field.name_ + } for field in fields] + + def __build_dashboard_widget_table_head(self, fields: List[Union[str, TorrentField]] = None) -> dict: + """ + 构造仪表板组件表头 + """ + return { + 'component': 'thead', + 'content': self.__build_dashboard_widget_table_head_content(fields=fields) + } + + def __build_dashboard_widget_table_body_content(self, data: List[List[Any]], fields: List[Union[str, TorrentField]] = None) -> list: + """ + 构造仪表板组件表体内容 + """ + if data: + return [{ + 'component': 'tr', + 'props': { + 'class': 'text-sm' + }, + 'content': [{ + 'component': 'td', + 'props': { + 'class': 'whitespace-nowrap' + }, + 'text': col + } for col in row] + } for row in data if row] + else: + return [{ + 'component': 'tr', + 'props': { + 'class': 'text-sm' + }, + 'content': [{ + 'component': 'td', + 'props': { + 'colspan': len(fields), + 'class': 'text-center' + }, + 'text': '暂无数据' + }] + }] + + def __build_dashboard_widget_table_body(self, data: List[List[Any]], fields: List[Union[str, TorrentField]] = None) -> dict: + """ + 构造仪表板组件表体内容 + """ + return { + 'component': 'tbody', + 'content': self.__build_dashboard_widget_table_body_content(data=data, fields=fields) + } + + def __get_downloader_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + """ + 获取下载器种子数据 + """ + # 目标下载器 + target_downloader = self.__get_config_item('dashboard_widget_target_downloader') + if target_downloader == 'default': + target_downloader = settings.DEFAULT_DOWNLOADER + if not target_downloader: + return None + # 字段 + if not fields: + fields = self.__get_config_item('dashboard_widget_display_fields') + fields = self.__ensure_torrent_fields(fields=fields) + if target_downloader == Downloader.QB.id: + return self.__get_qbittorrent_torrent_data(fields=fields) + elif target_downloader == Downloader.TR.id: + return self.__get_transmission_torrent_data(fields=fields) + else: + return None + + def __get_qbittorrent_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + """ + 获取qb种子数据 + """ + qbittorrent = Qbittorrent() + if not qbittorrent.qbc: + return None + # 字段 + if not fields: + fields = self.__get_config_item('dashboard_widget_display_fields') + fields = self.__ensure_torrent_fields(fields=fields) + status = [TorrentState.DOWNLOADING.value, TorrentState.UPLOADING.value] + torrents, _ = qbittorrent.get_torrents(status=status) + if not torrents: + return None + # 按状态过滤 + torrents = list(filter(lambda torrent: torrent.get(TorrentField.STATE.qb) in status, torrents)) + # 按添加时间倒序排序 + torrents = sorted(torrents, key=lambda torrent: torrent.get(TorrentField.ADD_TIME.qb), reverse=True) + return self.__convert_qbittorrent_torrents_data(torrents=torrents, fields=fields) + + def __convert_qbittorrent_torrents_data(self, torrents: List[TorrentDictionary], fields: List[TorrentField]) -> List[List[Any]]: + """ + 转换qb种子数据 + """ + if not torrents or not fields: + return None + return [self.__convert_qbittorrent_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if torrent] + + def __process_torrent_for_qbittorrent(self, torrent: TorrentDictionary): + """ + 加工qb种子 + """ + if not torrent: + return + try: + # 剩余大小 + remaining_size = torrent.get(TorrentField.SELECT_SIZE.qb) - torrent.get(TorrentField.COMPLETED.qb) + torrent[TorrentField.REMAINING.qb] = remaining_size + # 剩余时间 + if torrent.get(TorrentField.STATE.qb) == TorrentState.DOWNLOADING.value: + download_speed = torrent.get(TorrentField.DOWNLOAD_SPEED.qb) + if download_speed <= 0: + remaining_time = -1 + remaining_time = remaining_size / download_speed + else: + remaining_time = 0 + torrent[TorrentField.REMAINING_TIME.qb] = remaining_time + except Exception as e: + logger.error(f'加工qb种子: {str(e)}, torrent = {str(torrent)}', exc_info=True) + return None + + def __convert_qbittorrent_torrent_data(self, torrent: TorrentDictionary, fields: List[TorrentField]) -> List[Any]: + """ + 转换qb种子数据 + """ + if not torrent or not fields: + return None + # 加工qb种子 + self.__process_torrent_for_qbittorrent(torrent=torrent) + data = [] + for field in fields: + value = self.__extract_torrent_value_for_qbittorrent(torrent=torrent, field=field) + data.append(value) + return data + + def __extract_torrent_value_for_qbittorrent(self, torrent: TorrentDictionary, field: TorrentField) -> Any: + """ + 从qb种子中提取值 + """ + if not torrent or not field: + return None + try: + if not field.qb: + return None + value = torrent.get(field.qb) + if field.convertor: + value = field.convertor.convert(value) + return value + except Exception as e: + logger.error(f'从qb种子中提取值异常: {str(e)}, torrent = {str(torrent)}', exc_info=True) + return None + + def __get_transmission_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + """ + 获取tr种子数据 + """ + transmission = Transmission() + if not transmission.trc: + return None + # 字段 + if not fields: + fields = self.__get_config_item('dashboard_widget_display_fields') + fields = self.__ensure_torrent_fields(fields=fields) + status = [TorrentStatus.DOWNLOADING.value, TorrentStatus.SEEDING.value] + torrents, _ = transmission.get_torrents(status=status) + if not torrents: + return None + # 按添加时间倒序排序 + torrents = sorted(torrents, key=lambda torrent: torrent.fields.get(TorrentField.ADD_TIME.tr), reverse=True) + return self.__convert_transmission_torrents_data(torrents=torrents, fields=fields) + + def __convert_transmission_torrents_data(self, torrents: List[Torrent], fields: List[TorrentField]) -> List[List[Any]]: + """ + 转换tr种子数据 + """ + if not torrents or not fields: + return None + return [self.__convert_transmission_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if torrent] + + def __process_torrent_for_transmission(self, torrent: Torrent): + """ + 加工tr种子 + """ + try: + # 选定大小 + select_size = sum(x["bytesCompleted"] for x in torrent.fields["wanted"]) + torrent.fields[TorrentField.SELECT_SIZE.tr] = select_size + # 已完成大小 + completed = sum(x["bytesCompleted"] for x in torrent.fields["fileStats"]) + torrent.fields[TorrentField.COMPLETED.tr] = completed + # 剩余大小 + remaining_size = select_size - completed + torrent[TorrentField.REMAINING.tr] = remaining_size + # 剩余时间 + if torrent.get(TorrentField.STATE.tr) == TorrentStatus.DOWNLOADING.value: + download_speed = torrent.get(TorrentField.DOWNLOAD_SPEED.qb) + if download_speed <= 0: + remaining_time = -1 + remaining_time = remaining_size / download_speed + else: + remaining_time = 0 + torrent[TorrentField.REMAINING_TIME.tr] = remaining_time + except Exception as e: + logger.error(f'加工tr种子异常: {str(e)}, torrent = {str(torrent.fields)}', exc_info=True) + return None + + def __convert_transmission_torrent_data(self, torrent: Torrent, fields: List[TorrentField]) -> List[Any]: + """ + 转换tr种子数据 + """ + if not torrent or not fields: + return None + # 加工tr种子 + self.__process_torrent_for_transmission(torrent=torrent) + data = [] + for field in fields: + value = self.__extract_torrent_value_for_transmission(torrent=torrent, field=field) + data.append(value) + return data + + def __extract_torrent_value_for_transmission(self, torrent: Torrent, field: TorrentField) -> Any: + """ + 从tr种子中提取值 + """ + if not torrent or not field: + return None + try: + if not field.tr: + return None + value = torrent.get(field.tr) + if field.convertor: + value = field.convertor.convert(value) + return value + except Exception as e: + logger.error(f'从tr种子中提取值异常: {str(e)}, torrent = {str(torrent.fields)}', exc_info=True) + return None + + def __get_dashboard_elememts(self) -> list: + """ + 获取仪表板元素 + """ + fields = self.__get_config_item('dashboard_widget_display_fields') + fields = self.__ensure_torrent_fields(fields=fields) + data = self.__get_downloader_torrent_data(fields=fields) + return [{ + 'component': 'VTable', + 'props': { + 'hover': True, + 'style': { + 'height': '230px' + } + }, + 'content': [ + self.__build_dashboard_widget_table_head(fields=fields), + self.__build_dashboard_widget_table_body(data=data, fields=fields) + ] + }] + @eventmanager.register(EventType.DownloadAdded) def listen_download_added_event(self, event: Event = None): """ diff --git a/plugins/downloaderhelper/convertor.py b/plugins/downloaderhelper/convertor.py new file mode 100644 index 0000000..14ffd8c --- /dev/null +++ b/plugins/downloaderhelper/convertor.py @@ -0,0 +1,156 @@ +from qbittorrentapi import TorrentState +from transmission_rpc.torrent import Status as TorrentStatus + +from abc import ABCMeta, abstractmethod +from app.utils.string import StringUtils +from app.utils.singleton import Singleton +from app.log import logger + +class IConvertor(metaclass=ABCMeta): + """ + 转换器接口 + """ + @abstractmethod + def convert(self, data: any) -> any: + """ + 转换 + """ + pass + +class ByteSizeConvertor(IConvertor, metaclass=Singleton): + """ + byte size 转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + return StringUtils.str_filesize(data) + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class PercentageConvertor(IConvertor, metaclass=Singleton): + """ + 百分比转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + return f'{round(data * 100)}%' + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class StateConvertor(IConvertor, metaclass=Singleton): + """ + 状态转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + # qb + if data == TorrentState.UPLOADING.value: + return '做种' + if data == TorrentState.DOWNLOADING.value: + return '下载中' + # tr + if data == TorrentStatus.SEEDING.value: + return '做种' + if data == TorrentStatus.DOWNLOADING.value: + return '下载中' + return data + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class SpeedConvertor(IConvertor, metaclass=Singleton): + """ + 速度转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + data = ByteSizeConvertor().convert(data=data) + if not data: + data = '0B' + return f'{data}/s' + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class RatioConvertor(IConvertor, metaclass=Singleton): + """ + 比率(分享率)转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + return round(data, 2) + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class TimestampConvertor(IConvertor, metaclass=Singleton): + """ + 时间戳转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + return StringUtils.format_timestamp(timestamp=data, date_format='%Y/%m/%d %H:%M:%S') + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class TimeIntervalConvertor(IConvertor, metaclass=Singleton): + """ + 时间间隔转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + if data < 0: + return '∞' + if data == 0: + return '0' + return StringUtils.str_timelong(time_sec=data) + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class LimitSpeedConvertor(IConvertor, metaclass=Singleton): + """ + 限制速度转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + if data <= 0: + return '∞' + return SpeedConvertor().convert(data=data) + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None + +class LimitRatioConvertor(IConvertor, metaclass=Singleton): + """ + 限制比率(分享率)转换器 + """ + def convert(self, data: any) -> any: + if data is None: + return None + try: + if data <= 0: + return '∞' + return RatioConvertor().convert(data=data) + except Exception as e: + logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) + return None diff --git a/plugins/downloaderhelper/module.py b/plugins/downloaderhelper/module.py index cab6158..36e5280 100644 --- a/plugins/downloaderhelper/module.py +++ b/plugins/downloaderhelper/module.py @@ -1,5 +1,6 @@ from typing import Set, List, Optional from enum import Enum +from app.plugins.downloaderhelper.convertor import IConvertor, ByteSizeConvertor, PercentageConvertor, StateConvertor, SpeedConvertor, RatioConvertor, TimestampConvertor, LimitSpeedConvertor, LimitRatioConvertor, TimeIntervalConvertor class Downloader(Enum): @@ -264,3 +265,42 @@ class TaskContext: 获取操作用户名 """ return self.__username + + +class TorrentField(Enum): + """ + 种子字段枚举 + """ + NAME = ('名称', 'name', 'name', None) + SELECT_SIZE = ('选定大小', 'size', '#SELECT_SIZE', ByteSizeConvertor()) + TOTAL_SIZE = ('总大小', 'total_size', 'totalSize', ByteSizeConvertor()) + PROGRESS = ('已完成', 'progress', 'percentDone', PercentageConvertor()) + STATE = ('状态', 'state', '_status_str', StateConvertor()) + DOWNLOAD_SPEED = ('下载速度', 'dlspeed', 'rateDownload', SpeedConvertor()) + UPLOAD_SPEED = ('上传速度', 'upspeed', 'rateUpload', SpeedConvertor()) + REMAINING_TIME = ('剩余时间', '#REMAINING_TIME', '#REMAINING_TIME', TimeIntervalConvertor()) + RATIO = ('比率', 'ratio', 'uploadRatio', RatioConvertor()) + CATEGORY = ('分类', 'category', None, None) + TAGS = ('标签', 'tags', 'labels', None) + ADD_TIME = ('添加时间', 'added_on', 'addedDate', TimestampConvertor()) + COMPLETE_TIME = ('完成时间', 'completion_on', 'doneDate', TimestampConvertor()) + DOWNLOAD_LIMIT = ('下载限制', 'dl_limit', 'downloadLimit', LimitSpeedConvertor()) + UPLOAD_LIMIT = ('上传限制', 'up_limit', 'uploadLimit', LimitSpeedConvertor()) + DOWNLOADED = ('已下载', 'downloaded', 'downloadedEver', ByteSizeConvertor()) + UPLOADED = ('已上传', 'uploaded', 'uploadedEver', ByteSizeConvertor()) + DOWNLOADED_SESSION = ('本次会话下载', 'downloaded_session', None, ByteSizeConvertor()) + UPLOADED_SESSION = ('本次会话上传', 'uploaded_session', None, ByteSizeConvertor()) + REMAINING = ('剩余', '#REMAINING', '#REMAINING', ByteSizeConvertor()) + SAVE_PATH = ('保存路径', 'save_path', 'downloadDir', None) + COMPLETED = ('完成', 'completed', '#COMPLETED', ByteSizeConvertor()) + RATIO_LIMIT = ('比率限制', 'ratio_limit', 'seedRatioLimit', LimitRatioConvertor()) + + def __init__(self, name_: str, qb: str, tr: str, convertor: IConvertor): + self.name_ = name_ + self.qb = qb + self.tr = tr + self.convertor = convertor + + +# TorrentField 映射 +TorrentFieldMap = dict((field.name, field) for field in TorrentField) From c1ef5085f40add5dc6383099d92fa4e183da902e Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 14 May 2024 18:11:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix=20#291=20=E4=BF=AE=E5=A4=8DyCharm?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/downloaderhelper/__init__.py | 80 ++++++++++++++++----------- plugins/downloaderhelper/convertor.py | 20 +++++++ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/plugins/downloaderhelper/__init__.py b/plugins/downloaderhelper/__init__.py index 67c3899..81fb4b0 100644 --- a/plugins/downloaderhelper/__init__.py +++ b/plugins/downloaderhelper/__init__.py @@ -134,14 +134,14 @@ class DownloaderHelper(_PluginBase): 获取插件状态 """ state = True if self.__get_config_item(config_key='enable') and ( - ( ( - self.__get_config_item(config_key='cron') - or self.__check_enable_listen() + ( + self.__get_config_item(config_key='cron') + or self.__check_enable_listen() + ) + and self.__check_enable_any_task() ) - and self.__check_enable_any_task() - ) - or self.__check_enable_dashboard_widget() + or self.__check_enable_dashboard_widget() ) else False return state @@ -474,9 +474,9 @@ class DownloaderHelper(_PluginBase): 'model': 'tracker_mappings', 'label': 'Tracker映射', 'placeholder': '格式:\n' - ':\n\n' - '例如:\n' - 'chdbits.xyz:ptchdbits.co', + ':\n\n' + '例如:\n' + 'chdbits.xyz:ptchdbits.co', 'hint': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' } }] @@ -801,7 +801,8 @@ class DownloaderHelper(_PluginBase): dashboard_widget_refresh = config.get('dashboard_widget_refresh') config['dashboard_widget_refresh'] = int(dashboard_widget_refresh) if dashboard_widget_refresh else None dashboard_widget_display_fields = config.get('dashboard_widget_display_fields') - config['dashboard_widget_display_fields'] = list(filter(lambda field: TorrentFieldMap.get(field), dashboard_widget_display_fields)) if dashboard_widget_display_fields else [] + config['dashboard_widget_display_fields'] = list(filter(lambda field: TorrentFieldMap.get(field), + dashboard_widget_display_fields)) if dashboard_widget_display_fields else [] self.update_config(config=config) return config @@ -1785,7 +1786,9 @@ class DownloaderHelper(_PluginBase): 'content': self.__build_dashboard_widget_table_head_content(fields=fields) } - def __build_dashboard_widget_table_body_content(self, data: List[List[Any]], fields: List[Union[str, TorrentField]] = None) -> list: + @staticmethod + def __build_dashboard_widget_table_body_content(data: List[List[Any]], + fields: List[Union[str, TorrentField]] = None) -> list: """ 构造仪表板组件表体内容 """ @@ -1819,7 +1822,8 @@ class DownloaderHelper(_PluginBase): }] }] - def __build_dashboard_widget_table_body(self, data: List[List[Any]], fields: List[Union[str, TorrentField]] = None) -> dict: + def __build_dashboard_widget_table_body(self, data: List[List[Any]], + fields: List[Union[str, TorrentField]] = None) -> dict: """ 构造仪表板组件表体内容 """ @@ -1832,7 +1836,7 @@ class DownloaderHelper(_PluginBase): """ 获取下载器种子数据 """ - # 目标下载器 + # 目标下载器 target_downloader = self.__get_config_item('dashboard_widget_target_downloader') if target_downloader == 'default': target_downloader = settings.DEFAULT_DOWNLOADER @@ -1870,15 +1874,18 @@ class DownloaderHelper(_PluginBase): torrents = sorted(torrents, key=lambda torrent: torrent.get(TorrentField.ADD_TIME.qb), reverse=True) return self.__convert_qbittorrent_torrents_data(torrents=torrents, fields=fields) - def __convert_qbittorrent_torrents_data(self, torrents: List[TorrentDictionary], fields: List[TorrentField]) -> List[List[Any]]: + def __convert_qbittorrent_torrents_data(self, torrents: List[TorrentDictionary], + fields: List[TorrentField]) -> Optional[List[List[Any]]]: """ 转换qb种子数据 """ if not torrents or not fields: return None - return [self.__convert_qbittorrent_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if torrent] + return [self.__convert_qbittorrent_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if + torrent] - def __process_torrent_for_qbittorrent(self, torrent: TorrentDictionary): + @staticmethod + def __process_torrent_for_qbittorrent(torrent: TorrentDictionary): """ 加工qb种子 """ @@ -1893,7 +1900,8 @@ class DownloaderHelper(_PluginBase): download_speed = torrent.get(TorrentField.DOWNLOAD_SPEED.qb) if download_speed <= 0: remaining_time = -1 - remaining_time = remaining_size / download_speed + else: + remaining_time = remaining_size / download_speed else: remaining_time = 0 torrent[TorrentField.REMAINING_TIME.qb] = remaining_time @@ -1901,7 +1909,8 @@ class DownloaderHelper(_PluginBase): logger.error(f'加工qb种子: {str(e)}, torrent = {str(torrent)}', exc_info=True) return None - def __convert_qbittorrent_torrent_data(self, torrent: TorrentDictionary, fields: List[TorrentField]) -> List[Any]: + def __convert_qbittorrent_torrent_data(self, torrent: TorrentDictionary, + fields: List[TorrentField]) -> Optional[List[Any]]: """ 转换qb种子数据 """ @@ -1915,7 +1924,8 @@ class DownloaderHelper(_PluginBase): data.append(value) return data - def __extract_torrent_value_for_qbittorrent(self, torrent: TorrentDictionary, field: TorrentField) -> Any: + @staticmethod + def __extract_torrent_value_for_qbittorrent(torrent: TorrentDictionary, field: TorrentField) -> Any: """ 从qb种子中提取值 """ @@ -1951,15 +1961,18 @@ class DownloaderHelper(_PluginBase): torrents = sorted(torrents, key=lambda torrent: torrent.fields.get(TorrentField.ADD_TIME.tr), reverse=True) return self.__convert_transmission_torrents_data(torrents=torrents, fields=fields) - def __convert_transmission_torrents_data(self, torrents: List[Torrent], fields: List[TorrentField]) -> List[List[Any]]: + def __convert_transmission_torrents_data(self, torrents: List[Torrent], + fields: List[TorrentField]) -> Optional[List[List[Any]]]: """ 转换tr种子数据 """ if not torrents or not fields: return None - return [self.__convert_transmission_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if torrent] + return [self.__convert_transmission_torrent_data(torrent=torrent, fields=fields) for torrent in torrents if + torrent] - def __process_torrent_for_transmission(self, torrent: Torrent): + @staticmethod + def __process_torrent_for_transmission(torrent: Torrent): """ 加工tr种子 """ @@ -1972,21 +1985,23 @@ class DownloaderHelper(_PluginBase): torrent.fields[TorrentField.COMPLETED.tr] = completed # 剩余大小 remaining_size = select_size - completed - torrent[TorrentField.REMAINING.tr] = remaining_size + torrent.fields[TorrentField.REMAINING.tr] = remaining_size # 剩余时间 if torrent.get(TorrentField.STATE.tr) == TorrentStatus.DOWNLOADING.value: download_speed = torrent.get(TorrentField.DOWNLOAD_SPEED.qb) if download_speed <= 0: remaining_time = -1 - remaining_time = remaining_size / download_speed + else: + remaining_time = remaining_size / download_speed else: remaining_time = 0 - torrent[TorrentField.REMAINING_TIME.tr] = remaining_time + torrent.fields[TorrentField.REMAINING_TIME.tr] = remaining_time except Exception as e: logger.error(f'加工tr种子异常: {str(e)}, torrent = {str(torrent.fields)}', exc_info=True) return None - def __convert_transmission_torrent_data(self, torrent: Torrent, fields: List[TorrentField]) -> List[Any]: + def __convert_transmission_torrent_data(self, torrent: Torrent, + fields: List[TorrentField]) -> Optional[List[Any]]: """ 转换tr种子数据 """ @@ -2000,7 +2015,8 @@ class DownloaderHelper(_PluginBase): data.append(value) return data - def __extract_torrent_value_for_transmission(self, torrent: Torrent, field: TorrentField) -> Any: + @staticmethod + def __extract_torrent_value_for_transmission(torrent: Torrent, field: TorrentField) -> Any: """ 从tr种子中提取值 """ @@ -2057,8 +2073,8 @@ class DownloaderHelper(_PluginBase): logger.info('下载添加事件监听任务执行开始...') # enable_seeding=True是针对辅种添加种子并跳过校验的场景 context = TaskContext().enable_seeding(True) \ - .enable_tagging(True) \ - .enable_delete(False) + .enable_tagging(True) \ + .enable_delete(False) _hash = event.event_data.get('hash') if _hash: context.select_torrent(torrent=_hash) @@ -2087,8 +2103,8 @@ class DownloaderHelper(_PluginBase): logger.info('源文件删除事件监听任务执行开始...') # 针对源文件监听事件只需要处理删种 context = TaskContext().enable_seeding(False) \ - .enable_tagging(False) \ - .enable_delete(True) \ - .set_deleted_event_data(event.event_data) + .enable_tagging(False) \ + .enable_delete(True) \ + .set_deleted_event_data(event.event_data) self.__block_run(context=context) logger.info('源文件删除事件监听任务执行结束') diff --git a/plugins/downloaderhelper/convertor.py b/plugins/downloaderhelper/convertor.py index 14ffd8c..7dac879 100644 --- a/plugins/downloaderhelper/convertor.py +++ b/plugins/downloaderhelper/convertor.py @@ -6,10 +6,12 @@ from app.utils.string import StringUtils from app.utils.singleton import Singleton from app.log import logger + class IConvertor(metaclass=ABCMeta): """ 转换器接口 """ + @abstractmethod def convert(self, data: any) -> any: """ @@ -17,10 +19,12 @@ class IConvertor(metaclass=ABCMeta): """ pass + class ByteSizeConvertor(IConvertor, metaclass=Singleton): """ byte size 转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -30,10 +34,12 @@ class ByteSizeConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class PercentageConvertor(IConvertor, metaclass=Singleton): """ 百分比转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -43,10 +49,12 @@ class PercentageConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class StateConvertor(IConvertor, metaclass=Singleton): """ 状态转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -66,10 +74,12 @@ class StateConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class SpeedConvertor(IConvertor, metaclass=Singleton): """ 速度转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -82,10 +92,12 @@ class SpeedConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class RatioConvertor(IConvertor, metaclass=Singleton): """ 比率(分享率)转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -95,10 +107,12 @@ class RatioConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class TimestampConvertor(IConvertor, metaclass=Singleton): """ 时间戳转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -108,10 +122,12 @@ class TimestampConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class TimeIntervalConvertor(IConvertor, metaclass=Singleton): """ 时间间隔转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -125,10 +141,12 @@ class TimeIntervalConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class LimitSpeedConvertor(IConvertor, metaclass=Singleton): """ 限制速度转换器 """ + def convert(self, data: any) -> any: if data is None: return None @@ -140,10 +158,12 @@ class LimitSpeedConvertor(IConvertor, metaclass=Singleton): logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True) return None + class LimitRatioConvertor(IConvertor, metaclass=Singleton): """ 限制比率(分享率)转换器 """ + def convert(self, data: any) -> any: if data is None: return None