移除已独立维护的插件,新仓库:https://github.com/hotlcc/MoviePilot-Plugins-Third/

This commit is contained in:
jxxghp
2024-06-11 21:30:30 +08:00
parent b3a832e106
commit f3460c73f3
7 changed files with 0 additions and 5419 deletions

View File

@@ -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的登录壁纸下载到本地。",

View File

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

View File

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

View File

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

View File

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