mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-18 15:09:29 +00:00
移除已独立维护的插件,新仓库:https://github.com/hotlcc/MoviePilot-Plugins-Third/
This commit is contained in:
53
package.json
53
package.json
@@ -646,30 +646,6 @@
|
||||
"v1.2": "修复契约检查无数据返回的问题"
|
||||
}
|
||||
},
|
||||
"DownloaderHelper": {
|
||||
"name": "下载器助手",
|
||||
"description": "自动标签、自动做种、自动删种。",
|
||||
"labels": "下载管理,仪表板",
|
||||
"version": "2.8",
|
||||
"icon": "DownloaderHelper.png",
|
||||
"author": "hotlcc",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v2.8": "优化了仪表板组件高度;优化了活动种子状态;优化了自动标签,优化站点标签,新增BT/PT标签;监听原种删除事件补充删种逻辑。MP需要升级至v1.9.3,更新后若插件功能异常,请重启一次MP。",
|
||||
"v2.7": "实时速率仪表板样式微调。",
|
||||
"v2.6": "新增仪表板实时速率组件,支持单独展示qb和tr的实时速率(tr未测试,有问题提Issue并@hotlcc)。",
|
||||
"v2.5": "优化通知类型;降低认证级别要求,使MP非认证用户可用,但无法使用【站点名称优先】功能。主程序需升级至v1.9.2及以上版本,否则插件功能异常!",
|
||||
"v2.4": "修复tr活动种子仪表板的种子排序的bug;优化插件的消息发送。",
|
||||
"v2.3": "仪表板支持多个下载器活动种子组件(主程序版本需大于v1.9.1)。",
|
||||
"v2.2": "优化仪表板组件标题;优化仪表板下载剩余时间描述。",
|
||||
"v2.1": "优化了初始配置建议;优化了配置Tracker的弹窗大小。",
|
||||
"v2.0": "优化了仪表板种子状态;提升仪表板对TR的适配度。",
|
||||
"v1.9": "优化了仪表板组件性能。",
|
||||
"v1.8": "新增仪表板活动种子组件,qb完美支持,tr尚未测试,有问题提Issue并@hotlcc",
|
||||
"v1.7": "优化了表单界面和一些逻辑。",
|
||||
"v1.6": "修复事件触发tr打标问题;表单界面优化。"
|
||||
}
|
||||
},
|
||||
"FeiShuMsg": {
|
||||
"name": "飞书机器人消息通知",
|
||||
"description": "支持使用飞书群聊机器人发送消息通知。",
|
||||
@@ -700,35 +676,6 @@
|
||||
"author": "lethargicScribe",
|
||||
"level": 1
|
||||
},
|
||||
"PluginAutoUpgrade": {
|
||||
"name": "插件自动升级",
|
||||
"description": "定时检测、升级插件。",
|
||||
"labels": "自动更新",
|
||||
"version": "2.0",
|
||||
"icon": "PluginAutoUpgrade.png",
|
||||
"author": "hotlcc",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v2.0": "修正了缺省时的cron表达式。",
|
||||
"v1.9": "优化通知类型。主程序需升级至v1.9.2及以上版本,否则插件功能异常!",
|
||||
"v1.8": "修复重置插件后丢失配置建议的问题。",
|
||||
"v1.7": "修复了一些BUG。",
|
||||
"v1.6": "修正数字配置值提交为字符串导致的问题。",
|
||||
"v1.5": "支持配置升级记录最大保存数量和最大展示数量。"
|
||||
}
|
||||
},
|
||||
"MergeSiteSwitch": {
|
||||
"name": "聚合站点开关",
|
||||
"description": "统一管理所有与站点相关的开关。",
|
||||
"labels": "系统设置",
|
||||
"version": "1.1",
|
||||
"icon": "world.png",
|
||||
"author": "hotlcc",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v1.1": "优化插件配置生效;支持青蛙辅种助手。"
|
||||
}
|
||||
},
|
||||
"TmdbWallpaper": {
|
||||
"name": "登录壁纸本地化",
|
||||
"description": "将MoviePilot的登录壁纸下载到本地。",
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
## 下载器助手:一个解放双手的插件
|
||||
|
||||
### 1、简介
|
||||
|
||||
作者:hotlcc
|
||||
|
||||
主页:[Github](https://github.com/hotlcc) | [Gitee](https://gitee.com/hotlcc)
|
||||
|
||||
#### 1.1、功能说明
|
||||
|
||||
**三大功能**:
|
||||
|
||||
1. **自动做种**:通常在通过IYUU等工具辅种后,下载器中种子校验完毕但不会立即变成做种状态,可以通过本插件定时扫描下载器中“已完成但未做种”的种子并设为做种状态。
|
||||
1. **站点标签**:如果下载器中种子很多,要想统计各站点的种子数量变得麻烦,可以通过本插件定时(或者通过监听下载添加事件)给种子添加站点标签。
|
||||
1. **自动删种**:定时删除丢失文件的错误种子,或者通过监听源文件删除事件匹配对应的种子自动删除。
|
||||
|
||||
**三条执行路线**:
|
||||
|
||||
1. **定时执行**:通过配置【定时执行周期】实现后台定时执行。
|
||||
1. **事件驱动执行**:通过开启相关监听实现事件驱动执行。
|
||||
1. **手动执行**:通过【即运行一次】或者【设定/服务/执行】手动执行。
|
||||
|
||||
#### 1.2、移植说明
|
||||
|
||||
本插件由本人从原 NAStool 版本【[下载器助手](https://gitee.com/hotlcc/nastool-plugin/tree/master/downloader-helper)】移植而来,功能只增不减。
|
||||
|
||||
插件移植后未花太多心思测试,目前仅测试了 qBittorrent,理论上 Transmission 也不会有太大问题,但由于 MoviePilot 插件机制较原 NAStool 有很大升级,因而不排除存在潜在BUG,望使用者海涵,有问题提 Issues 即可!
|
||||
|
||||
### 2、使用说明
|
||||
|
||||
#### 2.1、配置项说明
|
||||
|
||||
##### 2.1.1、主配置项
|
||||
|
||||
|配置项|说明|
|
||||
|---|---|
|
||||
|启用插件|插件后台任务的总开关,这里的“后台任务”指的是“定时任务”和“事件触发任务”,不会影响【立即运行一次】的执行。|
|
||||
|发送通知|任务执行成功后是否发送通知消息。|
|
||||
|立即运行一次|保存配置后立即运行一次,不受【启用插件】的管控。|
|
||||
|监听下载事件|监听到下载添加事件后会触发插件给添加的种子打站点标签。|
|
||||
|监听源文件事件|监听到源文件删除事件后会触发插件根据文件路径判断该源文件对应的种子下的全部数据文件是否都已删除,若全部数据文件都已删除就删除种子,如果有辅种也会一并删除,同时支持单文件种子、多文件(剧集、原盘)种子。|
|
||||
|站点名称优先|表示在打站点标签时是否优先以站点名称作为标签,否则会以“域名关键字”作为标签;“域名关键字”指的是二级域名段。|
|
||||
|定时执行周期|插件定时服务的cron表达式,仅支持5位的,缺省时不注册定时服务。|
|
||||
|排除种子标签|多个标签通过英文逗号分割,具备配置的任意标签的种子不会进行自动做种、站点标签、自动删种操作。|
|
||||
|站点标签前缀|站点标签的前缀,缺省时不添加前缀。|
|
||||
|配置Tracker映射|该开关无实际业务意义,仅用于触发展开配置Tracker映射窗口。|
|
||||
|配置仪表板活动种子组件|该开关无实际业务意义,仅用于触发展开配置仪表板活动种子组件窗口。|
|
||||
|Tracker映射|站点标签的原理是根据tracker的域名去匹配站点,但是有的PT站的tracker域名和站点域名不一致,导致匹配不到站点,因此需要对这些特殊站点的tracker做映射;每行一个映射,格式是 `tracker域名:站点域名`,tracker域名可以是完整域名或者主域名。|
|
||||
|
||||
##### 2.1.2、下载器子任务配置项
|
||||
|
||||
|配置项|说明|
|
||||
|---|---|
|
||||
|任务开关|该下载器子任务的总开关。|
|
||||
|自动做种|是否启用自动做种功能,启用后还需要配合【定时周期】才可以在后台定时执行。|
|
||||
|站点标签|是否启用站点标签功能,启用后还需要配合【定时周期】或者【监听下载事件】才可以在后台定时或者事件驱动执行。|
|
||||
|自动删种|是否启用自动删种功能,启用后还需要配合【定时周期】或者【监听源文件事件】才可以在后台定时或者事件驱动执行。|
|
||||
|
||||
##### 2.1.3、仪表板活动种子组件配置项
|
||||
|
||||
|配置项|说明|
|
||||
|---|---|
|
||||
|启用仪表板组件|是否启用仪表板组件。|
|
||||
|组件尺寸|选择仪表板组件尺寸,即组件栅格化宽度。|
|
||||
|刷新间隔(秒)|组件刷新时间间隔,单位为秒,缺省时不刷新。**请合理配置,间隔太短可能会导致下载器假死。**|
|
||||
|目标下载器|选择要展示的目标下载器。|
|
||||
|展示的字段|选择要展示的字段,展示顺序以选择的顺序为准。|
|
||||
|
||||
#### 2.2、Q&A
|
||||
|
||||
(待补充)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,206 +0,0 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from qbittorrentapi import TorrentState
|
||||
|
||||
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 '下载中'
|
||||
if data == TorrentState.PAUSED_DOWNLOAD.value:
|
||||
return '暂停'
|
||||
if data == TorrentState.STALLED_DOWNLOAD.value:
|
||||
return '等待'
|
||||
if data == TorrentState.CHECKING_DOWNLOAD.value:
|
||||
return '校验'
|
||||
if data == TorrentState.QUEUED_DOWNLOAD.value:
|
||||
return '排队'
|
||||
# tr
|
||||
if data == 6:
|
||||
return '做种'
|
||||
if data == 4:
|
||||
return '下载中'
|
||||
if data == 0:
|
||||
return '暂停'
|
||||
if data == 3:
|
||||
return '等待'
|
||||
if data == 2:
|
||||
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 not data or data <= 0:
|
||||
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_secends(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
|
||||
|
||||
|
||||
class TagsConvertor(IConvertor, metaclass=Singleton):
|
||||
"""
|
||||
标签转换器
|
||||
"""
|
||||
|
||||
def convert(self, data: any) -> any:
|
||||
if not data:
|
||||
return None
|
||||
try:
|
||||
if isinstance(data, list):
|
||||
return ', '.join(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f'{__name__} Error: {str(e)}, data = {data}', exc_info=True)
|
||||
return None
|
||||
@@ -1,342 +0,0 @@
|
||||
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, TagsConvertor
|
||||
|
||||
|
||||
class Downloader(Enum):
|
||||
"""
|
||||
下载器枚举
|
||||
"""
|
||||
QB = ('qbittorrent', 'qBittorrent', 'qb', 'QB')
|
||||
TR = ('transmission', 'Transmission', 'tr', 'TR')
|
||||
|
||||
def __init__(self, id: str, name_: str, short_id: str, short_name: str):
|
||||
self.id: str = id
|
||||
self.name_: str = name_
|
||||
self.short_id: str = short_id
|
||||
self.short_name: str = short_name
|
||||
|
||||
|
||||
# Downloader 映射
|
||||
DownloaderMap = dict((d.id, d) for d in Downloader)
|
||||
|
||||
|
||||
class TaskResult:
|
||||
"""
|
||||
任务执行结果
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.__name: str = name
|
||||
self.__success: bool = True
|
||||
self.__total: int = 0
|
||||
self.__seeding: int = 0
|
||||
self.__tagging: int = 0
|
||||
self.__delete: int = 0
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.__name
|
||||
|
||||
def set_success(self, success: bool):
|
||||
self.__success = success
|
||||
return self
|
||||
|
||||
def is_success(self):
|
||||
return self.__success
|
||||
|
||||
def set_total(self, total: int):
|
||||
self.__total = total
|
||||
return self
|
||||
|
||||
def get_total(self):
|
||||
return self.__total
|
||||
|
||||
def set_seeding(self, seeding: int):
|
||||
self.__seeding = seeding
|
||||
return self
|
||||
|
||||
def get_seeding(self):
|
||||
return self.__seeding
|
||||
|
||||
def set_tagging(self, tagging: int):
|
||||
self.__tagging = tagging
|
||||
return self
|
||||
|
||||
def get_tagging(self):
|
||||
return self.__tagging
|
||||
|
||||
def set_delete(self, delete: int):
|
||||
self.__delete = delete
|
||||
return self
|
||||
|
||||
def get_delete(self):
|
||||
return self.__delete
|
||||
|
||||
|
||||
class TaskContext:
|
||||
"""
|
||||
任务上下文
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 选择的下载器集合,为None时表示选择全部
|
||||
self.__selected_downloaders: Optional[Set[str]] = None
|
||||
|
||||
# 启用的子任务
|
||||
# 启用做种
|
||||
self.__enable_seeding: bool = True
|
||||
# 启用打标
|
||||
self.__enable_tagging: bool = True
|
||||
# 启用删种
|
||||
self.__enable_delete: bool = True
|
||||
|
||||
# 选择的种子,为None时表示选择全部
|
||||
# self.__selected_torrents: Set[str] = None
|
||||
self.__selected_torrents = None
|
||||
|
||||
# 源文件删除事件数据
|
||||
self.__download_file_deleted_event_data = None
|
||||
# 下载任务删除事件数据
|
||||
self.__download_deleted_event_data = None
|
||||
|
||||
# 任务结果集
|
||||
self.__results: Optional[List[TaskResult]] = None
|
||||
|
||||
# 操作用户名
|
||||
self.__username: Optional[str] = None
|
||||
|
||||
def select_downloader(self, downloader_id: str):
|
||||
"""
|
||||
选择下载器
|
||||
:param downloader_id: 下载器id
|
||||
"""
|
||||
if not downloader_id:
|
||||
return self
|
||||
if not self.__selected_downloaders:
|
||||
self.__selected_downloaders = set()
|
||||
self.__selected_downloaders.add(downloader_id)
|
||||
return self
|
||||
|
||||
def select_downloaders(self, downloader_ids: List[str]):
|
||||
"""
|
||||
选择下载器
|
||||
:param downloader_ids: 下载器ids
|
||||
"""
|
||||
if not downloader_ids:
|
||||
return self
|
||||
for downloader_id in downloader_ids:
|
||||
self.select_downloader(downloader_id)
|
||||
return self
|
||||
|
||||
def __is_selected_the_downloader(self, downloader_id: str) -> bool:
|
||||
"""
|
||||
是否选择了指定的下载器
|
||||
:param downloader_id: 下载器id
|
||||
:return: 是否选择了指定的下载器
|
||||
"""
|
||||
if not downloader_id:
|
||||
return False
|
||||
return True if self.__selected_downloaders is None or downloader_id in self.__selected_downloaders \
|
||||
else False
|
||||
|
||||
def is_selected_qb_downloader(self) -> bool:
|
||||
"""
|
||||
是否选择了qb下载器
|
||||
:return: 是否选择了qb下载器
|
||||
"""
|
||||
return self.__is_selected_the_downloader(Downloader.QB.id)
|
||||
|
||||
def is_selected_tr_downloader(self) -> bool:
|
||||
"""
|
||||
是否选择了tr下载器
|
||||
:return: 是否选择了tr下载器
|
||||
"""
|
||||
return self.__is_selected_the_downloader(Downloader.TR.id)
|
||||
|
||||
def enable_seeding(self, enable_seeding: bool = True):
|
||||
"""
|
||||
是否启用做种
|
||||
:param enable_seeding: 是否启用做种
|
||||
"""
|
||||
self.__enable_seeding = enable_seeding if enable_seeding else False
|
||||
return self
|
||||
|
||||
def is_enabled_seeding(self) -> bool:
|
||||
"""
|
||||
是否启用了做种
|
||||
:return: 是否启用了做种
|
||||
"""
|
||||
return self.__enable_seeding
|
||||
|
||||
def enable_tagging(self, enable_tagging: bool = True):
|
||||
"""
|
||||
是否启用打标
|
||||
:param enable_tagging: 是否启用打标
|
||||
"""
|
||||
self.__enable_tagging = enable_tagging if enable_tagging else False
|
||||
return self
|
||||
|
||||
def is_enabled_tagging(self) -> bool:
|
||||
"""
|
||||
是否启用了打标
|
||||
:return: 是否启用了打标
|
||||
"""
|
||||
return self.__enable_tagging
|
||||
|
||||
def enable_delete(self, enable_delete: bool = True):
|
||||
"""
|
||||
是否启用删种
|
||||
:param enable_delete: 是否启用删种
|
||||
"""
|
||||
self.__enable_delete = enable_delete if enable_delete else False
|
||||
return self
|
||||
|
||||
def is_enabled_delete(self) -> bool:
|
||||
"""
|
||||
是否启用了删种
|
||||
:return: 是否启用了删种
|
||||
"""
|
||||
return self.__enable_delete
|
||||
|
||||
def select_torrent(self, torrent: str):
|
||||
"""
|
||||
选择种子
|
||||
:param torrent: 种子key
|
||||
"""
|
||||
if not torrent:
|
||||
return self
|
||||
if not self.__selected_torrents:
|
||||
self.__selected_torrents = set()
|
||||
self.__selected_torrents.add(torrent)
|
||||
return self
|
||||
|
||||
def select_torrents(self, torrents: List[str]):
|
||||
"""
|
||||
选择种子
|
||||
:param torrents: 种子keys
|
||||
"""
|
||||
if not torrents:
|
||||
return self
|
||||
for torrent in torrents:
|
||||
self.select_torrent(torrent)
|
||||
return self
|
||||
|
||||
# def get_selected_torrents(self) -> Set[str]:
|
||||
def get_selected_torrents(self):
|
||||
"""
|
||||
获取所有选择的种子
|
||||
"""
|
||||
return self.__selected_torrents
|
||||
|
||||
def set_download_file_deleted_event_data(self, download_file_deleted_event_data: dict):
|
||||
"""
|
||||
设置源文件删除事件数据
|
||||
"""
|
||||
self.__download_file_deleted_event_data = download_file_deleted_event_data
|
||||
return self
|
||||
|
||||
def get_download_file_deleted_event_data(self) -> dict:
|
||||
"""
|
||||
获取源文件删除事件数据
|
||||
"""
|
||||
return self.__download_file_deleted_event_data
|
||||
|
||||
def set_download_deleted_event_data(self, download_deleted_event_data: dict):
|
||||
"""
|
||||
设置下载任务删除事件数据
|
||||
"""
|
||||
self.__download_deleted_event_data = download_deleted_event_data
|
||||
return self
|
||||
|
||||
def get_download_deleted_event_data(self) -> dict:
|
||||
"""
|
||||
获取下载任务删除事件数据
|
||||
"""
|
||||
return self.__download_deleted_event_data
|
||||
|
||||
def save_result(self, result: TaskResult):
|
||||
"""
|
||||
存储结果
|
||||
:param result: 结果
|
||||
"""
|
||||
if not result:
|
||||
return self
|
||||
if not self.__results:
|
||||
self.__results = []
|
||||
self.__results.append(result)
|
||||
return self
|
||||
|
||||
def get_results(self) -> List[TaskResult]:
|
||||
"""
|
||||
获取结果集
|
||||
"""
|
||||
return self.__results
|
||||
|
||||
def set_username(self, username: str):
|
||||
"""
|
||||
设置操作用户名
|
||||
"""
|
||||
self.__username = username
|
||||
return self
|
||||
|
||||
def get_username(self) -> str:
|
||||
"""
|
||||
获取操作用户名
|
||||
"""
|
||||
return self.__username
|
||||
|
||||
|
||||
class TorrentField(Enum):
|
||||
"""
|
||||
种子字段枚举
|
||||
"""
|
||||
NAME = ('名称', 'name', 'name', None)
|
||||
SELECT_SIZE = ('选定大小', 'size', 'sizeWhenDone', ByteSizeConvertor())
|
||||
TOTAL_SIZE = ('总大小', 'total_size', 'totalSize', ByteSizeConvertor())
|
||||
PROGRESS = ('已完成', 'progress', 'percentDone', PercentageConvertor())
|
||||
STATE = ('状态', 'state', 'status', 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', TagsConvertor())
|
||||
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)
|
||||
|
||||
|
||||
class DownloaderTransferInfo():
|
||||
"""
|
||||
下载器传输信息
|
||||
"""
|
||||
|
||||
# 下载速度
|
||||
download_speed: Optional[str] = '0.00B/s'
|
||||
# 上传速度
|
||||
upload_speed: Optional[str] = '0.00B/s'
|
||||
# 下载量
|
||||
download_size: Optional[str] = '0.00B'
|
||||
# 上传量
|
||||
upload_size: Optional[str] = '0.00B'
|
||||
# 剩余空间
|
||||
free_space: Optional[str] = '0.00B'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,743 +0,0 @@
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Event as ThreadEvent, RLock
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
import pytz
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.plugin import PluginHelper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas import NotificationType
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
class PluginAutoUpgrade(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "插件自动升级"
|
||||
# 插件描述
|
||||
plugin_desc = "定时检测、升级插件。"
|
||||
# 插件图标
|
||||
plugin_icon = "PluginAutoUpgrade.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.0"
|
||||
# 插件作者
|
||||
plugin_author = "hotlcc"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/hotlcc"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "com.hotlcc.pluginautoupgrade."
|
||||
# 加载顺序
|
||||
plugin_order = 66
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 私有属性
|
||||
# 调度器
|
||||
__scheduler: Optional[BackgroundScheduler] = None
|
||||
# 退出事件
|
||||
__exit_event: ThreadEvent = ThreadEvent()
|
||||
# 任务锁
|
||||
__task_lock: RLock = RLock()
|
||||
# 插件数据key:升级记录
|
||||
__data_key_upgrade_records = "upgrade_records"
|
||||
|
||||
# 依赖组件
|
||||
# 插件管理器
|
||||
__plugin_manager: PluginManager = PluginManager()
|
||||
|
||||
# 配置相关
|
||||
# 插件缺省配置
|
||||
__config_default: Dict[str, Any] = {
|
||||
'cron': '0,5 0/4 * * *',
|
||||
'save_record_quantity': 100,
|
||||
'display_record_quantity': 10,
|
||||
}
|
||||
# 插件用户配置
|
||||
__config: Dict[str, Any] = {}
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
"""
|
||||
初始化插件
|
||||
"""
|
||||
# 修正配置
|
||||
config = self.__fix_config(config=config)
|
||||
# 加载插件配置
|
||||
self.__config = config
|
||||
# 停止现有服务
|
||||
self.stop_service()
|
||||
# 如果需要立即运行一次
|
||||
if self.__get_config_item(config_key='run_once'):
|
||||
if self.__start_scheduler():
|
||||
self.__scheduler.add_job(func=self.__try_run,
|
||||
trigger='date',
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
|
||||
name=f'{self.plugin_name}-立即运行一次')
|
||||
logger.info(f"立即运行一次成功")
|
||||
# 关闭一次性开关
|
||||
self.__config['run_once'] = False
|
||||
self.update_config(self.__config)
|
||||
|
||||
def get_state(self) -> bool:
|
||||
"""
|
||||
获取插件状态
|
||||
"""
|
||||
state = True if self.__get_config_item(config_key='enable') and self.__get_config_item(config_key='cron') \
|
||||
else False
|
||||
return state
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
定义远程控制命令
|
||||
:return: 命令关键字、事件、描述、附带数据
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
注册插件公共服务
|
||||
"""
|
||||
try:
|
||||
if self.get_state():
|
||||
cron = self.__get_config_item(config_key='cron')
|
||||
return [{
|
||||
"id": "PluginAutoUpgradeTimerService",
|
||||
"name": f"{self.plugin_name}定时服务",
|
||||
"trigger": CronTrigger.from_crontab(cron),
|
||||
"func": self.__try_run,
|
||||
"kwargs": {}
|
||||
}]
|
||||
else:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"注册插件公共服务异常: {str(e)}", exc_info=True)
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
# 建议的配置
|
||||
config_suggest = {}
|
||||
# 合并默认配置
|
||||
config_suggest.update(self.__config_default)
|
||||
# 定时周期
|
||||
cron = self.__config_default.get('cron')
|
||||
# 保存记录数量
|
||||
save_record_quantity = self.__config_default.get('save_record_quantity')
|
||||
# 展示记录数量
|
||||
display_record_quantity = self.__config_default.get('display_record_quantity')
|
||||
# 已安装的在线插件下拉框数据
|
||||
installed_online_plugin_options = self.__get_installed_online_plugin_options()
|
||||
form = [{
|
||||
'component': 'VForm',
|
||||
'content': [{ # 业务无关总控
|
||||
'component': 'VRow',
|
||||
'content': [{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enable',
|
||||
'label': '启用插件',
|
||||
'hint': '插件总开关'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enable_notify',
|
||||
'label': '发送通知',
|
||||
'hint': '执行插件任务后是否发送通知'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'run_once',
|
||||
'label': '立即运行一次',
|
||||
'hint': '保存插件配置后是否立即触发一次插件任务运行'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
'component': 'VRow',
|
||||
'content': [{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '定时执行周期',
|
||||
'placeholder': cron,
|
||||
'hint': f'设置插件任务执行周期。支持5位cron表达式,应避免任务执行过于频繁,缺省时为:【{cron}】'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'save_record_quantity',
|
||||
'label': '保存记录数量',
|
||||
'type': 'number',
|
||||
'placeholder': save_record_quantity,
|
||||
'hint': f'设置插件最多保存多少条插件升级记录。缺省时为{save_record_quantity}。'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'display_record_quantity',
|
||||
'label': '展示记录数量',
|
||||
'type': 'number',
|
||||
'placeholder': display_record_quantity,
|
||||
'hint': f'设置插件数据页最多展示多少条插件升级记录。缺省时为{display_record_quantity}。'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
'component': 'VRow',
|
||||
'content': [{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'include_plugins',
|
||||
'label': '包含的插件',
|
||||
'multiple': True,
|
||||
'chips': True,
|
||||
'items': installed_online_plugin_options,
|
||||
'hint': '选择哪些插件需要自动升级,不选时默认全部已安装插件。'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12
|
||||
},
|
||||
'content': [{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'model': 'exclude_plugins',
|
||||
'label': '排除的插件',
|
||||
'multiple': True,
|
||||
'chips': True,
|
||||
'items': installed_online_plugin_options,
|
||||
'hint': '选择哪些插件需要排除升级(在【包含的插件】的基础上排除),不选时默认不排除。'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
return form, config_suggest
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
page_data = self.__get_upgrade_records_to_page_data()
|
||||
if page_data:
|
||||
contents = [{
|
||||
'component': 'tr',
|
||||
'props': {
|
||||
'class': 'text-sm'
|
||||
},
|
||||
'content': [{
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'class': 'whitespace-nowrap'
|
||||
},
|
||||
'text': item.get('datetime_str')
|
||||
}, {
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'class': 'whitespace-nowrap'
|
||||
},
|
||||
'text': item.get('plugin_name')
|
||||
}, {
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'class': 'whitespace-nowrap'
|
||||
},
|
||||
'text': f'v{item.get("old_plugin_version")}'
|
||||
}, {
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'class': 'whitespace-nowrap'
|
||||
},
|
||||
'text': f'v{item.get("new_plugin_version")}'
|
||||
}, {
|
||||
'component': 'td',
|
||||
'text': item.get('info')
|
||||
}, {
|
||||
'component': 'td',
|
||||
'text': item.get('upgrade_info')
|
||||
}]
|
||||
} for item in page_data if item]
|
||||
else:
|
||||
contents = [{
|
||||
'component': 'tr',
|
||||
'props': {
|
||||
'class': 'text-sm'
|
||||
},
|
||||
'content': [{
|
||||
'component': 'td',
|
||||
'props': {
|
||||
'colspan': '6',
|
||||
'class': 'text-center'
|
||||
},
|
||||
'text': '暂无数据'
|
||||
}]
|
||||
}]
|
||||
return [{
|
||||
'component': 'VTable',
|
||||
'props': {
|
||||
'hover': True
|
||||
},
|
||||
'content': [{
|
||||
'component': 'thead',
|
||||
'content': [{
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '时间'
|
||||
}, {
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '插件名称'
|
||||
}, {
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '旧版本'
|
||||
}, {
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '新版本'
|
||||
}, {
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '执行结果'
|
||||
}, {
|
||||
'component': 'th',
|
||||
'props': {
|
||||
'class': 'text-start ps-4'
|
||||
},
|
||||
'text': '升级描述'
|
||||
}]
|
||||
}, {
|
||||
'component': 'tbody',
|
||||
'content': contents
|
||||
}]
|
||||
}]
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
logger.info('尝试停止插件服务...')
|
||||
self.__exit_event.set()
|
||||
self.__stop_scheduler()
|
||||
logger.info('插件服务停止成功')
|
||||
except Exception as e:
|
||||
logger.error(f"插件服务停止异常: {str(e)}", exc_info=True)
|
||||
finally:
|
||||
self.__exit_event.clear()
|
||||
|
||||
def __fix_config(self, config: dict) -> dict:
|
||||
"""
|
||||
修正配置
|
||||
"""
|
||||
if not config:
|
||||
return None
|
||||
# 忽略主程序在reset时赋予的内容
|
||||
reset_config = {
|
||||
"enabled": False,
|
||||
"enable": False
|
||||
}
|
||||
if config == reset_config:
|
||||
return None
|
||||
|
||||
config_keys = config.keys()
|
||||
if 'save_record_quantity' in config_keys:
|
||||
save_record_quantity = config.get("save_record_quantity")
|
||||
config['save_record_quantity'] = int(save_record_quantity) if save_record_quantity else None
|
||||
if 'display_record_quantity' in config_keys:
|
||||
display_record_quantity = config.get("display_record_quantity")
|
||||
config['display_record_quantity'] = int(display_record_quantity) if display_record_quantity else None
|
||||
self.update_config(config=config)
|
||||
return config
|
||||
|
||||
def __get_config_item(self, config_key: str, use_default: bool = True) -> Any:
|
||||
"""
|
||||
获取插件配置项
|
||||
:param config_key: 配置键
|
||||
:param use_default: 是否使用缺省值
|
||||
:return: 配置值
|
||||
"""
|
||||
if not config_key:
|
||||
return None
|
||||
config = self.__config if self.__config else {}
|
||||
config_value = config.get(config_key)
|
||||
if (config_value is None or config_value == '') and use_default:
|
||||
config_default = self.__config_default if self.__config_default else {}
|
||||
config_value = config_default.get(config_key)
|
||||
return config_value
|
||||
|
||||
@classmethod
|
||||
def __get_local_plugins(cls) -> List[schemas.Plugin]:
|
||||
"""
|
||||
获取所有本地插件信息
|
||||
"""
|
||||
local_plugins = cls.__plugin_manager.get_local_plugins()
|
||||
return local_plugins
|
||||
|
||||
@classmethod
|
||||
def __get_installed_local_plugins(cls) -> List[schemas.Plugin]:
|
||||
"""
|
||||
获取所有已安装的本地插件信息
|
||||
"""
|
||||
local_plugins = cls.__get_local_plugins()
|
||||
installed_local_plugins = [local_plugin for local_plugin in local_plugins if
|
||||
local_plugin and local_plugin.installed]
|
||||
return installed_local_plugins
|
||||
|
||||
@classmethod
|
||||
def __get_installed_local_plugin(cls, plugin_id: str) -> Optional[schemas.Plugin]:
|
||||
"""
|
||||
获取指定的已安装的本地插件信息
|
||||
"""
|
||||
if not plugin_id:
|
||||
return None
|
||||
# 已安装的本地插件
|
||||
installed_plugins = cls.__get_installed_local_plugins()
|
||||
for installed_plugin in installed_plugins:
|
||||
if installed_plugin and installed_plugin.id and installed_plugin.id == plugin_id:
|
||||
return installed_plugin
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def __get_online_plugins(cls) -> List[schemas.Plugin]:
|
||||
"""
|
||||
获取所有在线插件
|
||||
"""
|
||||
online_plugins = cls.__plugin_manager.get_online_plugins()
|
||||
return online_plugins
|
||||
|
||||
@classmethod
|
||||
def __get_installed_online_plugins(cls) -> List[schemas.Plugin]:
|
||||
"""
|
||||
获取所有已安装的在线插件
|
||||
"""
|
||||
online_plugins = cls.__get_online_plugins()
|
||||
installed_online_plugins = [online_plugin for online_plugin in online_plugins if
|
||||
online_plugin and online_plugin.installed]
|
||||
return installed_online_plugins
|
||||
|
||||
@classmethod
|
||||
def __get_installed_online_plugin_options(cls) -> list:
|
||||
"""
|
||||
获取所有已安装的在线插件的选项数据
|
||||
"""
|
||||
installed_online_plugin_options = []
|
||||
installed_online_plugins = cls.__get_installed_online_plugins()
|
||||
for installed_online_plugin in installed_online_plugins:
|
||||
if not installed_online_plugin:
|
||||
continue
|
||||
installed_online_plugin_options.append({
|
||||
'value': installed_online_plugin.id,
|
||||
'title': installed_online_plugin.plugin_name
|
||||
})
|
||||
return installed_online_plugin_options
|
||||
|
||||
@classmethod
|
||||
def __get_has_update_online_plugins(cls) -> Optional[List[schemas.Plugin]]:
|
||||
"""
|
||||
获取所有可升级的在线插件
|
||||
"""
|
||||
installed_online_plugins = cls.__get_installed_online_plugins()
|
||||
if not installed_online_plugins:
|
||||
return None
|
||||
has_update_online_plugins = [installed_online_plugin for installed_online_plugin in installed_online_plugins if installed_online_plugin and installed_online_plugin.has_update]
|
||||
return has_update_online_plugins
|
||||
|
||||
def __start_scheduler(self, timezone=None) -> bool:
|
||||
"""
|
||||
启动调度器
|
||||
:param timezone: 时区
|
||||
"""
|
||||
try:
|
||||
if not self.__scheduler:
|
||||
if not timezone:
|
||||
timezone = settings.TZ
|
||||
self.__scheduler = BackgroundScheduler(timezone=timezone)
|
||||
logger.debug(f"插件服务调度器初始化完成: timezone = {str(timezone)}")
|
||||
if not self.__scheduler.running:
|
||||
self.__scheduler.start()
|
||||
logger.debug(f"插件服务调度器启动成功")
|
||||
self.__scheduler.print_jobs()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"插件服务调度器启动异常: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
def __stop_scheduler(self):
|
||||
"""
|
||||
停止调度器
|
||||
"""
|
||||
try:
|
||||
logger.info('尝试停止插件服务调度器...')
|
||||
if self.__scheduler:
|
||||
self.__scheduler.remove_all_jobs()
|
||||
if self.__scheduler.running:
|
||||
self.__scheduler.shutdown()
|
||||
self.__scheduler = None
|
||||
logger.info('插件服务调度器停止成功')
|
||||
else:
|
||||
logger.info('插件未启用服务调度器,无须停止')
|
||||
except Exception as e:
|
||||
logger.error(f"插件服务调度器停止异常: {str(e)}", exc_info=True)
|
||||
|
||||
def __check_allow_upgrade(self, plugin_id: str) -> bool:
|
||||
"""
|
||||
判断插件是否允许升级:包含、排除
|
||||
"""
|
||||
if not plugin_id:
|
||||
return False
|
||||
exclude_plugins = self.__get_config_item('exclude_plugins')
|
||||
if exclude_plugins and plugin_id in exclude_plugins:
|
||||
return False
|
||||
include_plugins = self.__get_config_item('include_plugins')
|
||||
if not include_plugins or plugin_id in include_plugins:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __install_plugin(plugin_id: str, repo_url: str = "", force: bool = False) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
安装插件,参考:app.api.endpoints.plugin.install
|
||||
:param plugin_id: 插件ID
|
||||
:param repo_url: 插件仓库URL
|
||||
:param force: 是否强制安装
|
||||
"""
|
||||
# 已安装插件
|
||||
install_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
# 如果是非本地括件,或者强制安装时,则需要下载安装
|
||||
if repo_url and (force or plugin_id not in PluginManager().get_plugin_ids()):
|
||||
# 下载安装
|
||||
state, msg = PluginHelper().install(pid=plugin_id, repo_url=repo_url)
|
||||
if not state:
|
||||
# 安装失败
|
||||
return False, msg
|
||||
# 安装插件
|
||||
if plugin_id not in install_plugins:
|
||||
install_plugins.append(plugin_id)
|
||||
# 保存设置
|
||||
SystemConfigOper().set(SystemConfigKey.UserInstalledPlugins, install_plugins)
|
||||
# 加载插件到内存
|
||||
PluginManager().reload_plugin(plugin_id)
|
||||
# 注册插件服务
|
||||
Scheduler().update_plugin_job(plugin_id)
|
||||
return True, None
|
||||
|
||||
def __try_run(self):
|
||||
"""
|
||||
尝试运行插件任务
|
||||
"""
|
||||
if not self.__task_lock.acquire(blocking=False):
|
||||
logger.info('已有进行中的任务,本次不执行')
|
||||
return
|
||||
try:
|
||||
self.__run()
|
||||
finally:
|
||||
self.__task_lock.release()
|
||||
|
||||
def __run(self):
|
||||
""""
|
||||
运行插件任务
|
||||
"""
|
||||
self.__upgrade_batch()
|
||||
|
||||
def __upgrade_batch(self):
|
||||
"""
|
||||
批量升级
|
||||
"""
|
||||
has_update_online_plugins = self.__get_has_update_online_plugins()
|
||||
if not has_update_online_plugins:
|
||||
return
|
||||
upgrade_results = []
|
||||
for has_update_online_plugin in has_update_online_plugins:
|
||||
upgrade_result = self.__upgrade_single(has_update_online_plugin)
|
||||
if upgrade_result:
|
||||
upgrade_results.append(upgrade_result)
|
||||
# 保存升级记录
|
||||
self.__save_upgrade_records(records=upgrade_results)
|
||||
# 发送通知
|
||||
self.__send_notify(results=upgrade_results)
|
||||
|
||||
def __upgrade_single(self, online_plugin: schemas.Plugin) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
单个升级
|
||||
"""
|
||||
if not online_plugin or not online_plugin.has_update or not online_plugin.id or not online_plugin.repo_url or not self.__check_allow_upgrade(
|
||||
plugin_id=online_plugin.id):
|
||||
return None
|
||||
installed_local_plugin = self.__get_installed_local_plugin(plugin_id=online_plugin.id)
|
||||
if not installed_local_plugin:
|
||||
return None
|
||||
success, message = self.__install_plugin(plugin_id=online_plugin.id, repo_url=online_plugin.repo_url,
|
||||
force=True)
|
||||
logger.info(
|
||||
f"插件升级结果: plugin_name = {online_plugin.plugin_name}, plugin_version = v{installed_local_plugin.plugin_version} -> v{online_plugin.plugin_version}, success = {success}, message = {message}")
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'plugin_id': online_plugin.id,
|
||||
'plugin_name': online_plugin.plugin_name,
|
||||
'new_plugin_version': online_plugin.plugin_version,
|
||||
'old_plugin_version': installed_local_plugin.plugin_version,
|
||||
'datetime_str': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'upgrade_info': self.__extract_upgrade_history(online_plugin)
|
||||
}
|
||||
|
||||
def __send_notify(self, results: List[Dict[str, Any]]):
|
||||
"""
|
||||
发送通知
|
||||
:param results: 插件升级结果
|
||||
"""
|
||||
if not results or not self.__get_config_item('enable_notify'):
|
||||
return
|
||||
text = self.__build_notify_message(results=results)
|
||||
if not text:
|
||||
return
|
||||
self.post_message(title=f'{self.plugin_name}任务执行结果', text=text, mtype=NotificationType.Plugin)
|
||||
|
||||
@staticmethod
|
||||
def __build_notify_message(results: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
构建通知消息内容
|
||||
"""
|
||||
text = ''
|
||||
if not results:
|
||||
return text
|
||||
for result in results:
|
||||
if not result:
|
||||
continue
|
||||
text += f"【{result.get('plugin_name')}】[v{result.get('old_plugin_version')} -> v{result.get('new_plugin_version')}]:"
|
||||
if result.get('success'):
|
||||
text += f"成功\n"
|
||||
else:
|
||||
text += f"{result.get('message')}\n"
|
||||
return text
|
||||
|
||||
def __save_upgrade_records(self, records: List[Dict[str, Any]]):
|
||||
"""
|
||||
保存升级记录
|
||||
"""
|
||||
if not records:
|
||||
return
|
||||
upgrade_records = self.get_data(self.__data_key_upgrade_records)
|
||||
if not upgrade_records:
|
||||
upgrade_records = []
|
||||
upgrade_records.extend(records)
|
||||
# 最多保存多少条
|
||||
save_record_quantity = self.__get_config_item('save_record_quantity')
|
||||
upgrade_records = upgrade_records[-save_record_quantity:]
|
||||
self.save_data(self.__data_key_upgrade_records, upgrade_records)
|
||||
|
||||
@staticmethod
|
||||
def __convert_upgrade_record_to_page_data(upgrade_record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
if not upgrade_record:
|
||||
return None
|
||||
info = "成功" if upgrade_record.get("success") else upgrade_record.get("message")
|
||||
upgrade_record.update({"info": info})
|
||||
return upgrade_record
|
||||
|
||||
def __get_upgrade_records_to_page_data(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取升级记录为page数据
|
||||
"""
|
||||
upgrade_records = self.get_data(self.__data_key_upgrade_records)
|
||||
if not upgrade_records:
|
||||
return []
|
||||
# 只展示最近多少条
|
||||
display_record_quantity = self.__get_config_item('display_record_quantity')
|
||||
upgrade_records = upgrade_records[-display_record_quantity:]
|
||||
page_data = [self.__convert_upgrade_record_to_page_data(upgrade_record) for upgrade_record in upgrade_records if
|
||||
upgrade_record]
|
||||
# 按时间倒序
|
||||
page_data = sorted(page_data, key=lambda item: item.get("datetime_str"), reverse=True)
|
||||
return page_data
|
||||
|
||||
@staticmethod
|
||||
def __extract_upgrade_history(plugin: schemas.Plugin, version: str = None) -> Optional[str]:
|
||||
"""
|
||||
提取指定版本的升级历史信息
|
||||
"""
|
||||
if not plugin or not plugin.history:
|
||||
return None
|
||||
if not version:
|
||||
version = plugin.plugin_version
|
||||
if not version:
|
||||
return None
|
||||
version_history = plugin.history.get(f'v{version}')
|
||||
if not version_history:
|
||||
# 兼容处理
|
||||
version_history = plugin.history.get(version)
|
||||
return version_history
|
||||
Reference in New Issue
Block a user