Merge remote-tracking branch 'origin/main'

This commit is contained in:
mayun110
2024-05-14 20:04:05 +08:00
5 changed files with 769 additions and 18 deletions

View File

@@ -617,11 +617,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打标问题表单界面优化。"
}

View File

@@ -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
(待补充)

View File

@@ -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',
@@ -432,15 +474,125 @@ class DownloaderHelper(_PluginBase):
'model': 'tracker_mappings',
'label': 'Tracker映射',
'placeholder': '格式:\n'
'<tracker-domain>:<site-domain>\n\n'
'例如:\n'
'chdbits.xyz:ptchdbits.co',
'<tracker-domain>:<site-domain>\n\n'
'例如:\n'
'chdbits.xyz:ptchdbits.co',
'hint': 'Tracker映射。用于在站点打标签时指定tracker和站点域名不同的种子的域名对应关系前面为tracker域名完整域名或者主域名皆可中间是英文冒号后面是站点域名。'
}
}]
}]
}]
}]
}, {
'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,22 @@ 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 +909,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 +1741,319 @@ 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)
}
@staticmethod
def __build_dashboard_widget_table_body_content(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]) -> 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]
@staticmethod
def __process_torrent_for_qbittorrent(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
else:
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]) -> Optional[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
@staticmethod
def __extract_torrent_value_for_qbittorrent(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]) -> 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]
@staticmethod
def __process_torrent_for_transmission(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.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
else:
remaining_time = remaining_size / download_speed
else:
remaining_time = 0
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]) -> Optional[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
@staticmethod
def __extract_torrent_value_for_transmission(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):
"""
@@ -1550,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)
@@ -1580,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('源文件删除事件监听任务执行结束')

View File

@@ -0,0 +1,176 @@
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

View File

@@ -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)