From f3460c73f329c29bc33fbfe2a43b7c1571635953 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 11 Jun 2024 21:30:30 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=B7=B2=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=BB=B4=E6=8A=A4=E7=9A=84=E6=8F=92=E4=BB=B6=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E4=BB=93=E5=BA=93=EF=BC=9Ahttps://github.com/hotlcc/MoviePilot?= =?UTF-8?q?-Plugins-Third/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 53 - plugins/downloaderhelper/README.md | 71 - plugins/downloaderhelper/__init__.py | 2997 ------------------------- plugins/downloaderhelper/convertor.py | 206 -- plugins/downloaderhelper/module.py | 342 --- plugins/mergesiteswitch/__init__.py | 1007 --------- plugins/pluginautoupgrade/__init__.py | 743 ------ 7 files changed, 5419 deletions(-) delete mode 100644 plugins/downloaderhelper/README.md delete mode 100644 plugins/downloaderhelper/__init__.py delete mode 100644 plugins/downloaderhelper/convertor.py delete mode 100644 plugins/downloaderhelper/module.py delete mode 100644 plugins/mergesiteswitch/__init__.py delete mode 100644 plugins/pluginautoupgrade/__init__.py diff --git a/package.json b/package.json index 7a8064f..1b6d7ba 100644 --- a/package.json +++ b/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的登录壁纸下载到本地。", diff --git a/plugins/downloaderhelper/README.md b/plugins/downloaderhelper/README.md deleted file mode 100644 index e171836..0000000 --- a/plugins/downloaderhelper/README.md +++ /dev/null @@ -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 - -(待补充) diff --git a/plugins/downloaderhelper/__init__.py b/plugins/downloaderhelper/__init__.py deleted file mode 100644 index 9a5af12..0000000 --- a/plugins/downloaderhelper/__init__.py +++ /dev/null @@ -1,2997 +0,0 @@ -import os -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, Union -from urllib.parse import urlparse - -import pytz -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.cron import CronTrigger -from cachetools import TTLCache -from qbittorrentapi import TorrentDictionary, TorrentState -from transmission_rpc.torrent import Torrent, Status as TorrentStatus -from ruamel.yaml.comments import CommentedMap - -from app.core.config import settings -from app.core.event import eventmanager, Event -from app.core.module import ModuleManager -from app.helper.sites import SitesHelper -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, TorrentField, TorrentFieldMap, DownloaderMap, DownloaderTransferInfo -from app.schemas import NotificationType -from app.schemas.types import EventType -from app.utils.string import StringUtils - - -class DownloaderHelper(_PluginBase): - # 插件名称 - plugin_name = "下载器助手" - # 插件描述 - plugin_desc = "自动标签、自动做种、自动删种。" - # 插件图标 - plugin_icon = "DownloaderHelper.png" - # 插件版本 - plugin_version = "2.8" - # 插件作者 - plugin_author = "hotlcc" - # 作者主页 - author_url = "https://github.com/hotlcc" - # 插件配置项ID前缀 - plugin_config_prefix = "com.hotlcc.downloaderhelper." - # 加载顺序 - plugin_order = 66 - # 可使用的用户级别 - auth_level = 1 - - # 插件说明链接 - __help_url = 'https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/downloaderhelper' - - # 私有属性 - # 调度器 - __scheduler: Optional[BackgroundScheduler] = None - # 退出事件 - __exit_event: ThreadEvent = ThreadEvent() - # 任务锁 - __task_lock: RLock = RLock() - # 缓存 - __ttl_cache = TTLCache(maxsize=128, ttl=1800) - - # 配置相关 - # 插件缺省配置 - __config_default: Dict[str, Any] = { - 'site_name_priority': True, - 'tag_prefix': '站点/', - 'dashboard_widget_size': 12, - 'dashboard_widget_target_downloaders': ['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, - ], - 'dashboard_speed_widget_target_downloaders': ['default'], - } - # 插件用户配置 - __config: Dict[str, Any] = {} - # 缺省traker映射 - __tracker_mappings_default: Dict[str, str] = { - 'chdbits.xyz': 'ptchdbits.co', - 'agsvpt.trackers.work': 'agsvpt.com', - 'tracker.cinefiles.info': 'audiences.me' - } - # 用户配置的tracker映射 - __tracker_mappings: Dict[str, str] = {} - # 排除种子标签 - __exclude_tags: Set[str] = set() - # 多级根域名,用于在打标时做特殊处理 - __multi_level_root_domain: List[str] = ['edu.cn', 'com.cn', 'net.cn', 'org.cn'] - # vuetifyjs mdi 图标 svg path 值 - __mdi_icon_svg_path = { - 'mdi-cloud-upload': 'M11 20H6.5q-2.28 0-3.89-1.57Q1 16.85 1 14.58q0-1.95 1.17-3.48q1.18-1.53 3.08-1.95q.63-2.3 2.5-3.72Q9.63 4 12 4q2.93 0 4.96 2.04Q19 8.07 19 11q1.73.2 2.86 1.5q1.14 1.28 1.14 3q0 1.88-1.31 3.19T18.5 20H13v-7.15l1.6 1.55L16 13l-4-4l-4 4l1.4 1.4l1.6-1.55Z', - 'mdi-download-box': 'M5 3h14a2 2 0 0 1 2 2v14c0 1.11-.89 2-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2m3 14h8v-2H8zm8-7h-2.5V7h-3v3H8l4 4z', - 'mdi-content-save': 'M15 9H5V5h10m-3 14a3 3 0 0 1-3-3a3 3 0 0 1 3-3a3 3 0 0 1 3 3a3 3 0 0 1-3 3m5-16H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7z', - } - # 公共 tracker url - __public_tracker_urls = [ - "** [DHT] **", - "** [PeX] **", - "** [LSD] **", - ] - - def init_plugin(self, config: dict = None): - """ - 初始化插件 - """ - # 停止现有服务 - self.stop_service() - - # 检查环境 - self.__check_environment() - - # 修正配置 - config = self.__fix_config(config=config) - # 加载插件配置 - self.__config = config - # 解析tracker映射 - tracker_mappings = self.__get_config_item(config_key='tracker_mappings') - self.__tracker_mappings = self.__parse_tracker_mappings(tracker_mappings=tracker_mappings) - # 解析排除种子标签 - exclude_tags = self.__get_config_item(config_key='exclude_tags') - self.__exclude_tags = self.__split_tags(tags=exclude_tags) - logger.info(f"插件配置加载完成:{config}") - - # 如果需要立即运行一次 - if self.__get_config_item(config_key='run_once'): - if self.__check_enable_any_task(): - 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='下载器助手任务-立即运行一次') - logger.info(f"立即运行一次成功") - else: - logger.warn(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') - or self.__check_enable_listen() - ) - and self.__check_enable_any_task() - ) - or self.__check_enable_dashboard_active_torrent_widget() - or self.__check_enable_dashboard_speed_widget() - ) 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: - cron = self.__get_config_item(config_key='cron') - if self.get_state() and cron: - return [{ - "id": "DownloaderHelperTimerService", - "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 = { - 'listen_download_event': True, - 'listen_source_file_event': True, - 'cron': '0/30 * * * *', - 'exclude_tags': 'BT,刷流', - 'dashboard_widget_refresh': 5, - 'dashboard_speed_widget_refresh': 5, - } - # 合并默认配置 - config_suggest.update(self.__config_default) - # 下载器tabs - downloader_tabs = [{ - 'component': 'VTab', - 'props': { - 'value': d.id - }, - 'text': d.name_ - } for d in Downloader if d] - # 下载器tab items - downloader_tab_items = [{ - 'component': 'VWindowItem', - 'props': { - 'value': d.id - }, - 'content': [{ - 'component': 'VRow', - 'props': { - 'style': { - 'margin-top': '0' - } - }, - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': f'{d.short_id}_enable', - 'label': '任务开关', - 'hint': '该下载器子任务的开关' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': f'{d.short_id}_enable_tagging', - 'label': '自动标签', - 'hint': '是否开启自动标签功能;包含BT/PT标签和站点标签(仅PT有效);不受【排除种子标签】限制。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': f'{d.short_id}_enable_seeding', - 'label': '自动做种', - 'hint': '是否开启自动做种功能;受【排除种子标签】限制。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': f'{d.short_id}_enable_delete', - 'label': '自动删种', - 'hint': '是否开启自动删种功能;受【排除种子标签】限制。' - } - }] - }] - }] - } 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', - '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': 'VSwitch', - 'props': { - 'model': 'listen_download_event', - 'label': '监听下载事件', - 'hint': '监听下载添加事件。当MoviePilot添加下载任务时,会触发本插件进行自动标签和自动做种。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'listen_source_file_event', - 'label': '监听源文件事件', - 'hint': '监听源文件删除事件以及原种删除事件。当在【历史记录】中删除源文件时,会触发本插件进行自动删种。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'site_name_priority', - 'label': '站点名称优先', - 'hint': '给种子添加站点标签时,是否优先以站点名称作为标签内容(否则将使用域名关键字)?MoviePilot需要认证,否则将不生效。' - } - }] - }] - }, { - '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': '0/30 * * * *', - 'hint': '设置插件任务执行周期。支持5位cron表达式,应避免任务执行过于频繁,例如:0/30 * * * *。缺省时不执行定时任务,但不影响监听任务的执行。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VTextField', - 'props': { - 'model': 'exclude_tags', - 'label': '排除种子标签', - 'hint': '下载器中的种子有这些标签时不进行任何操作,多个标签使用英文“,”分割' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VTextField', - 'props': { - 'model': 'tag_prefix', - 'label': '站点标签前缀', - 'placeholder': '站点/', - 'hint': '给种子添加站点标签时的标签前缀,默认值为“站点/”' - } - }] - }] - }, { - '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': '_config_tracker_mappings_dialog_closed', - 'label': '配置Tracker映射', - '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_active_torrent_dialog_closed', - 'label': '配置仪表板活动种子组件', - 'hint': '点击展开仪表板活动种子组件配置窗口。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': '_config_dashboard_speed_dialog_closed', - 'label': '配置仪表板实时速率组件', - 'hint': '点击展开仪表板实时速率组件配置窗口。' - } - }] - }] - }, { - 'component': 'VDialog', - 'props': { - 'model': '_config_tracker_mappings_dialog_closed', - 'max-width': '40rem' - }, - 'content': [{ - 'component': 'VCard', - 'props': { - 'title': '配置Tracker映射', - 'style': { - 'padding': '0 20px 20px 20px' - } - }, - 'content': [{ - 'component': 'VDialogCloseBtn', - 'props': { - 'model': '_config_tracker_mappings_dialog_closed' - } - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12 - }, - 'content': [{ - 'component': 'VTextarea', - 'props': { - 'model': 'tracker_mappings', - 'label': 'Tracker映射', - 'placeholder': '格式:\n' - ':\n\n' - '例如:\n' - 'chdbits.xyz:ptchdbits.co', - 'hint': 'Tracker映射。用于在站点标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' - } - }] - }] - }] - }] - }, { - 'component': 'VDialog', - 'props': { - 'model': '_config_dashboard_active_torrent_dialog_closed', - 'max-width': '40rem' - }, - 'content': [{ - 'component': 'VCard', - 'props': { - 'title': '配置仪表板活动种子组件', - 'style': { - 'padding': '0 20px 20px 20px' - } - }, - 'content': [{ - 'component': 'VDialogCloseBtn', - 'props': { - 'model': '_config_dashboard_active_torrent_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_downloaders', - 'label': '目标下载器', - 'multiple': True, - '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': 'VDialog', - 'props': { - 'model': '_config_dashboard_speed_dialog_closed', - 'max-width': '40rem' - }, - 'content': [{ - 'component': 'VCard', - 'props': { - 'title': '配置仪表板实时速率组件', - 'style': { - 'padding': '0 20px 20px 20px' - } - }, - 'content': [{ - 'component': 'VDialogCloseBtn', - 'props': { - 'model': '_config_dashboard_speed_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_speed_widget', - 'label': '启用组件', - 'hint': '是否启用仪表板实时速率组件。' - } - }] - }] - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VTextField', - 'props': { - 'model': 'dashboard_speed_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_speed_widget_target_downloaders', - 'label': '目标下载器', - 'multiple': True, - 'items': [ - {'title': '系统默认下载器', 'value': 'default'}, - {'title': Downloader.QB.name_, 'value': Downloader.QB.id}, - {'title': Downloader.TR.name_, 'value': Downloader.TR.id} - ], - 'hint': '选择要展示的目标下载器。' - } - }] - }] - }] - }] - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12 - }, - 'content': [{ - 'component': 'VTabs', - 'props': { - 'model': '_tabs', - 'height': 72, - 'style': { - 'margin-top-': '20px', - 'margin-bottom-': '20px' - } - }, - 'content': downloader_tabs - }, { - 'component': 'VWindow', - 'props': { - 'model': '_tabs' - }, - 'content': downloader_tab_items - }] - }] - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'style': { - 'margin-top': '20px' - } - }, - 'content': [{ - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal' - }, - 'content': [{ - 'component': 'a', - 'props': { - 'href': self.__help_url, - 'target': '_blank' - }, - 'text': '点此查看详细的插件使用说明' - }] - }] - }] - }] - }], config_suggest - - def get_page(self) -> List[dict]: - pass - - def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]: - """ - 获取插件仪表盘元信息 - 返回示例: - [{ - "key": "dashboard1", // 仪表盘的key,在当前插件范围唯一 - "name": "仪表盘1" // 仪表盘的名称 - }, { - "key": "dashboard2", - "name": "仪表盘2" - }] - """ - dashboard_meta = [] - if not self.get_state(): - return dashboard_meta - if self.__check_enable_dashboard_active_torrent_widget(): - target_downloader_ids = self.__get_dashboard_active_torrent_widget_target_downloader_ids() - for target_downloader_id in target_downloader_ids: - downloader = self.__get_downloader_enum_by_id(downloader_id=target_downloader_id) - if not downloader: - continue - dashboard_meta.append({ - "key": downloader.id, - "name": f"活动种子 #{downloader.short_name}", - }) - if self.__check_enable_dashboard_speed_widget(): - target_downloader_ids = self.__get_dashboard_speed_widget_target_downloader_ids() - for target_downloader_id in target_downloader_ids: - downloader = self.__get_downloader_enum_by_id(downloader_id=target_downloader_id) - if not downloader: - continue - dashboard_meta.append({ - "key": f"{downloader.id}_speed", - "name": f"实时速率 #{downloader.short_name}", - }) - return dashboard_meta - - def get_dashboard(self, key: str = None, **kwargs) -> 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/ - - kwargs参数可获取的值:1、user_agent:浏览器UA - - :param key: 仪表盘key,根据指定的key返回相应的仪表盘数据,缺省时返回一个固定的仪表盘数据(兼容旧版) - """ - if not self.get_state(): - return None - enable_dashboard_active_torrent_widget = self.__check_enable_dashboard_active_torrent_widget() - enable_dashboard_speed_widget = self.__check_enable_dashboard_speed_widget() - if not enable_dashboard_active_torrent_widget and not enable_dashboard_speed_widget: - return None - # 无key兼容历史 - dashboard_active_torrent_widget_target_downloader_ids = self.__get_dashboard_active_torrent_widget_target_downloader_ids() - if not key: - if enable_dashboard_active_torrent_widget and dashboard_active_torrent_widget_target_downloader_ids: - return self.__get_dashboard_active_torrent_widget(downloader_id=dashboard_active_torrent_widget_target_downloader_ids[0]) - else: - return None - # 有key - dashboard_speed_widget_target_downloader_ids = self.__get_dashboard_speed_widget_target_downloader_ids() - if key == Downloader.QB.id and enable_dashboard_active_torrent_widget and Downloader.QB.id in dashboard_active_torrent_widget_target_downloader_ids: - return self.__get_dashboard_active_torrent_widget(downloader_id=Downloader.QB.id) - if key == Downloader.TR.id and enable_dashboard_active_torrent_widget and Downloader.TR.id in dashboard_active_torrent_widget_target_downloader_ids: - return self.__get_dashboard_active_torrent_widget(downloader_id=Downloader.TR.id) - if key == f"{Downloader.QB.id}_speed" and enable_dashboard_speed_widget and Downloader.QB.id in dashboard_speed_widget_target_downloader_ids: - return self.__get_dashboard_speed_widget(downloader_id=Downloader.QB.id) - if key == f"{Downloader.TR.id}_speed" and enable_dashboard_speed_widget and Downloader.TR.id in dashboard_speed_widget_target_downloader_ids: - return self.__get_dashboard_speed_widget(downloader_id=Downloader.TR.id) - return None - - def __get_dashboard_active_torrent_widget(self, - downloader_id: str) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: - """ - 获取仪表板活动种子组件 - """ - downloader = self.__get_downloader_enum_by_id(downloader_id=downloader_id) - if not downloader: - return None - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - 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 = { - 'title': f'活动种子 #{downloader.short_name}' - } - if self.__check_target_downloader(downloader_id=downloader_id): - attrs['refresh'] = self.__get_config_item('dashboard_widget_refresh') - - # 页面元素 - elements = self.__get_dashboard_active_torrent_widget_elememts(downloader_id=downloader_id) - - return cols, attrs, elements - - def __get_dashboard_speed_widget(self, - downloader_id: str) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: - """ - 获取仪表板实时速率组件 - """ - downloader = self.__get_downloader_enum_by_id(downloader_id=downloader_id) - if not downloader: - return None - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - - # 列配置 - cols = { - 'cols': 12, - 'xxl': 4, - 'xl': 4, - 'lg': 4, - 'md': 4, - 'sm': 12, - 'xs': 12 - } - - # 全局配置 - attrs = { - 'title': f'实时速率 #{downloader.short_name}' - } - if self.__check_target_downloader(downloader_id=downloader_id): - attrs['refresh'] = self.__get_config_item('dashboard_speed_widget_refresh') - - # 页面元素 - elements = self.__get_dashboard_speed_widget_elememts(downloader_id=downloader_id) - - return cols, attrs, elements - - def stop_service(self): - """ - 退出插件 - """ - try: - logger.info('尝试停止插件服务...') - self.__exit_event.set() - self.__stop_scheduler() - self.__clear_cache() - logger.info('插件服务停止完成') - except Exception as e: - logger.error(f"插件服务停止异常: {str(e)}", exc_info=True) - finally: - self.__exit_event.clear() - - @staticmethod - def __check_mp_user_auth() -> bool: - """ - 检查mp用户认证 - :return: True表示已认证 - """ - return SitesHelper().auth_level >= 2 - - def __parse_tracker_mappings(self, tracker_mappings: str) -> Dict[str, str]: - """ - 解析配置的tracker映射 - :param tracker_mappings: 配置的tracker映射 - :return: tracker映射,词典 - """ - mappings = {} - if not tracker_mappings: - return mappings - lines = tracker_mappings.split('\n') - for line in lines: - if not line: - continue - line = line.strip() - arr = line.split(':') - if len(arr) < 2: - continue - key, value = arr[0], arr[1] - if not key or not value: - continue - key, value = key.strip(), value.strip() - if not key or not value: - continue - if self.__is_valid_domain(key) and self.__is_valid_domain(value): - mappings[key] = value - return mappings - - @staticmethod - def __split_tags(tags: str = None) -> Set[str]: - """ - 分割tags字符串为set - :param tags: tags字符串 - """ - return set(re.split(r"\s*,\s*", tags.strip())) if tags else set() - - def __exists_exclude_tag(self, tags=None) -> bool: - """ - 判断多个标签中是否存在被排除的标签 - :param tags: 字符串或者集合 - """ - if not tags: - return False - tags_type = type(tags) - if tags_type == str: - return self.__exists_exclude_tag(self.__split_tags(tags)) - elif tags_type == set or tags_type == list: - if not self.__exclude_tags: - return False - for tag in tags: - if tag in self.__exclude_tags: - return True - return False - else: - return False - - def __start_scheduler(self, timezone=None): - """ - 启动调度器 - :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() - except Exception as e: - logger.error(f"插件服务调度器启动异常: {str(e)}", exc_info=True) - - 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 __clear_cache(self): - """ - 清除缓存 - """ - try: - logger.info('尝试清除插件缓存...') - if self.__ttl_cache: - self.__ttl_cache.clear() - logger.info('插件缓存清除成功') - else: - logger.info('插件未启用缓存,无须清除') - except Exception as e: - logger.error(f"插件缓存清除异常: {str(e)}", exc_info=True) - - 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 'dashboard_widget_size' in config_keys: - dashboard_widget_size = config.get('dashboard_widget_size') - config['dashboard_widget_size'] = int(dashboard_widget_size) if dashboard_widget_size else None - if 'dashboard_widget_refresh' in config_keys: - dashboard_widget_refresh = config.get('dashboard_widget_refresh') - config['dashboard_widget_refresh'] = int(dashboard_widget_refresh) if dashboard_widget_refresh else None - if 'dashboard_widget_display_fields' in config_keys: - 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 __check_environment(self): - """" - 检查环境 - """ - if not self.__check_mp_user_auth(): - logger.warn("MoviePilot未认证,【站点名称优先】功能将不可用。") - - 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 and use_default: - config_default = self.__config_default if self.__config_default else {} - config_value = config_default.get(config_key) - return config_value - - def __match_site_domain_by_tracker_domain(self, tracker_domain: str, use_default: bool = True) -> Optional[str]: - """ - 通过tracker映射配置根据tracker域名匹配站点域名 - :param tracker_domain: tracker域名 - :param use_default: 是否使用缺省值 - :return: 站点域名 - """ - if not tracker_domain: - return None - tracker_mappings = self.__tracker_mappings if self.__tracker_mappings else {} - site_domain = tracker_mappings.get(tracker_domain) - if site_domain: - return site_domain - for key, value in tracker_mappings.items(): - if tracker_domain.endswith('.' + key): - return value - if not use_default: - return None - tracker_mappings_default = self.__tracker_mappings_default if self.__tracker_mappings_default else {} - site_domain = tracker_mappings_default.get(tracker_domain) - if site_domain: - return site_domain - for key, value in tracker_mappings_default.items(): - if tracker_domain.endswith('.' + key): - return value - return None - - def __get_site_info_by_domain(self, site_domain: str) -> CommentedMap: - """ - 根据站点域名从索引中获取站点信息 - :param site_domain: 站点域名 - :return: 站点信息 - """ - if not site_domain: - return None - return SitesHelper().get_indexer(site_domain) - - def __check_enable_listen(self) -> bool: - """ - 判断是否启用了事件监听 - :return: 是否启用了事件监听 - """ - return True if self.__get_config_item(config_key='listen_download_event') \ - or self.__get_config_item(config_key='listen_source_file_event') else False - - def __check_enable_qb_sub_task(self) -> bool: - """ - 判断是否启用了qb子任务 - :return: 是否启用了qb子任务 - """ - return True if self.__get_config_item(config_key='qb_enable_seeding') \ - or self.__get_config_item(config_key='qb_enable_tagging') \ - or self.__get_config_item(config_key='qb_enable_delete') else False - - def __check_enable_qb_task(self) -> bool: - """ - 判断是否启用了qb任务 - :return: 是否启用了qb任务 - """ - return True if self.__get_config_item(config_key='qb_enable') \ - and self.__check_enable_qb_sub_task() else False - - def __check_enable_tr_sub_task(self) -> bool: - """ - 判断是否启用了tr子任务 - :return: 是否启用了tr子任务 - """ - return True if self.__get_config_item(config_key='tr_enable_seeding') \ - or self.__get_config_item(config_key='tr_enable_tagging') \ - or self.__get_config_item(config_key='tr_enable_delete') else False - - def __check_enable_tr_task(self) -> bool: - """ - 判断是否启用了tr任务 - :return: 是否启用了tr任务 - """ - return True if self.__get_config_item(config_key='tr_enable') \ - and self.__check_enable_tr_sub_task() else False - - def __check_enable_any_task(self) -> bool: - """ - 判断是否启用了任意任务 - :return: 是否启用了任意任务 - """ - return True if self.__check_enable_qb_task() \ - or self.__check_enable_tr_task() else False - - def __check_enable_dashboard_active_torrent_widget(self) -> bool: - """ - 判断是否启用了仪表板活动种子组件 - :return: 是否启用了仪表板活动种子组件 - """ - return True if self.__get_config_item('enable_dashboard_widget') else False - - def __check_enable_dashboard_speed_widget(self) -> bool: - """ - 判断是否启用了仪表板实时速率组件 - :return: 是否启用了仪表板实时速率组件 - """ - return True if self.__get_config_item('enable_dashboard_speed_widget') else False - - @classmethod - def __parse_tracker_for_qbittorrent(cls, torrent: TorrentDictionary) -> Optional[str]: - """ - qb解析 tracker - :return: tracker url - """ - if not torrent: - return None - tracker = torrent.get('tracker') - if tracker and len(tracker) > 0: - return tracker - magnet_uri = torrent.get('magnet_uri') - if not magnet_uri or len(magnet_uri) <= 0: - return None - magnet_uri_obj = urlparse(magnet_uri) - query = cls.__parse_url_query(magnet_uri_obj.query) - tr = query['tr'] - if not tr or len(tr) <= 0: - return None - return tr[0] - - @classmethod - def __parse_tracker_for_transmission(cls, torrent: Torrent) -> Optional[str]: - """ - tr解析 tracker - :return: tracker url - """ - if not torrent: - return None - trackers = torrent.trackers - if not trackers or len(trackers) <= 0: - return None - tracker = trackers[0] - if not tracker: - return None - return tracker.get('announce') - - @staticmethod - def __parse_url_query(query) -> dict: - """ - 解析url的query - :param query 字典 - """ - if not query or len(query) <= 0: - return {} - return urllib.parse.parse_qs(query) - - @staticmethod - def __get_url_domain(url: str) -> Optional[str]: - """ - 获取url的域名 - """ - if not url: - return None - scheme, netloc = StringUtils.get_url_netloc(url) - return netloc - - def __get_main_domain(self, domain: str) -> Optional[str]: - """ - 获取域名的主域名 - :param domain: 原域名 - :return: 主域名 - """ - if not domain: - return None - domain_arr = domain.split('.') - domain_len = len(domain_arr) - if domain_len < 2: - return None - root_domain, root_domain_len = self.__match_multi_level_root_domain(domain=domain) - if root_domain: - return f'{domain_arr[-root_domain_len - 1]}.{root_domain}' - else: - return f'{domain_arr[-2]}.{domain_arr[-1]}' - - def __get_domain_keyword(self, domain: str) -> Optional[str]: - """ - 获取域名关键字 - """ - main_domain = self.__get_main_domain(domain=domain) - if not main_domain: - return None - return main_domain.split('.')[0] - - def __match_multi_level_root_domain(self, domain: str) -> Tuple[Optional[str], int]: - """ - 匹配多级根域名 - :param domain: 被匹配的域名 - :return: 匹配的根域名, 匹配的根域名长度 - """ - if not domain or not self.__multi_level_root_domain: - return None, 0 - for root_domain in self.__multi_level_root_domain: - if domain.endswith('.' + root_domain): - root_domain_len = len(root_domain.split('.')) - return root_domain, root_domain_len - return None, 0 - - def __is_valid_domain(self, domain: str) -> bool: - """ - 判断域名是否有效 - :param domain: 被判断的域名 - :return: 是否有效 - """ - if not domain: - return False - domain_len = len(domain.split('.')) - root_domain, root_domain_len = self.__match_multi_level_root_domain(domain) - if root_domain: - return domain_len > root_domain_len - return domain_len > 1 - - def __generate_site_tag(self, site: str) -> Optional[str]: - """ - 生成站点标签 - """ - if not site: - return None - tag_prefix = self.__get_config_item('tag_prefix') - if not tag_prefix: - return site - return f'{tag_prefix}{site}' - - def __consult_site_tag_by_tracker(self, tracker_url: str) -> Tuple[Optional[str], Optional[Set[str]]]: - """ - 根据tracker地址咨询站点标签 - :return: ('本次需要添加的站点标签', '建议移除的可能存在的历史标签集合') - """ - if not tracker_url: - return None, None - - # tracker的完整域名 - tracker_domain = self.__get_url_domain(url=tracker_url) - if not tracker_domain: - return None, None - - # 建议移除的可能存在的历史标签集合 - delete_suggest = set() - - # tracker域名关键字 - tracker_domain_keyword = self.__get_domain_keyword(domain=tracker_domain) - if tracker_domain_keyword: - # 建议移除 - delete_suggest.add(tracker_domain_keyword) - delete_suggest.add(self.__generate_site_tag(site=tracker_domain_keyword)) - - # 首先根据tracker的完整域名去匹配站点信息 - site_info = self.__get_site_info_by_domain(site_domain=tracker_domain) - - # 如果没有匹配到,再根据主域名去匹配 - if not site_info: - tracker_main_domain = self.__get_main_domain(domain=tracker_domain) - if tracker_main_domain and tracker_main_domain != tracker_domain: - site_info = self.__get_site_info_by_domain(tracker_main_domain) - - # 如果还是没有匹配到,就根据tracker映射的域名匹配 - matched_site_domain = None - if not site_info: - matched_site_domain = self.__match_site_domain_by_tracker_domain(tracker_domain) - if matched_site_domain: - site_info = self.__get_site_info_by_domain(matched_site_domain) - - matched_site_domain_keyword = self.__get_domain_keyword(matched_site_domain) - if matched_site_domain_keyword: - # 建议移除 - delete_suggest.add(matched_site_domain_keyword) - delete_suggest.add(self.__generate_site_tag(matched_site_domain_keyword)) - - # 如果匹配到了站点信息 - if site_info: - site_name = site_info.get('name') - site_tag_by_name = self.__generate_site_tag(site_name) - site_domain_keyword = self.__get_domain_keyword(self.__get_url_domain(site_info.get('domain'))) - site_tag_by_domain_keyword = self.__generate_site_tag(site_domain_keyword) - # 站点名称优先 - site_name_priority = self.__get_config_item('site_name_priority') - site_tag = site_tag_by_name if site_name_priority else site_tag_by_domain_keyword - # 建议移除 - delete_suggest.add(site_name) - delete_suggest.add(site_tag_by_name) - delete_suggest.add(site_domain_keyword) - delete_suggest.add(site_tag_by_domain_keyword) - else: - if matched_site_domain: - site_tag = self.__generate_site_tag(self.__get_domain_keyword(matched_site_domain)) - else: - site_tag = self.__generate_site_tag(self.__get_domain_keyword(tracker_domain)) - - if site_tag and site_tag in delete_suggest: - delete_suggest.remove(site_tag) - - return site_tag, delete_suggest - - @classmethod - def __check_need_delete_for_qbittorrent(cls, torrent: TorrentDictionary, context: TaskContext) -> bool: - """ - 检查qb种子是否满足删除条件 - :param context: 任务上下文 - """ - if not torrent or not context: - return False - - # 根据种子状态判断是否应该删种:状态为丢失文件时需要删除 - if torrent.get('state') == 'missingFiles': - return True - - # 源文件删除事件数据 - download_file_deleted_event_data = context.get_download_file_deleted_event_data() - # 下载任务删除事件数据 - download_deleted_event_data = context.get_download_deleted_event_data() - - # 源文件删除事件触发 - if download_file_deleted_event_data: - # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 - match, torrent_data_path = cls.__check_torrent_match_file_for_qbittorrent(torrent=torrent, - source_file_info=download_file_deleted_event_data) - if not match: - return False - # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 - return not os.path.exists(torrent_data_path) - # 下载任务删除事件触发 - elif download_deleted_event_data: - torrent_info = download_deleted_event_data - match = cls.__check_torrent_match_torrent_info(torrent_hash=torrent.get('hash'), - torrent_data_file_name=torrent.get('name'), - torrent_size=torrent.get('total_size'), - torrent_info=torrent_info) - return match - return False - - @classmethod - def __check_need_delete_for_transmission(cls, torrent: Torrent, context: TaskContext) -> bool: - """ - 检查tr种子是否满足删除条件 - :param deleted_event_data: 任务执行伴随的源文件删除事件数据 - """ - if not torrent or not context: - return False - - # 根据种子状态判断是否应该删种:状态为丢失文件时需要删除 - if torrent.error == 3 and torrent.error_string and 'No data found' in torrent.error_string: - return True - - # 源文件删除事件数据 - download_file_deleted_event_data = context.get_download_file_deleted_event_data() - # 下载任务删除事件数据 - download_deleted_event_data = context.get_download_deleted_event_data() - - # 源文件删除事件触发 - if download_file_deleted_event_data: - # 根据伴随的源文件删除事件判断是否应该删种:如果当前种子和事件匹配并且种子中已经不存在数据文件时就需要删除 - match, torrent_data_path = cls.__check_torrent_match_file_for_transmission(torrent=torrent, - source_file_info=download_file_deleted_event_data) - if not match: - return False - # 如果匹配的种子数据路径不存在,说明数据文件已经(全部)被删除了,那么就允许删种 - return not os.path.exists(torrent_data_path) - # 下载任务删除事件触发 - elif download_deleted_event_data: - torrent_info = download_deleted_event_data - match = cls.__check_torrent_match_torrent_info(torrent_hash=torrent.hashString, - torrent_data_file_name=torrent.name, - torrent_size=torrent.total_size, - torrent_info=torrent_info) - return match - return False - - @classmethod - def __check_torrent_match_file_for_qbittorrent(cls, torrent: TorrentDictionary, - source_file_info: dict) -> Tuple[bool, Optional[str]]: - """ - 检查种子和源文件是否匹配 - :param torrent: 种子 - :param source_file_info: 源文件信息:src=源文件路径,hash=源文件对应的种子hash - :return: 是否匹配, 匹配的种子数据文件路径 - """ - if not torrent or not source_file_info: - return False, None - return cls.__check_torrent_match_file(torrent_hash=torrent.get('hash'), - torrent_data_file_name=torrent.get('name'), - source_hash=None, - source_file_path=source_file_info.get('src')) - - @classmethod - def __check_torrent_match_file_for_transmission(cls, torrent: Torrent, - source_file_info: dict) -> Tuple[bool, Optional[str]]: - """ - 检查种子和源文件是否匹配 - :param torrent: 种子 - :param source_file_info: 源文件信息:src=源文件路径,hash=源文件对应的种子hash - :return: 是否匹配, 匹配的种子数据文件路径 - """ - if not torrent or not source_file_info: - return False, None - return cls.__check_torrent_match_file(torrent_hash=torrent.hashString, - torrent_data_file_name=torrent.get('name'), - source_hash=None, - source_file_path=source_file_info.get('src')) - - @classmethod - def __check_torrent_match_file(cls, torrent_hash: str, - torrent_data_file_name: str, - source_hash: Optional[str], - source_file_path: str) -> Tuple[bool, Optional[str]]: - """ - 检查种子和源文件是否匹配 - :param torrent_hash: 种子hash - :param torrent_data_file_name: 种子数据文件名 - :param source_hash: 源文件对应的种子hash - :param source_file_path: 源文件路径 - :return: 是否匹配, 匹配的种子数据文件(在MoviePilot中的)路径 - """ - if not torrent_hash or not torrent_data_file_name or not source_file_path: - return False, None - # 当前传入源hash时,先根据hash判断是否匹配 - if source_hash and torrent_hash != hash: - return False, None - - # 从源文件路径中分离文件夹路径和文件名称 - source_file_dir, source_file_name = os.path.split(source_file_path) - # 情况一:如果源文件名称和种子数据文件(夹)名称一致,则认为匹配,适用于单文件种子和原盘资源的情况 - if source_file_name == torrent_data_file_name: - return True, source_file_path - # 情况二:如果原文件父目录名称和种子数据文件(夹)名称一致,则认为匹配,适用于多文件剧集种子的情况 - _, source_file_dir_name = os.path.split(source_file_dir) - if source_file_dir_name == torrent_data_file_name: - return True, source_file_dir - # 情况三:如果种子数据文件(夹)名称是源文件路径的一部分,则认为匹配 - torrent_data_file_name_wrap = os.path.sep + torrent_data_file_name + os.path.sep - index = source_file_path.find(torrent_data_file_name_wrap) - if index >= 0: - return True, source_file_path[0:index] + os.path.sep + torrent_data_file_name - - return False, None - - @classmethod - def __check_torrent_match_torrent_info(cls, - torrent_hash: str, - torrent_data_file_name: str, - torrent_size: int, - torrent_info: dict) -> bool: - """ - 判断种子是否和种子信息匹配 - :param torrent_hash: 种子hash - :param torrent_data_file_name: 种子数据文件名 - :param torrent_size: 种子大小 - :param torrent_info: 被判断的其它种子信息 - :return: 是否匹配 - """ - if not torrent_hash or not torrent_data_file_name or not torrent_size or not torrent_info: - return False - return torrent_data_file_name == torrent_info.get("title") \ - and torrent_size == torrent_info.get("size") - - def __send_notify(self, context: TaskContext): - """ - 发送通知 - :param context: 任务执行上下文 - """ - if not context or not self.__get_config_item('enable_notify'): - return - text = self.__build_notify_message(context=context) - if not text: - return - self.post_message(title=f'{self.plugin_name}任务执行结果', text=text, mtype=NotificationType.Plugin) - - @staticmethod - def __build_notify_message(context: TaskContext): - """ - 构建通知消息内容 - """ - text = '' - if not context: - return text - results = context.get_results() - if not results or len(results) <= 0: - return text - for result in results: - if not result: - continue - seeding = result.get_seeding() - tagging = result.get_tagging() - delete = result.get_delete() - if result.is_success() and not seeding and not tagging and not delete: - continue - text += f'【任务:{result.get_name()}】\n' - if result.is_success(): - text += f'总种数:{result.get_total()}\n' - if seeding: - text += f'做种数:{seeding}\n' - if tagging: - text += f'打标数:{tagging}\n' - if delete: - text += f'删种数:{delete}\n' - else: - text += '执行失败\n' - text += '\n————————————\n' - return text - - def __get_qbittorrent(self) -> Qbittorrent: - """ - 获取qb实例 - """ - module = ModuleManager().get_running_module(module_id='QbittorrentModule') - if not module: - return None - qbittorrent = getattr(module, 'qbittorrent') - if not qbittorrent or not getattr(qbittorrent, 'qbc'): - return None - return qbittorrent - - def __get_transmission(self) -> Transmission: - """ - 获取tr实例 - """ - module = ModuleManager().get_running_module(module_id='TransmissionModule') - if not module: - return None - transmission = getattr(module, 'transmission') - if not transmission or not getattr(transmission, 'trc'): - return None - return transmission - - def __try_run(self, context: TaskContext = None): - """ - 尝试运行插件任务 - """ - if not self.__task_lock.acquire(blocking=False): - logger.info('已有进行中的任务,本次不执行') - return - try: - self.__run_for_all(context=context) - finally: - self.__task_lock.release() - - def __block_run(self, context: TaskContext = None): - """ - 阻塞运行插件任务 - """ - self.__task_lock.acquire() - try: - self.__run_for_all(context=context) - finally: - self.__task_lock.release() - - def __run_for_all(self, context: TaskContext = None) -> TaskContext: - """ - 针对所有下载器运行插件任务 - :param context: 任务上下文 - :return: 任务上下文 - """ - if not context: - context = TaskContext() - - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,任务终止') - return context - - self.__run_for_qbittorrent(context=context) - - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,任务终止') - return context - - self.__run_for_transmission(context=context) - - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,任务终止') - return context - - # 发送通知 - self.__send_notify(context=context) - - return context - - def __run_for_qbittorrent(self, context: TaskContext = None) -> TaskContext: - """ - 针对qb下载器运行插件任务 - :param context: 任务上下文 - :return: 任务上下文 - """ - if not context: - context = TaskContext() - downloader_name = 'qBittorrent' - - # 处理前置条件 - if not self.__check_enable_qb_task(): - return context - if not context.is_selected_qb_downloader(): - return context - enable_tagging = True if self.__get_config_item( - config_key='qb_enable_tagging') and context.is_enabled_tagging() else False - enable_seeding = True if self.__get_config_item( - config_key='qb_enable_seeding') and context.is_enabled_seeding() else False - enable_delete = True if self.__get_config_item( - config_key='qb_enable_delete') and context.is_enabled_delete() else False - if not enable_seeding and not enable_tagging and not enable_delete: - return context - # 任务结果 - result = TaskResult(downloader_name) - try: - qbittorrent = self.__get_qbittorrent() - if not qbittorrent: - return context - - logger.info(f'下载器[{downloader_name}]任务执行开始...') - - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - - context.save_result(result=result) - - torrents, error = qbittorrent.get_torrents() - if error: - logger.warn(f'从下载器[{downloader_name}]中获取种子失败,任务终止') - return context - if not torrents or len(torrents) <= 0: - logger.warn(f'下载器[{downloader_name}]中没有种子,任务终止') - return context - result.set_total(len(torrents)) - - # 根据上下文过滤种子 - selected_torrents = context.get_selected_torrents() - torrents = torrents if selected_torrents is None \ - else [torrent for torrent in torrents if torrent and torrent.hash in selected_torrents] - if not torrents or len(torrents) <= 0: - logger.warn(f'下载器[{downloader_name}]中没有目标种子,任务终止') - return context - - logger.info( - f'子任务执行状态: 自动标签={enable_tagging}, 自动做种={enable_seeding}, 自动删种={enable_delete}') - - # 自动标签 - if enable_tagging: - result.set_tagging(self.__tagging_batch_for_qbittorrent(qbittorrent=qbittorrent, torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 自动做种 - if enable_seeding: - result.set_seeding(self.__seeding_batch_for_qbittorrent(torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 自动删种 - if enable_delete: - result.set_delete(self.__delete_batch_for_qbittorrent(qbittorrent=qbittorrent, torrents=torrents, - context=context)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - - logger.info(f'下载器[{downloader_name}]任务执行成功') - except Exception as e: - result.set_success(False) - logger.error(f'下载器[{downloader_name}]任务执行失败: {str(e)}', exc_info=True) - return context - - def __seeding_batch_for_qbittorrent(self, torrents: List[TorrentDictionary]) -> int: - """ - qb批量自动做种 - :return: 做种数 - """ - logger.info('[QB]批量自动做种开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if self.__seeding_single_for_qbittorrent(torrent=torrent): - count += 1 - logger.info('[QB]批量自动做种结束') - return count - - def __seeding_single_for_qbittorrent(self, torrent: TorrentDictionary) -> bool: - """ - qb单个自动做种 - :return: 是否执行 - """ - if not torrent: - return False - # 种子当前已经存在的标签 - torrent_tags = self.__split_tags(torrent.get('tags')) - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - need_seeding = torrent.state_enum.is_complete and torrent.state_enum.is_paused - if not need_seeding: - return False - torrent.resume() - logger.info(f"[QB]单个自动做种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") - return True - - def __tagging_batch_for_qbittorrent(self, - qbittorrent: Qbittorrent, - torrents: List[TorrentDictionary]) -> int: - """ - qb批量自动标签 - :return: 打标数 - """ - logger.info('[QB]批量自动标签开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if self.__tagging_single_for_qbittorrent(qbittorrent=qbittorrent, torrent=torrent): - count += 1 - logger.info('[QB]批量自动标签结束') - return count - - def __tagging_single_for_qbittorrent(self, - qbittorrent: Qbittorrent, - torrent: TorrentDictionary) -> bool: - """ - qb单个自动标签 - :return: 是否执行 - """ - if not torrent: - return False - - hash_str = torrent.get('hash') - # 种子当前已经存在的标签 - torrent_tags = self.__split_tags(torrent.get('tags')) - # 需要移除的标签 - remove_tags = None - # 要添加的标签 - add_tags = [] - - # 处理BT/PT标签 - if "BT" not in torrent_tags and "PT" not in torrent_tags: - is_private = self.__check_private_torrent_for_qbittorrent(qbittorrent=qbittorrent, hash_str=hash_str) - btpt_tag = "PT" if is_private else "BT" - add_tags.append(btpt_tag) - - # 处理站点标签 - # BT种子与站点无关,故排除BT标签 - if "BT" not in torrent_tags and "BT" not in add_tags: - # 种子的tracker地址 - tracker_url = self.__parse_tracker_for_qbittorrent(torrent=torrent) - if tracker_url: - # 获取标签建议 - site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) - # 移除建议删除的标签 - if delete_suggest: - remove_tags = [to_delete for to_delete in delete_suggest if to_delete and to_delete in torrent_tags] - # 如果本次需要打标签 - if site_tag and site_tag not in torrent_tags and site_tag not in add_tags: - add_tags.append(site_tag) - - if not remove_tags and not add_tags: - return False - if remove_tags: - torrent.remove_tags(tags=remove_tags) - # 打标签 - if add_tags: - torrent.add_tags(tags=add_tags) - logger.info(f"[QB]单个自动标签成功: hash = {hash_str}, name = {torrent.get('name')}") - # Flush 标签 - self.__flush_torrent_tags_for_qbittorrent(torrent=torrent, remove_tags=remove_tags, add_tags=add_tags) - return True - - def __flush_torrent_tags_for_qbittorrent(self, torrent: TorrentDictionary, remove_tags: List[str], add_tags: List[str]): - """ - qb Flush 标签到种子信息中(即更新内存数据) - """ - try: - if not torrent: - return - torrent_tags = self.__split_tags(torrent.get('tags')) - if remove_tags: - for remove_tag in remove_tags: - if remove_tag and remove_tag in torrent_tags: - torrent_tags.remove(remove_tag) - if add_tags: - for add_tag in add_tags: - if add_tag and add_tag not in torrent_tags: - torrent_tags.add(add_tag) - tag_str = ', '.join(torrent_tags) - torrent.update({'tags': tag_str}) - except Exception as e: - logger.error(f'Flush种子标签异常: {str(e)}', exc_info=True) - - def __check_private_torrent_for_qbittorrent(self, - qbittorrent: Qbittorrent, - hash_str: str) -> bool: - """ - qb检查种子是否是私有种子 - :return: 是否是私有种子 - """ - trackers = qbittorrent.qbc.torrents_trackers(torrent_hash=hash_str) - if not trackers: - return False - for tracker in trackers: - if not tracker: - continue - url = tracker.get("url") - status = tracker.get("status") - tier = tracker.get("tier") - if url in self.__public_tracker_urls and status == 0 and tier == -1: - return True - return False - - def __delete_batch_for_qbittorrent(self, qbittorrent: Qbittorrent, torrents: List[TorrentDictionary], - context: TaskContext) -> int: - """ - qb批量自动删种 - :return: 删种数 - """ - logger.info('[QB]批量自动删种开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if (self.__delete_single_for_qbittorrent(qbittorrent=qbittorrent, torrent=torrent, - context=context)): - count += 1 - logger.info('[QB]批量自动删种结束') - return count - - def __delete_single_for_qbittorrent(self, qbittorrent: Qbittorrent, torrent: TorrentDictionary, - context: TaskContext) -> bool: - """ - qb单个自动删种 - :return: 是否执行 - """ - if not torrent: - return False - # 种子当前已经存在的标签 - torrent_tags = self.__split_tags(torrent.get('tags')) - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - if not self.__check_need_delete_for_qbittorrent(torrent=torrent, context=context): - return False - qbittorrent.delete_torrents(True, torrent.get('hash')) - logger.info(f"[QB]单个自动删种完成: hash = {torrent.get('hash')}, name = {torrent.get('name')}") - return True - - def __run_for_transmission(self, context: TaskContext = None) -> TaskContext: - """ - 针对tr下载器运行插件任务 - :param context: 任务上下文 - :return: 运行结果 - """ - if not context: - context = TaskContext() - downloader_name = 'Transmission' - - # 处理前置条件 - if not self.__check_enable_tr_task(): - return context - if not context.is_selected_tr_downloader(): - return context - enable_tagging = True if self.__get_config_item( - config_key='tr_enable_tagging') and context.is_enabled_tagging() else False - enable_seeding = True if self.__get_config_item( - config_key='tr_enable_seeding') and context.is_enabled_seeding() else False - enable_delete = True if self.__get_config_item( - config_key='tr_enable_delete') and context.is_enabled_delete() else False - if not enable_seeding and not enable_tagging and not enable_delete: - return context - - # 任务结果 - result = TaskResult(downloader_name) - - try: - transmission = self.__get_transmission() - if not transmission: - return context - - logger.info(f'下载器[{downloader_name}]任务执行开始...') - - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - - context.save_result(result=result) - - # 获取全部种子 - # 需要 isPrivate 字段判断是否是私有种子 - arguments = transmission._trarg.copy() - is_private_field = "isPrivate" - if is_private_field not in arguments: - arguments.append(is_private_field) - try: - torrents = transmission.trc.get_torrents(arguments=arguments) - except Exception as e: - logger.warn(f'从下载器[{downloader_name}]中获取种子失败,任务终止') - return context - if not torrents or len(torrents) <= 0: - logger.warn(f'下载器[{downloader_name}]中没有种子,任务终止') - return context - result.set_total(len(torrents)) - - # 根据上下文过滤种子 - selected_torrents = context.get_selected_torrents() - torrents = torrents if selected_torrents is None \ - else [torrent for torrent in torrents if torrent and torrent.hashString in selected_torrents] - if not torrents or len(torrents) <= 0: - logger.warn(f'下载器[{downloader_name}]中没有目标种子,任务终止') - return context - - logger.info( - f'子任务执行状态: 自动标签={enable_tagging}, 自动做种={enable_seeding}, 自动删种={enable_delete}') - - # 自动标签 - if enable_tagging: - result.set_tagging(self.__tagging_batch_for_transmission(transmission=transmission, torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 自动做种 - if enable_seeding: - result.set_seeding(self.__seeding_batch_for_transmission(transmission=transmission, torrents=torrents)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - # 自动删种 - if enable_delete: - result.set_delete(self.__delete_batch_for_transmission(transmission=transmission, torrents=torrents, - context=context)) - if self.__exit_event.is_set(): - logger.warn(f'插件服务正在退出,任务终止[{downloader_name}]') - return context - - logger.info(f'下载器[{downloader_name}]任务执行成功') - except Exception as e: - result.set_success(False) - logger.error(f'下载器[{downloader_name}]任务执行失败: {str(e)}', exc_info=True) - return context - - def __seeding_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent]) -> int: - """ - tr批量自动做种 - :return: 做种数 - """ - logger.info('[TR]批量自动做种开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if self.__seeding_single_for_transmission(transmission=transmission, torrent=torrent): - count += 1 - logger.info('[TR]批量自动做种结束') - return count - - def __seeding_single_for_transmission(self, transmission: Transmission, torrent: Torrent) -> bool: - """ - tr单个自动做种 - :return: 是否执行 - """ - if not torrent: - return False - # 种子当前已经存在的标签 - torrent_tags = torrent.get('labels') - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - need_seeding = torrent.progress == 100 and torrent.stopped and torrent.error == 0 - if not need_seeding: - return False - transmission.start_torrents(torrent.hashString) - logger.info(f"[TR]单个自动做种完成: hash = {torrent.hashString}, name = {torrent.get('name')}") - return True - - def __tagging_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent]) -> int: - """ - tr批量自动标签 - :return: 打标数 - """ - logger.info('[TR]批量自动标签开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if self.__tagging_single_for_transmission(transmission=transmission, torrent=torrent): - count += 1 - logger.info('[TR]批量自动标签结束') - return count - - def __tagging_single_for_transmission(self, transmission: Transmission, torrent: Torrent) -> bool: - """ - tr单个自动标签 - :return: 是否执行 - """ - if not torrent: - return False - - hash_str = torrent.hashString - # 种子当前已经存在的标签 - torrent_tags = torrent.get('labels') or [] - # 需要移除的标签 - remove_tags = None - # 要添加的标签 - add_tags = [] - - # 处理BT/PT标签 - if "BT" not in torrent_tags and "PT" not in torrent_tags: - is_private = self.__check_private_torrent_for_transmission(torrent=torrent) - btpt_tag = "PT" if is_private else "BT" - add_tags.append(btpt_tag) - - # 处理站点标签 - # BT种子与站点无关,故排除BT标签 - if "BT" not in torrent_tags and "BT" not in add_tags: - # 种子的tracker地址 - tracker_url = self.__parse_tracker_for_transmission(torrent=torrent) - if tracker_url: - # 获取标签建议 - site_tag, delete_suggest = self.__consult_site_tag_by_tracker(tracker_url=tracker_url) - # 移除建议删除的标签 - if delete_suggest: - remove_tags = [to_delete for to_delete in delete_suggest if to_delete and to_delete in torrent_tags] - # 如果本次需要打标签 - if site_tag and site_tag not in torrent_tags and site_tag not in add_tags: - add_tags.append(site_tag) - - # 如果没有变化就不继续保存 - if not remove_tags and not add_tags: - return False - torrent_tags_copy = torrent_tags.copy() - if remove_tags: - for remove_tag in remove_tags: - torrent_tags_copy.remove(remove_tag) - if add_tags: - for add_tag in add_tags: - torrent_tags_copy.append(add_tag) - # 保存标签 - transmission.set_torrent_tag(hash_str, torrent_tags_copy) - logger.info(f"[TR]单个自动标签成功: hash = {hash_str}, name = {torrent.get('name')}") - # Flush 标签 - self.__flush_torrent_tags_for_transmission(torrent=torrent, tags=torrent_tags_copy) - return True - - def __flush_torrent_tags_for_transmission(self, torrent: Torrent, tags: List[str]): - """ - tr Flush 标签到种子信息中(即更新内存数据) - """ - try: - if not torrent: - return - torrent.fields.update({'labels': tags}) - except Exception as e: - logger.error(f'Flush种子标签异常: {str(e)}', exc_info=True) - - def __check_private_torrent_for_transmission(self, - torrent: Torrent) -> bool: - """ - tr检查种子是否是私有种子 - :return: 是否是私有种子 - """ - return torrent.get("isPrivate") - - def __delete_batch_for_transmission(self, transmission: Transmission, torrents: List[Torrent], - context: TaskContext) -> int: - """ - tr批量自动删种 - :return: 删种数 - """ - logger.info('[TR]批量自动删种开始...') - count = 0 - if not torrents: - return count - for torrent in torrents: - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,子任务终止') - return count - if (self.__delete_single_for_transmission(transmission=transmission, torrent=torrent, - context=context)): - count += 1 - logger.info('[TR]批量自动删种结束') - return count - - def __delete_single_for_transmission(self, transmission: Transmission, torrent: Torrent, - context: TaskContext) -> bool: - """ - tr单个自动删种 - :return: 是否执行 - """ - if not torrent: - return False - # 种子当前已经存在的标签 - torrent_tags = torrent.get('labels') - # 判断种子中是否存在排除的标签 - if self.__exists_exclude_tag(torrent_tags): - return False - if not self.__check_need_delete_for_transmission(torrent=torrent, context=context): - return False - transmission.delete_torrents(True, torrent.hashString) - 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_torrent_table_head_content(self, - fields: List[TorrentField] = None) -> list: - """ - 构造仪表板组件种子表头内容 - """ - if not fields: - fields = self.__get_dashboard_active_torrent_widget_display_fields() - if not fields: - return [] - return [{ - 'component': 'th', - 'props': { - 'class': 'text-start ps-4' - }, - 'text': field.name_ - } for field in fields if field] - - def __build_dashboard_widget_torrent_table_head(self, - fields: List[TorrentField] = None) -> dict: - """ - 构造仪表板组件种子表头 - """ - return { - 'component': 'thead', - 'content': self.__build_dashboard_widget_torrent_table_head_content(fields=fields) - } - - def __build_dashboard_widget_torrent_table_body_content(self, - data: List[List[Any]], - field_count: int, - downloader_id: str) -> list: - """ - 构造仪表板组件种子表体内容 - :param downloader_id: 下载器ID - :param data: 表格数据 - :param field_count: 字段数量 - """ - 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: - empty_text = '暂无数据' if self.__check_target_downloader(downloader_id=downloader_id) else '目标下载器配置无效' - return [{ - 'component': 'tr', - 'props': { - 'class': 'text-sm' - }, - 'content': [{ - 'component': 'td', - 'props': { - 'colspan': field_count, - 'class': 'text-center' - }, - 'text': empty_text - }] - }] - - def __build_dashboard_widget_torrent_table_body(self, - data: List[List[Any]], - field_count: int, - downloader_id: str) -> dict: - """ - 构造仪表板组件种子表体 - """ - return { - 'component': 'tbody', - 'content': self.__build_dashboard_widget_torrent_table_body_content(data=data, field_count=field_count, downloader_id=downloader_id) - } - - def __get_dashboard_widget_target_downloader_ids(self, config_key: str) -> List[str]: - """ - 获取仪表板组件目标下载器ids - """ - target_downloader_ids = [] - if not config_key: - return target_downloader_ids - target_downloaders = self.__get_config_item(config_key) - if not target_downloaders: - return target_downloader_ids - for target_downloader in target_downloaders: - if target_downloader == 'default': - target_downloader = settings.DEFAULT_DOWNLOADER - if target_downloader and target_downloader not in target_downloader_ids: - target_downloader_ids.append(target_downloader) - return target_downloader_ids - - def __get_dashboard_active_torrent_widget_target_downloader_ids(self) -> List[str]: - """ - 获取仪表板活动种子组件目标下载器ids - """ - return self.__get_dashboard_widget_target_downloader_ids(config_key='dashboard_widget_target_downloaders') - - def __get_dashboard_speed_widget_target_downloader_ids(self) -> List[str]: - """ - 获取仪表板实时速率组件目标下载器ids - """ - return self.__get_dashboard_widget_target_downloader_ids(config_key='dashboard_speed_widget_target_downloaders') - - def __get_dashboard_active_torrent_widget_display_fields(self) -> List[TorrentField]: - """ - 获取仪表板活动种子组件展示字段 - """ - fields = self.__get_config_item('dashboard_widget_display_fields') - return self.__ensure_torrent_fields(fields=fields) - - @staticmethod - def __get_downloader_enum_by_id(downloader_id: str) -> Downloader: - """ - 根据下载器id获取枚举 - """ - if not downloader_id: - return None - return DownloaderMap.get(downloader_id) - - def __check_target_downloader(self, downloader_id: str) -> bool: - """ - 检查目标下载器是否有效 - """ - if not downloader_id: - return False - if downloader_id == Downloader.QB.id: - return self.__get_qbittorrent() is not None - elif downloader_id == Downloader.TR.id: - return self.__get_transmission() is not None - else: - return False - - def __get_downloader_active_torrent_data(self, - downloader_id: str, - fields: List[TorrentField] = None): - """ - 获取下载器活动种子数据 - """ - if not downloader_id: - return None - # 字段 - if not fields: - fields = self.__get_dashboard_active_torrent_widget_display_fields() - if downloader_id == Downloader.QB.id: - return self.__get_qbittorrent_active_torrent_data(fields=fields) - elif downloader_id == Downloader.TR.id: - return self.__get_transmission_active_torrent_data(fields=fields) - else: - return None - - def __get_qbittorrent_active_torrent_data(self, - fields: List[TorrentField] = None): - """ - 获取qb活动种子数据 - """ - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - qbittorrent = self.__get_qbittorrent() - if not qbittorrent: - return None - # 字段 - if not fields: - fields = self.__get_dashboard_active_torrent_widget_display_fields() - # 活动种子 - torrents, error = qbittorrent.get_torrents(status=['active']) - if error: - return None - torrent_hashs = set([torrent.get('hash') for torrent in torrents if torrent and torrent.get('hash')]) - # 未下载完的种子 - downloading_torrents, _ = qbittorrent.get_torrents(status=['downloading']) - if downloading_torrents: - for downloading_torrent in downloading_torrents: - torrent_hash = downloading_torrent.get('hash') - if not torrent_hash or torrent_hash in torrent_hashs: - continue - torrents.append(downloading_torrent) - # 按添加时间倒序排序 - 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, - fields: List[TorrentField]): - """ - 加工qb种子 - """ - if not torrent: - return - - def calculate_remaining_size(torrent: TorrentDictionary): - """ - 计算剩余大小 - """ - remaining_size = torrent.get(TorrentField.REMAINING.qb) - if not remaining_size: - remaining_size = torrent.get(TorrentField.SELECT_SIZE.qb) - torrent.get(TorrentField.COMPLETED.qb) - torrent[TorrentField.REMAINING.qb] = remaining_size - return remaining_size - - try: - # 剩余大小 - if TorrentField.REMAINING in fields: - calculate_remaining_size(torrent=torrent) - # 剩余时间 - if TorrentField.REMAINING_TIME in fields: - 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_size = calculate_remaining_size(torrent=torrent) - 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, fields=fields) - 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 __build_transmission_field_arguments(self, fields: List[TorrentField]) -> List[str]: - """ - 构造tr字段查询参数 - """ - if not fields: - return [] - arguments = [field.tr for field in fields if field and field.tr and not field.tr.startswith('#')] - arguments.append('id') - arguments.append(TorrentField.NAME.tr) - arguments.append('hashString') - arguments.append(TorrentField.ADD_TIME.tr) - # 处理依赖的字段 - if TorrentField.COMPLETED in fields: - arguments.append('fileStats') - if TorrentField.REMAINING in fields: - arguments.append(TorrentField.SELECT_SIZE.tr) - arguments.append('fileStats') - if TorrentField.REMAINING_TIME in fields: - arguments.append(TorrentField.STATE.tr) - arguments.append(TorrentField.DOWNLOAD_SPEED.tr) - arguments.append(TorrentField.SELECT_SIZE.tr) - arguments.append('fileStats') - if TorrentField.DOWNLOAD_LIMIT in fields: - arguments.append('downloadLimited') - if TorrentField.UPLOAD_LIMIT in fields: - arguments.append('uploadLimited') - return list(set(arguments)) - - def __get_transmission_active_torrent_data(self, - fields: List[TorrentField] = None): - """ - 获取tr活动种子数据 - """ - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - transmission = self.__get_transmission() - if not transmission: - return None - # 字段 - if not fields: - fields = self.__get_dashboard_active_torrent_widget_display_fields() - torrents, _ = transmission.trc.get_recently_active_torrents(arguments=self.__build_transmission_field_arguments(fields=fields)) - 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, - fields: List[TorrentField]): - """ - 加工tr种子 - """ - if not torrent or not fields: - return - - def calculate_completed(torrent: Torrent): - """ - 计算已完成大小 - """ - completed = torrent.get(TorrentField.COMPLETED.tr) - if not completed: - completed = sum(x["bytesCompleted"] for x in torrent.fields["fileStats"]) - torrent.fields[TorrentField.COMPLETED.tr] = completed - return completed - - def calculate_remaining_size(torrent: Torrent): - """ - 计算剩余大小 - """ - remaining_size = torrent.get(TorrentField.REMAINING.tr) - if not remaining_size: - select_size = torrent.get(TorrentField.SELECT_SIZE.tr) - completed = calculate_completed(torrent=torrent) - remaining_size = select_size - completed - torrent.fields[TorrentField.REMAINING.tr] = remaining_size - return remaining_size - - try: - # 已完成大小 - if TorrentField.COMPLETED in fields: - calculate_completed(torrent=torrent) - # 剩余大小 - if TorrentField.REMAINING in fields: - calculate_remaining_size(torrent=torrent) - # 剩余时间 - if TorrentField.REMAINING_TIME in fields: - if torrent.get(TorrentField.STATE.tr) == TorrentStatus.DOWNLOADING.value: - download_speed = torrent.get(TorrentField.DOWNLOAD_SPEED.tr) - if download_speed <= 0: - remaining_time = -1 - else: - remaining_size = calculate_remaining_size(torrent=torrent) - remaining_time = remaining_size / download_speed - else: - remaining_time = 0 - torrent.fields[TorrentField.REMAINING_TIME.tr] = remaining_time - # 下载限速 - if TorrentField.DOWNLOAD_LIMIT in fields: - if not torrent.get('downloadLimited'): - torrent.fields[TorrentField.DOWNLOAD_LIMIT.tr] = None - # 上传限速 - if TorrentField.UPLOAD_LIMIT in fields: - if not torrent.get('uploadLimited'): - torrent.fields[TorrentField.UPLOAD_LIMIT.tr] = None - 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,fields=fields) - 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_active_torrent_widget_elememts(self, downloader_id: str) -> list: - """ - 获取仪表板活动种子组件元素 - """ - if not downloader_id: - return None - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - fields = self.__get_dashboard_active_torrent_widget_display_fields() - field_count=len(fields) - data = self.__get_downloader_active_torrent_data(downloader_id=downloader_id, fields=fields) - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - return [{ - 'component': 'VTable', - 'props': { - 'hover': True, - 'fixed-header': True, - 'density': 'compact', - 'style': { - 'height': '242px' - } - }, - 'content': [ - self.__build_dashboard_widget_torrent_table_head(fields=fields), - self.__build_dashboard_widget_torrent_table_body(data=data, field_count=field_count, downloader_id=downloader_id) - ] - }] - - def __get_downloader_transfer_info(self, - downloader_id: str) -> DownloaderTransferInfo: - """ - 获取下载器传输信息 - """ - if downloader_id == Downloader.QB.id: - return self.__get_qbittorrent_transfer_info() - elif downloader_id == Downloader.TR.id: - return self.__get_transmission_transfer_info() - else: - return DownloaderTransferInfo() - - def __get_qbittorrent_transfer_info(self) -> DownloaderTransferInfo: - """ - 获取qb下载器传输信息 - """ - result = DownloaderTransferInfo() - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return result - qbittorrent = self.__get_qbittorrent() - if not qbittorrent: - return result - info = qbittorrent.transfer_info() - if info: - result.download_speed = f'{StringUtils.str_filesize(info.get("dl_info_speed"))}/s' - result.upload_speed = f'{StringUtils.str_filesize(info.get("up_info_speed"))}/s' - result.download_size = StringUtils.str_filesize(info.get("dl_info_data")) - result.upload_size = StringUtils.str_filesize(info.get("up_info_data")) - maindata = self.__get_qbittorrent_maindata() - if maindata: - server_state = maindata.get("server_state") - if server_state: - result.free_space = StringUtils.str_filesize(server_state.get("free_space_on_disk")) - return result - - def __get_qbittorrent_maindata(self): - """ - 获取qb的maindata - """ - cache_key = "qbittorrent_maindata" - maindata = self.__ttl_cache.get(cache_key) - if not maindata: - qbittorrent = self.__get_qbittorrent() - if qbittorrent: - maindata = qbittorrent.qbc.sync_maindata() - self.__ttl_cache[cache_key] = maindata - return maindata - - def __get_transmission_transfer_info(self) -> DownloaderTransferInfo: - """ - 获取qb下载器传输信息 - """ - result = DownloaderTransferInfo() - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return result - transmission = self.__get_transmission() - if not transmission: - return result - info = transmission.transfer_info() - if info: - result.download_speed = f"{StringUtils.str_filesize(info.download_speed)}/s" - result.upload_speed = f"{StringUtils.str_filesize(info.upload_speed,)}/s" - result.download_size = StringUtils.str_filesize(info.current_stats.downloaded_bytes) - result.upload_size = StringUtils.str_filesize(info.current_stats.uploaded_bytes) - session = self.__get_transmission_session() - if session: - result.free_space = StringUtils.str_filesize(session.download_dir_free_space) - return result - - def __get_transmission_session(self): - """ - 获取tr的session - """ - cache_key = "transmission_session" - session = self.__ttl_cache.get(cache_key) - if not session: - transmission = self.__get_transmission() - if transmission: - session = transmission.get_session() - self.__ttl_cache[cache_key] = session - return session - - def __build_mdi_icon_svg_elememt(self, mdi_icon: str) -> dict: - """ - 构造 svg mdi 图标元素 - """ - if not mdi_icon: - return None - path = self.__mdi_icon_svg_path.get(mdi_icon) - if not path: - return None - return { - 'component': 'svg', - 'props': { - 'class': 'v-icon notranslate v-icon--size-default iconify iconify--mdi', - 'rounded': True, - 'width': '1em', - 'height': '1em', - 'viewBox': '0 0 24 24', - 'style': { - 'top': '-1px' - } - }, - 'content': [{ - 'component': 'path', - 'props': { - 'fill': 'currentColor', - 'd': path - } - }] - } - - def __build_dashboard_speed_widget_list_item_element(self, mdi_icon: str, label: str, value: str, is_last: bool = False) -> dict: - """ - 构造仪表板实时速率组件列表item元素 - """ - if not mdi_icon or not label or not value: - return None - div_style = { - 'display': 'grid', - 'grid-template-areas': '"prepend content append"', - 'grid-template-columns': 'max-content 1fr auto', - 'padding-bottom': '16px' - } - if is_last: - del div_style['padding-bottom'] - return { - 'component': 'div', - 'props': { - 'style': div_style - }, - 'content': [{ - 'component': 'div', - 'props': { - 'style': { - 'grid-area': 'prepend', - 'height': '21px' - } - }, - 'content': [self.__build_mdi_icon_svg_elememt(mdi_icon=mdi_icon)] - }, { - 'component': 'div', - 'props': { - 'style': { - 'grid-area': 'content', - 'margin-left': '15px' - } - }, - 'content': [{ - 'component': 'h6', - 'props': { - 'class': 'text-sm font-weight-medium mb-1' - }, - 'text': label - }] - }, { - 'component': 'div', - 'props': { - 'style': { - 'grid-area': 'append' - } - }, - 'content': [{ - 'component': 'h6', - 'props': { - 'class': 'text-sm font-weight-medium mb-2' - }, - 'text': value - }] - }] - } - - def __get_dashboard_speed_widget_elememts(self, downloader_id: str) -> list: - """ - 获取仪表板实时速率组件元素 - """ - if not downloader_id: - return None - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - data = self.__get_downloader_transfer_info(downloader_id=downloader_id) - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,操作取消') - return None - list_items = [ - self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-cloud-upload', label='总上传量', value=data.upload_size), - self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-download-box', label='总下载量', value=data.download_size), - self.__build_dashboard_speed_widget_list_item_element(mdi_icon='mdi-content-save', label='磁盘剩余空间', value=data.free_space, is_last=True), - ] - return [{ - 'component': 'div', - 'props': { - 'style': { - 'padding': '16px 0 0 0' - } - }, - 'content': [{ - 'component': 'div', - 'content': [{ - 'component': 'p', - 'props': { - 'class': 'text-h5 me-2' - }, - 'text': f'↑{data.upload_speed}' - }, { - 'component': 'p', - 'props': { - 'class': 'text-h4 me-2' - }, - 'text': f'↓{data.download_speed}' - }] - }, { - 'component': 'div', - 'props': { - 'class': 'card-list mt-9' - }, - 'content': list_items - }] - }] - - @eventmanager.register(EventType.DownloadAdded) - def listen_download_added_event(self, event: Event = None): - """ - 监听下载添加事件 - """ - logger.info('监听到下载添加事件') - if not self.get_state() or not self.__get_config_item(config_key='listen_download_event'): - logger.warn('插件状态无效或未开启监听,忽略事件') - return - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,忽略事件') - return - # 执行 - logger.info('下载添加事件监听任务执行开始...') - # enable_seeding=True是针对辅种添加种子并跳过校验的场景 - context = TaskContext().enable_seeding(True) \ - .enable_tagging(True) \ - .enable_delete(False) - _hash = event.event_data.get('hash') - if _hash: - context.select_torrent(torrent=_hash) - username = event.event_data.get('username') - if username: - context.set_username(username=username) - self.__block_run(context=context) - logger.info('下载添加事件监听任务执行结束') - - @eventmanager.register(EventType.DownloadFileDeleted) - def listen_download_file_deleted_event(self, event: Event = None): - """ - 监听源文件删除事件 - """ - logger.info('监听到源文件删除事件') - if not self.get_state() or not self.__get_config_item(config_key='listen_source_file_event'): - logger.warn('插件状态无效或未开启监听,忽略事件') - return - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,忽略事件') - return - # 执行 - logger.info('源文件删除事件监听任务执行开始...') - # 针对源文件监听事件只需要处理删种 - context = TaskContext().enable_seeding(False) \ - .enable_tagging(False) \ - .enable_delete(True) \ - .set_download_file_deleted_event_data(event.event_data) - self.__block_run(context=context) - logger.info('源文件删除事件监听任务执行结束') - - @eventmanager.register(EventType.DownloadDeleted) - def listen_download_deleted_event(self, event: Event = None): - """ - 监听下载任务删除事件 - """ - logger.info('监听到下载任务删除事件') - if not self.get_state() or not self.__get_config_item(config_key='listen_source_file_event'): - logger.warn('插件状态无效或未开启监听,忽略事件') - return - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return - torrents = event.event_data.get("torrents") - if not torrents: - logger.warn('事件信息无效,忽略事件') - return - torrents = [torrent for torrent in torrents if torrent] - if not torrents: - logger.warn('事件信息无效,忽略事件') - return - if self.__exit_event.is_set(): - logger.warn('插件服务正在退出,忽略事件') - return - # 执行 - logger.info('下载任务删除事件监听任务执行开始...') - # 针对下载任务删除事件只需要处理删种 - # 删除的种子信息 - torrent_info = torrents[0] - context = TaskContext().enable_seeding(False) \ - .enable_tagging(False) \ - .enable_delete(True) \ - .set_download_deleted_event_data(torrent_info) - self.__block_run(context=context) - logger.info('下载任务删除事件监听任务执行结束') diff --git a/plugins/downloaderhelper/convertor.py b/plugins/downloaderhelper/convertor.py deleted file mode 100644 index b85d586..0000000 --- a/plugins/downloaderhelper/convertor.py +++ /dev/null @@ -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 diff --git a/plugins/downloaderhelper/module.py b/plugins/downloaderhelper/module.py deleted file mode 100644 index 686b7aa..0000000 --- a/plugins/downloaderhelper/module.py +++ /dev/null @@ -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' diff --git a/plugins/mergesiteswitch/__init__.py b/plugins/mergesiteswitch/__init__.py deleted file mode 100644 index a286915..0000000 --- a/plugins/mergesiteswitch/__init__.py +++ /dev/null @@ -1,1007 +0,0 @@ -import inspect -import os -from typing import Any, List, Dict, Tuple - -from app.core.event import eventmanager, Event -from app.core.plugin import PluginManager -from app.db.site_oper import SiteOper -from app.db.systemconfig_oper import SystemConfigOper -from app.log import logger -from app.plugins import _PluginBase -from app.scheduler import Scheduler -from app.schemas.types import SystemConfigKey, EventType - - -class MergeSiteSwitch(_PluginBase): - # 插件名称 - plugin_name = "聚合站点开关" - # 插件描述 - plugin_desc = "统一管理所有与站点相关的开关。" - # 插件图标 - plugin_icon = "world.png" - # 插件版本 - plugin_version = "1.1" - # 插件作者 - plugin_author = "hotlcc" - # 作者主页 - author_url = "https://github.com/hotlcc" - # 插件配置项ID前缀 - plugin_config_prefix = "com.hotlcc.mergesiteswitch." - # 加载顺序 - plugin_order = 66 - # 可使用的用户级别 - auth_level = 2 - - # 依赖组件 - # 站点操作 - __site_oper: SiteOper = SiteOper() - # 系统配置操作 - __system_config_oper: SystemConfigOper = SystemConfigOper() - - # 其它插件ID - # 站点自动签到 - __plugin_id_auto_signin: str = 'AutoSignIn' - # 站点数据统计 - __plugin_id_site_statistic: str = 'SiteStatistic' - # IYUU自动辅种 - __plugin_id_iyuu_auto_seed: str = 'IYUUAutoSeed' - # 站点刷流 - __plugin_id_brush_flow: str = 'BrushFlow' - # 青蛙辅种助手 - __plugin_id_cross_seed: str = 'CrossSeed' - - # 配置相关 - # 插件缺省配置 - __config_default: Dict[str, Any] = {} - # 插件用户配置 - __config: Dict[str, Any] = {} - - def init_plugin(self, config: dict = None): - """ - 初始化插件 - """ - # 加载配置 - self.__config = config - # 当页面通过调用接口保存配置时保存其它各项配置 - if self.__check_stack_is_save_config_request(): - self.__set_config(config=config) - - def get_state(self) -> bool: - """ - 获取插件状态 - """ - return self.__check_any_follow_enable_sites() - - @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]]: - """ - 注册插件公共服务 - """ - pass - - def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: - """ - 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 - """ - # 站点选项 - site_options = self.__get_site_options() - # 已安装的插件IDs - installed_plugin_ids = self.__get_installed_plugin_ids() - # 建议的配置 - config_suggest = {} - - # 表单内容 - form_content = [{ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 12, 'xl': 12, 'lg': 12, 'md': 12, 'sm': 12, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'enable_sites', - 'label': '启用的站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '对应功能【站点管理 / 添加编辑站点 / 启用】' - } - }] - }] - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'search_sites', - 'label': '设定 / 搜索 / 搜索站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在搜索中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'search_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }, { - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'rss_sites', - 'label': '设定 / 订阅 / 订阅站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在订阅中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'rss_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }] - # 站点自动签到 - if self.__plugin_id_auto_signin in installed_plugin_ids: - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'signin_sites', - 'label': '插件 / 站点自动签到 / 签到站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在签到中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'signin_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'login_sites', - 'label': '插件 / 站点自动签到 / 登录站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在登录中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'login_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }) - # 站点数据统计 - if self.__plugin_id_site_statistic in installed_plugin_ids: - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'statistic_sites', - 'label': '插件 / 站点数据统计 / 统计站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '缺省时默认全部站点。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'statistic_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }) - # IYUU自动辅种 - if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids: - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'iyuu_seed_sites', - 'label': '插件 / IYUU自动辅种 / 辅种站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '缺省时默认全部站点。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'iyuu_seed_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }) - # 站点刷流 - if self.__plugin_id_brush_flow in installed_plugin_ids: - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'brush_flow_sites', - 'label': '插件 / 站点刷流 / 刷流站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在刷流中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'brush_flow_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }) - # 青蛙辅种助手 - if self.__plugin_id_cross_seed in installed_plugin_ids: - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 9, 'xl': 9, 'lg': 9, 'md': 9, 'sm': 8, 'xs': 12 - }, - 'content': [{ - 'component': 'VSelect', - 'props': { - 'model': 'cross_seed_sites', - 'label': '插件 / 青蛙辅种助手 / 辅种站点', - 'multiple': True, - 'chips': True, - 'items': site_options, - 'hint': '只有选中的站点才会在辅种中使用。' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 4, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'cross_seed_follow_enable_sites', - 'label': '跟随启用的站点', - 'hint': '与站点的启用状态保持一致,保存时会立即生效,并在后台监听站点状态变化实时生效。' - } - }] - }] - }) - # 提示信息 - form_content.append({ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 12, 'xl': 12, 'lg': 12, 'md': 12, 'sm': 12, 'xs': 12 - }, - 'content': [{ - 'component': 'VAlert', - 'props': { - 'type': 'warning', - 'variant': 'tonal' - }, - 'text': '点击保存后会立即生效,各项站点开关配置即会更新!' - }] - }] - }) - # 表单 - form = [{ - 'component': 'VForm', - 'content': form_content - }] - - # 重载配置 - self.__get_config() - return form, config_suggest - - def get_page(self) -> List[dict]: - pass - - def stop_service(self): - """ - 退出插件 - """ - pass - - @classmethod - def __check_stack_exist_function(cls, package_name: str, function_name: str) -> bool: - """ - 判断当前调用栈是否存在指定的函数 - """ - if not package_name or not function_name: - return False - for stack in inspect.stack(): - if stack.function != 'set_plugin_config': - continue - package_path = package_name.replace('.', os.sep) - if stack.filename.endswith(f"{package_path}.py") or stack.filename.endswith(f"{package_path}{os.sep}__init__.py"): - return True - return False - - @classmethod - def __check_stack_is_save_config_request(cls) -> bool: - """ - 判断当前调用栈是否来源于“插件配置保存”接口 - """ - return cls.__check_stack_exist_function('app.api.endpoints.plugin', 'set_plugin_config') - - def __get_config_value(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 and use_default: - config_default = self.__config_default if self.__config_default else {} - config_value = config_default.get(config_key) - return config_value - - def __check_follow_enable_sites(self, config_key: str, plugin_id: str = None, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断某个跟随按钮是否打开 - :param plugin_id: 传插件ID时还要同时根据插件是否安装进行判断 - """ - if not self.__get_config_value(config_key=config_key): - return False - if not plugin_id: - return True - if not installed_plugin_ids: - installed_plugin_ids = self.__get_installed_plugin_ids() - return plugin_id in installed_plugin_ids - - def __check_search_follow_enable_sites(self) -> bool: - """ - 判断搜索站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='search_follow_enable_sites') - - def __check_rss_follow_enable_sites(self) -> bool: - """ - 判断订阅站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='rss_follow_enable_sites') - - def __check_signin_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断签到站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='signin_follow_enable_sites', plugin_id=self.__plugin_id_auto_signin, installed_plugin_ids=installed_plugin_ids) - - def __check_login_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断登录站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='login_follow_enable_sites', plugin_id=self.__plugin_id_auto_signin, installed_plugin_ids=installed_plugin_ids) - - def __check_statistic_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断统计站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='statistic_follow_enable_sites', plugin_id=self.__plugin_id_site_statistic, installed_plugin_ids=installed_plugin_ids) - - def __check_iyuu_seed_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断iyuu辅种站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='iyuu_seed_follow_enable_sites', plugin_id=self.__plugin_id_iyuu_auto_seed, installed_plugin_ids=installed_plugin_ids) - - def __check_brush_flow_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断刷流站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='brush_flow_follow_enable_sites', plugin_id=self.__plugin_id_brush_flow, installed_plugin_ids=installed_plugin_ids) - - def __check_cross_seed_follow_enable_sites(self, installed_plugin_ids: List[str] = None) -> bool: - """ - 判断青蛙辅种站点的跟随按钮是否打开 - """ - return self.__check_follow_enable_sites(config_key='cross_seed_follow_enable_sites', plugin_id=self.__plugin_id_cross_seed, installed_plugin_ids=installed_plugin_ids) - - def __check_any_follow_enable_sites(self) -> bool: - """ - 判断是否开启任意跟随按钮 - """ - # 已安装的插件IDs - installed_plugin_ids = self.__get_installed_plugin_ids() - return True if self.__check_search_follow_enable_sites() \ - or self.__check_rss_follow_enable_sites() \ - or self.__check_signin_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \ - or self.__check_login_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \ - or self.__check_statistic_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \ - or self.__check_iyuu_seed_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) \ - or self.__check_brush_flow_follow_enable_sites(installed_plugin_ids=installed_plugin_ids) else False - - def __get_site_options(self) -> List[Dict[str, Any]]: - """ - 获取站点选项 - """ - sites = self.__site_oper.list_order_by_pri() - if not sites: - return [] - return [{ - 'title': site.name, - 'value': site.id - } for site in sites if site] - - def __get_installed_plugin_ids(self): - """ - 获取已安装的插件IDs - """ - installed_plugin_ids = self.__system_config_oper.get(SystemConfigKey.UserInstalledPlugins) - return installed_plugin_ids if installed_plugin_ids else [] - - def __get_config(self): - """ - 获取配置,包含聚合外部配置 - """ - config = self.get_config() - if not config: - config = {} - config.update({ - 'enable_sites': self.__get_enable_site_ids(), - 'search_sites': self.__get_search_site_ids(), - 'rss_sites': self.__get_rss_site_ids(), - }) - # 已安装的插件IDs - installed_plugin_ids = self.__get_installed_plugin_ids() - if self.__plugin_id_auto_signin in installed_plugin_ids: - config.update({ - 'signin_sites': self.__get_signin_site_ids(), - 'login_sites': self.__get_login_site_ids(), - }) - if self.__plugin_id_site_statistic in installed_plugin_ids: - config.update({ - 'statistic_sites': self.__get_statistic_site_ids(), - }) - if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids: - config.update({ - 'iyuu_seed_sites': self.__get_iyuu_seed_site_ids(), - }) - if self.__plugin_id_brush_flow in installed_plugin_ids: - config.update({ - 'brush_flow_sites': self.__get_brush_flow_site_ids(), - }) - if self.__plugin_id_cross_seed in installed_plugin_ids: - config.update({ - 'cross_seed_sites': self.__get_cross_seed_site_ids(), - }) - self.update_config(config=config) - return config - - def __pre_config_follow_enable_sites(self, config: dict) -> dict: - """ - 处理跟随站点 - """ - if not config: - config = {} - enable_sites = config.get('enable_sites') or [] - if config.get('search_follow_enable_sites'): - config.update({"search_sites": enable_sites.copy()}) - if config.get('rss_follow_enable_sites'): - config.update({"rss_sites": enable_sites.copy()}) - if config.get('signin_follow_enable_sites'): - config.update({"signin_sites": enable_sites.copy()}) - if config.get('login_follow_enable_sites'): - config.update({"login_sites": enable_sites.copy()}) - if config.get('statistic_follow_enable_sites'): - config.update({"statistic_sites": enable_sites.copy()}) - if config.get('iyuu_seed_follow_enable_sites'): - config.update({"iyuu_seed_sites": enable_sites.copy()}) - if config.get('brush_flow_follow_enable_sites'): - config.update({"brush_flow_sites": enable_sites.copy()}) - if config.get('cross_seed_follow_enable_sites'): - config.update({"cross_seed_sites": enable_sites.copy()}) - return config - - def __pre_config(self, config: dict) -> dict: - """ - 预处理配置 - """ - config = self.__pre_config_follow_enable_sites(config=config) - logger.debug(f"配置预处理完成: {config}") - return config - - def __set_config(self, config: dict): - """ - 保存配置,包含保存外部配置到各自表 - """ - # 预处理配置 - config = self.__pre_config(config=config) - # 更新各项配置 - self.update_config(config=config) - logger.info("插件配置更新完成") - self.__set_enable_site_ids(config.get('enable_sites')) - self.__set_search_site_ids(config.get('search_sites')) - self.__set_rss_site_ids(config.get('rss_sites')) - # 已安装的插件IDs - installed_plugin_ids = self.__get_installed_plugin_ids() - if self.__plugin_id_auto_signin in installed_plugin_ids: - self.__set_signin_site_ids(config.get('signin_sites')) - self.__set_login_site_ids(config.get('login_sites')) - if self.__plugin_id_site_statistic in installed_plugin_ids: - self.__set_statistic_site_ids(config.get('statistic_sites')) - if self.__plugin_id_iyuu_auto_seed in installed_plugin_ids: - self.__set_iyuu_seed_site_ids(config.get('iyuu_seed_sites')) - if self.__plugin_id_brush_flow in installed_plugin_ids: - self.__set_brush_flow_site_ids(config.get('brush_flow_sites')) - if self.__plugin_id_cross_seed in installed_plugin_ids: - self.__set_cross_seed_site_ids(config.get('cross_seed_sites')) - return config - - def __get_enable_site_ids(self) -> List[int]: - """ - 获取启用的站点IDs - """ - sites = self.__site_oper.list_order_by_pri() - if not sites: - return [] - return [site.id for site in sites if site and site.is_active] - - def __set_enable_site_ids(self, site_ids: List[int]): - """ - 设置启用的站点IDs - """ - sites = self.__site_oper.list_order_by_pri() - if not sites: - return - for site in sites: - if not site_ids or site.id not in site_ids: - if site.is_active: - self.__site_oper.update(site.id, {'is_active': False}) - else: - if not site.is_active: - self.__site_oper.update(site.id, {'is_active': True}) - logger.info("启用的站点配置完成") - - def __get_search_site_ids(self) -> List[int]: - """ - 获取搜索站点IDs - """ - sites = self.__system_config_oper.get(SystemConfigKey.IndexerSites) - return sites if sites else [] - - def __set_search_site_ids(self, site_ids: List[int]): - """ - 设置搜索站点IDs - """ - self.__system_config_oper.set(SystemConfigKey.IndexerSites, site_ids) - logger.info("搜索站点配置完成") - - def __get_rss_site_ids(self) -> List[int]: - """ - 获取订阅站点IDs - """ - sites = self.__system_config_oper.get(SystemConfigKey.RssSites) - return sites if sites else [] - - def __set_rss_site_ids(self, site_ids: List[int]): - """ - 设置订阅站点IDs - """ - self.__system_config_oper.set(SystemConfigKey.RssSites, site_ids) - logger.info("订阅站点配置完成") - - def __get_plugin_config_value(self, plugin_id: str, config_key: str) -> Any: - """ - 获取插件配置值 - """ - if not plugin_id or not config_key: - return None - config = self.get_config(plugin_id) - if not config: - return None - return config.get(config_key) - - def __reload_plugin_config(self, plugin_id: str, config: dict = None): - """ - 重载插件配置 - """ - if not plugin_id: - return - if not config: - config = self.get_config(plugin_id) - # 重新生效插件 - PluginManager().init_plugin(plugin_id, config) - # 注册插件服务 - Scheduler().update_plugin_job(plugin_id) - - def __set_plugin_config_value(self, plugin_id: str, config_key: str, config_value: Any) -> Any: - """ - 设置插件配置值 - """ - if not plugin_id or not config_key: - return - config = self.get_config(plugin_id) - if not config: - config = {} - config.update({config_key: config_value}) - self.update_config(plugin_id=plugin_id, config=config) - self.__reload_plugin_config(plugin_id=plugin_id, config=config) - - def __get_signin_site_ids(self) -> List[int]: - """ - 获取签到站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_auto_signin, 'sign_sites') - return sites if sites else [] - - def __set_signin_site_ids(self, site_ids: List[int]): - """ - 设置签到站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_auto_signin, 'sign_sites', site_ids) - logger.info("签到站点配置完成") - - def __get_login_site_ids(self) -> List[int]: - """ - 获取登录站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_auto_signin, 'login_sites') - return sites if sites else [] - - def __set_login_site_ids(self, site_ids: List[int]): - """ - 设置登录站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_auto_signin, 'login_sites', site_ids) - logger.info("登录站点配置完成") - - def __get_statistic_site_ids(self) -> List[int]: - """ - 获取统计站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_site_statistic, 'statistic_sites') - return sites if sites else [] - - def __set_statistic_site_ids(self, site_ids: List[int]): - """ - 设置统计站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_site_statistic, 'statistic_sites', site_ids) - logger.info("统计站点配置完成") - - def __get_iyuu_seed_site_ids(self) -> List[int]: - """ - 获取iyuu自动辅种站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_iyuu_auto_seed, 'sites') - return sites if sites else [] - - def __set_iyuu_seed_site_ids(self, site_ids: List[int]): - """ - 设置iyuu自动辅种站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_iyuu_auto_seed, 'sites', site_ids) - logger.info("IYUU辅种站点配置完成") - - def __get_brush_flow_site_ids(self) -> List[int]: - """ - 获取刷流站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_brush_flow, 'brushsites') - return sites if sites else [] - - def __set_brush_flow_site_ids(self, site_ids: List[int]): - """ - 设置刷流站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_brush_flow, 'brushsites', site_ids) - logger.info("刷流站点配置完成") - - def __get_cross_seed_site_ids(self) -> List[int]: - """ - 获取青蛙辅种站点IDs - """ - sites = self.__get_plugin_config_value(self.__plugin_id_cross_seed, 'sites') - return sites if sites else [] - - def __set_cross_seed_site_ids(self, site_ids: List[int]): - """ - 设置青蛙辅种站点IDs - """ - self.__set_plugin_config_value(self.__plugin_id_cross_seed, 'sites', site_ids) - logger.info("青蛙辅种站点配置完成") - - def __update_search_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_search_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_search_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_search_site_ids(site_ids=site_ids) - - def __update_rss_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_rss_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_rss_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_rss_site_ids(site_ids=site_ids) - - def __update_signin_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_signin_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_signin_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_signin_site_ids(site_ids=site_ids) - - def __update_login_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_login_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_login_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_login_site_ids(site_ids=site_ids) - - def __update_statistic_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_statistic_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_statistic_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_statistic_site_ids(site_ids=site_ids) - - def __update_iyuu_seed_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_iyuu_seed_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_iyuu_seed_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_iyuu_seed_site_ids(site_ids=site_ids) - - def __update_brush_flow_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_brush_flow_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_brush_flow_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_brush_flow_site_ids(site_ids=site_ids) - - def __update_cross_seed_site_ids_by_site(self, site_id: int, site_status: bool): - if site_id == None: - return - site_ids = self.__get_cross_seed_site_ids() or [] - if site_id not in site_ids and site_status: - site_ids.append(site_id) - self.__set_cross_seed_site_ids(site_ids=site_ids) - elif site_id in site_ids and not site_status: - site_ids.remove(site_id) - self.__set_cross_seed_site_ids(site_ids=site_ids) - - def __update_site_ids_for_site_event(self, site_id: int, site_status: bool): - """ - 针对站点事件更新各项配置 - """ - if site_id == None: - return - if self.__check_search_follow_enable_sites(): - self.__update_search_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_rss_follow_enable_sites(): - self.__update_rss_site_ids_by_site(site_id=site_id, site_status=site_status) - # 已安装的插件IDs - installed_plugin_ids = self.__get_installed_plugin_ids() - if self.__check_signin_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_signin_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_login_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_login_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_statistic_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_statistic_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_iyuu_seed_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_iyuu_seed_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_brush_flow_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_brush_flow_site_ids_by_site(site_id=site_id, site_status=site_status) - if self.__check_cross_seed_follow_enable_sites(installed_plugin_ids=installed_plugin_ids): - self.__update_cross_seed_site_ids_by_site(site_id=site_id, site_status=site_status) - - @eventmanager.register(EventType.SiteUpdated) - def listen_site_updated_event(self, event: Event = None): - """ - 监听站点更新事件 - """ - logger.info('监听到站点更新事件') - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return - domain = event.event_data.get("domain") - if not domain: - logger.warn('事件信息无效,忽略事件') - return - if not self.__check_any_follow_enable_sites(): - logger.warn('未打开任一【跟随启用的站点】开关,忽略事件') - return - site = self.__site_oper.get_by_domain(domain=domain) - if not site: - logger.warn(f'目标站点不存在,忽略事件: domain = {domain}') - return - self.__update_site_ids_for_site_event(site_id=site.id, site_status=site.is_active) - logger.info('站点更新事件监听任务执行完成') - - @eventmanager.register(EventType.SiteDeleted) - def listen_site_deleted_event(self, event: Event = None): - """ - 监听站点删除事件 - """ - logger.info('监听到站点删除事件') - if not event or not event.event_data: - logger.warn('事件信息无效,忽略事件') - return - site_id = event.event_data.get("site_id") - if site_id == None: - logger.warn('事件信息无效,忽略事件') - return - if not self.__check_any_follow_enable_sites(): - logger.warn('未打开任一【跟随启用的站点】开关,忽略事件') - return - self.__update_site_ids_for_site_event(site_id=site_id, site_status=False) - logger.info('站点删除事件监听任务执行完成') diff --git a/plugins/pluginautoupgrade/__init__.py b/plugins/pluginautoupgrade/__init__.py deleted file mode 100644 index 8c23609..0000000 --- a/plugins/pluginautoupgrade/__init__.py +++ /dev/null @@ -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