diff --git a/README.md b/README.md index e0d332c..0dc5fcd 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ class EventType(Enum): SubscribeAdded = "subscribe.added" # 订阅已完成 SubscribeComplete = "subscribe.complete" + # 系统错误 + SystemError = "system.error" ``` ### 2. 如何在插件中实现远程命令响应? @@ -426,7 +428,7 @@ class EventType(Enum): - **请不要添加对黄赌毒站点的支持,否则随时封闭接口。** ### 7. 如何在插件中调用API接口? -- 目前仅在插件的数据页面支持`GET/POST`API接口调用,可调用插件自身、主程序或其它插件的API(v1.8.4+)。 +- `v1.8.4+` 在插件的数据页面支持`GET/POST`API接口调用,可调用插件自身、主程序或其它插件的API。 - 在`get_page`中定义好元素的事件,以及相应的API参数,具体可参考插件`豆瓣想看`: ```json { @@ -448,11 +450,27 @@ class EventType(Enum): ### 8. 如何将插件内容显示到仪表板? - `v1.8.7+` 支持将插件的内容显示到仪表盘,并支持定义占据的单元格大小,插件产生的仪表板仅管理员可见。 - 1. 根据插件需要展示的Widget内容规划展示内容的样式和规格,也可设计多个规格样式并提供配置项供用户选择。 -- 2. 实现 `get_dashboard` 方法,返回仪表盘的配置信息,包括仪表盘的cols列配置(适配不同屏幕),以及仪表盘的页面配置json,具体可参考插件`站点数据统计`: +- 2. 实现 `get_dashboard_meta` 方法,定义仪表板key及名称,支持一个插件有多个仪表板: ```python -def get_dashboard(self, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: +def get_dashboard_meta(self) -> Optional[List[Dict[str, str]]]: """ - 获取插件仪表盘页面,需要返回:1、仪表板cols配置字典;2、全局配置(自动刷新等);2、仪表板页面元素配置json(含数据) + 获取插件仪表盘元信息 + 返回示例: + [{ + "key": "dashboard1", // 仪表盘的key,在当前插件范围唯一 + "name": "仪表盘1" // 仪表盘的名称 + }, { + "key": "dashboard2", + "name": "仪表盘2" + }] + """ + pass +``` +- 3. 实现 `get_dashboard` 方法,根据key返回仪表盘的详细配置信息,包括仪表盘的cols列配置(适配不同屏幕),以及仪表盘的页面配置json,具体可参考插件`站点数据统计`: +```python +def get_dashboard(self, key: str, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据) 1、col配置参考: { "cols": 12, "md": 6 @@ -465,8 +483,10 @@ def get_dashboard(self, **kwargs) -> Optional[Tuple[Dict[str, Any], Dict[str, An "subtitle": "组件子标题", // 组件子标题,缺省时不展示子标题 } 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ - + kwargs参数可获取的值:1、user_agent:浏览器UA + + :param key: 仪表盘key,根据指定的key返回相应的仪表盘数据,缺省时返回一个固定的仪表盘数据(兼容旧版) """ pass ``` diff --git a/icons/TrendingShow.jpg b/icons/TrendingShow.jpg new file mode 100644 index 0000000..5f58e0b Binary files /dev/null and b/icons/TrendingShow.jpg differ diff --git a/package.json b/package.json index bac94f0..a8370c7 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "name": "站点自动签到", "description": "自动模拟登录、签到站点。", "labels": "站点", - "version": "2.3.1", + "version": "2.3.2", "icon": "signin.png", "author": "thsrite", "level": 2, "history": { + "v2.3.2": "修复YemaPT登录失败,支持YemaPT自动签到", "v2.3.1": "修复签到报错问题", "v2.3": "优化模拟登录逻辑,支持YemaPT模拟登录", "v2.2": "适配馒头最新变化,需要升级至v1.8.5+版本且维护好Authorization", @@ -74,11 +75,13 @@ "name": "目录监控", "description": "监控目录文件发生变化时实时整理到媒体库。", "labels": "文件整理", - "version": "2.0", + "version": "2.2", "icon": "directory.png", "author": "jxxghp", "level": 1, "history": { + "v2.2": "更新目录设置说明", + "v2.1": "增加了元数据刮削开关,升级后需要手动打开,否则默认不刮削", "v2.0": "增强API安全性", "v1.9": "修复目录监控不能正确获取下载历史记录进行识别的问题" } @@ -110,10 +113,13 @@ "name": "媒体库刮削", "description": "定时对媒体库进行刮削,补齐缺失元数据和图片。", "labels": "刮削", - "version": "1.4", + "version": "1.4.1", "icon": "scraper.png", "author": "jxxghp", - "level": 1 + "level": 1, + "history": { + "v1.4.1": "修复nfo文件读取失败时任务中断问题" + } }, "TorrentRemover": { "name": "自动删种", @@ -128,11 +134,12 @@ "name": "媒体文件同步删除", "description": "同步删除历史记录、源文件和下载任务。", "labels": "文件整理", - "version": "1.5", + "version": "1.6", "icon": "mediasyncdel.png", "author": "thsrite", "level": 1, "history": { + "v1.6": "修复删除辅种", "v1.5": "支持手动删除订阅历史记录(本次更新之后的历史)" } }, @@ -278,11 +285,12 @@ "name": "整理VCB动漫压制组作品", "description": "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理", "labels": "文件整理,识别", - "version": "1.7.1", + "version": "1.8", "icon": "vcbmonitor.png", "author": "pixel@qingwa", "level": 2, "history": { + "v1.8": "增加了元数据刮削开关,升级后需要手动打开,否则默认不刮削", "v1.7.1": "修复偶尔安装失败问题" } }, @@ -302,11 +310,12 @@ "name": "自定义订阅", "description": "定时刷新RSS报文,识别内容后添加订阅或直接下载。", "labels": "订阅", - "version": "1.3", + "version": "1.4", "icon": "rss.png", "author": "jxxghp", "level": 2, "history": { + "v1.4": "修复剧集本地是否存在的判断错误问题", "v1.3": "支持手动删除订阅历史记录" } }, @@ -529,7 +538,7 @@ "name": "二级分类策略", "description": "编辑下载目录和媒体库目录的二级分类规则。", "labels": "文件整理", - "version": "1.1", + "version": "1.2", "icon": "Bookstack_A.png", "author": "jxxghp", "level": 1 @@ -631,11 +640,15 @@ "name": "下载器助手", "description": "自动做种、站点标签、自动删种。", "labels": "下载管理,仪表板", - "version": "2.2", + "version": "2.6", "icon": "DownloaderHelper.png", "author": "hotlcc", - "level": 2, + "level": 1, "history": { + "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的适配度。", @@ -679,11 +692,12 @@ "name": "插件自动升级", "description": "定时检测、升级插件。", "labels": "自动更新", - "version": "1.8", + "version": "1.9", "icon": "PluginAutoUpgrade.png", "author": "hotlcc", "level": 1, "history": { + "v1.9": "优化通知类型。主程序需升级至v1.9.2及以上版本,否则插件功能异常!", "v1.8": "修复重置插件后丢失配置建议的问题。", "v1.7": "修复了一些BUG。", "v1.6": "修正数字配置值提交为字符串导致的问题。", @@ -727,11 +741,14 @@ "name": "清理QB无效做种", "description": "清理已经被站点删除的种子及对应源文件,仅支持QB", "labels": "Qbittorrent", - "version": "1.5", + "version": "1.8", "icon": "clean_a.png", "author": "DzAvril", "level": 1, "history": { + "v1.8": "增加远程命令切换全量通知;修复bug", + "v1.7": "修复因消息内容包含'_'导致telegram API调用失败的问题", + "v1.6": "修复当种子有多个标签时,通过标签过滤不删除种子会失效的问题", "v1.5": "1. 增加通过分类、标签过滤不删除种子功能;2. 全量通知提供更多信息", "v1.4": "修复插件功能失效的问题", "v1.3": "1. 增加远程命令 2. 根据tracker error_message字段进行过滤,避免误删", @@ -745,7 +762,16 @@ "description": "在仪表板中显示流行趋势海报轮播图。", "labels": "仪表板", "version": "1.0", - "icon": "Dsphoto_A.png", + "icon": "TrendingShow.jpg", + "author": "jxxghp", + "level": 1 + }, + "DailyWord": { + "name": "每日一言", + "description": "在仪表板中显示每日一言卡片。", + "labels": "仪表板", + "version": "1.1", + "icon": "Calibre_B.png", "author": "jxxghp", "level": 1 } diff --git a/plugins/autosignin/__init__.py b/plugins/autosignin/__init__.py index eb6f09b..6c3a439 100644 --- a/plugins/autosignin/__init__.py +++ b/plugins/autosignin/__init__.py @@ -38,7 +38,7 @@ class AutoSignIn(_PluginBase): # 插件图标 plugin_icon = "signin.png" # 插件版本 - plugin_version = "2.3.1" + plugin_version = "2.3.2" # 插件作者 plugin_author = "thsrite" # 作者主页 diff --git a/plugins/autosignin/sites/yema.py b/plugins/autosignin/sites/yema.py index dc62cc9..879611f 100644 --- a/plugins/autosignin/sites/yema.py +++ b/plugins/autosignin/sites/yema.py @@ -8,9 +8,9 @@ from app.plugins.autosignin.sites import _ISiteSigninHandler from app.utils.http import RequestUtils -class MTorrent(_ISiteSigninHandler): +class YemaPT(_ISiteSigninHandler): """ - m-team签到 + YemaPT 签到 """ # 匹配的站点Url,每一个实现类都需要设置为自己的站点Url site_url = "yemapt.org" @@ -26,7 +26,7 @@ class MTorrent(_ISiteSigninHandler): def signin(self, site_info: CommentedMap) -> Tuple[bool, str]: """ - 执行签到操作,馒头实际没有签到,非仿真模式下需要更新访问时间 + 执行签到操作 :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息 :return: 签到结果信息 """ @@ -35,19 +35,20 @@ class MTorrent(_ISiteSigninHandler): "User-Agent": site_info.get("ua"), "Accept": "application/json, text/plain, */*", } - # 更新最后访问时间 - res = RequestUtils(headers=headers, - timeout=15, - cookies=site_info.get("cookie"), - proxies=settings.PROXY if site_info.get("proxy") else None, - referer=site_info.get('url') - ).post_res(url=urljoin(site_info.get('url'), "api/user/profile")) + # 获取用户信息,更新最后访问时间 + res = (RequestUtils(headers=headers, + timeout=15, + cookies=site_info.get("cookie"), + proxies=settings.PROXY if site_info.get("proxy") else None, + referer=site_info.get('url') + ).get_res(urljoin(site_info.get('url'), "api/consumer/checkIn"))) + if res and res.json().get("success"): - return True, "模拟登录成功" + return True, "签到成功" elif res is not None: - return False, f"模拟登录失败,状态码:{res.status_code}" + return False, f"签到失败,签到结果:{res.json().get('errorMessage')}" else: - return False, "模拟登录失败,无法打开网站" + return False, "签到失败,无法打开网站" def login(self, site_info: CommentedMap) -> Tuple[bool, str]: """ @@ -55,4 +56,23 @@ class MTorrent(_ISiteSigninHandler): :param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息 :return: 登录结果信息 """ - return self.signin(site_info) + + headers = { + "Content-Type": "application/json", + "User-Agent": site_info.get("ua"), + "Accept": "application/json, text/plain, */*", + } + # 获取用户信息,更新最后访问时间 + res = (RequestUtils(headers=headers, + timeout=15, + cookies=site_info.get("cookie"), + proxies=settings.PROXY if site_info.get("proxy") else None, + referer=site_info.get('url') + ).get_res(urljoin(site_info.get('url'), "api/user/profile"))) + + if res and res.json().get("success"): + return True, "模拟登录成功" + elif res is not None: + return False, f"模拟登录失败,状态码:{res.status_code}" + else: + return False, "模拟登录失败,无法打开网站" diff --git a/plugins/categoryeditor/__init__.py b/plugins/categoryeditor/__init__.py index a7f2cfe..cafc705 100644 --- a/plugins/categoryeditor/__init__.py +++ b/plugins/categoryeditor/__init__.py @@ -13,7 +13,7 @@ class CategoryEditor(_PluginBase): # 插件图标 plugin_icon = "Bookstack_A.png" # 插件版本 - plugin_version = "1.1" + plugin_version = "1.2" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -39,12 +39,9 @@ class CategoryEditor(_PluginBase): # 写入文件 if self._enabled: self.user_yaml.write_text(self._content, encoding="utf-8") - if not settings.LIBRARY_CATEGORY: - self.systemmessage.put("二级分类未开启,策略已保存但未生效!", title="二级分类策略") - return # 立即生效 CategoryHelper().init() - self.systemmessage.put("二级分类策略已更新!", title="二级分类策略") + self.systemmessage.put("二级分类策略已更新,请注意同步调整目录设置!", title="二级分类策略") def get_state(self) -> bool: return self._enabled diff --git a/plugins/cleaninvalidseed/__init__.py b/plugins/cleaninvalidseed/__init__.py index aee2cea..2457f4c 100644 --- a/plugins/cleaninvalidseed/__init__.py +++ b/plugins/cleaninvalidseed/__init__.py @@ -28,7 +28,7 @@ class CleanInvalidSeed(_PluginBase): # 插件图标 plugin_icon = "clean_a.png" # 插件版本 - plugin_version = "1.5" + plugin_version = "1.8" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -94,22 +94,7 @@ class CleanInvalidSeed(_PluginBase): ) # 关闭一次性开关 self._onlyonce = False - self.update_config( - { - "onlyonce": False, - "cron": self._cron, - "enabled": self._enabled, - "notify": self._notify, - "delete_invalid_torrents": self._delete_invalid_torrents, - "delete_invalid_files": self._delete_invalid_files, - "detect_invalid_files": self._detect_invalid_files, - "notify_all": self._notify_all, - "download_dirs": self._download_dirs, - "exclude_keywords": self._exclude_keywords, - "exclude_categories": self._exclude_categories, - "exclude_labels": self._exclude_labels, - } - ) + self._update_config() # 启动任务 if self._scheduler.get_jobs(): @@ -119,6 +104,24 @@ class CleanInvalidSeed(_PluginBase): def get_state(self) -> bool: return self._enabled + def _update_config(self): + self.update_config( + { + "onlyonce": False, + "cron": self._cron, + "enabled": self._enabled, + "notify": self._notify, + "delete_invalid_torrents": self._delete_invalid_torrents, + "delete_invalid_files": self._delete_invalid_files, + "detect_invalid_files": self._detect_invalid_files, + "notify_all": self._notify_all, + "download_dirs": self._download_dirs, + "exclude_keywords": self._exclude_keywords, + "exclude_categories": self._exclude_categories, + "exclude_labels": self._exclude_labels, + } + ) + @staticmethod def get_command() -> List[Dict[str, Any]]: """ @@ -154,6 +157,13 @@ class CleanInvalidSeed(_PluginBase): "category": "QB", "data": {"action": "delete_invalid_files"}, }, + { + "cmd": "/toggle_notify_all", + "event": EventType.PluginAction, + "desc": "QB清理插件切换全量通知", + "category": "QB", + "data": {"action": "toggle_notify_all"}, + }, ] @eventmanager.register(EventType.PluginAction) @@ -189,6 +199,22 @@ class CleanInvalidSeed(_PluginBase): logger.info("收到远程命令,开始清理无效源文件") self._delete_invalid_files = True self.detect_invalid_files() + elif event_data.get("action") == "toggle_notify_all": + self._notify_all = not self._notify_all + self._update_config() + if self._notify_all: + self.post_message( + channel=event.event_data.get("channel"), + title="已开启全量通知", + userid=event.event_data.get("user"), + ) + else: + self.post_message( + channel=event.event_data.get("channel"), + title="已关闭全量通知", + userid=event.event_data.get("user"), + ) + return else: logger.error("收到未知远程命令") return @@ -256,8 +282,8 @@ class CleanInvalidSeed(_PluginBase): # tracker未工作,但暂时不能判定为失效做种,需人工判断 tracker_not_working_torrents = [] working_tracker_set = set() - exclude_categories = self._exclude_categories.split("\n") - exclude_labels = self._exclude_labels.split("\n") + exclude_categories = self._exclude_categories.split("\n") if self._exclude_categories else [] + exclude_labels = self._exclude_labels.split("\n") if self._exclude_labels else [] # 第一轮筛选出所有未工作的种子 for torrent in all_torrents: trackers = torrent.trackers @@ -307,7 +333,7 @@ class CleanInvalidSeed(_PluginBase): if torrent.category in exclude_categories: is_excluded = True invalid_torrents_exclude_categories.append(torrent) - torrent_labels = torrent.tags.split(",") + torrent_labels = [tag.strip() for tag in torrent.tags.split(",")] for label in torrent_labels: if label in exclude_labels: is_excluded = True @@ -398,29 +424,34 @@ class CleanInvalidSeed(_PluginBase): logger.info(exclude_labels_msg) # 通知 if self._notify: + invalid_msg = invalid_msg.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=invalid_msg, ) if self._notify_all: + tracker_not_working_msg = tracker_not_working_msg.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=tracker_not_working_msg, ) if self._delete_invalid_torrents: + deleted_msg = deleted_msg.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=deleted_msg, ) if self._notify_all: + exclude_categories_msg = exclude_categories_msg.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", text=exclude_categories_msg, ) + exclude_labels_msg = exclude_labels_msg.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", @@ -437,7 +468,15 @@ class CleanInvalidSeed(_PluginBase): source_paths = [] total_size = 0 deleted_file_cnt = 0 - exclude_key_words = self._exclude_keywords.split("\n") + exclude_key_words = self._exclude_keywords.split("\n") if self._exclude_keywords else [] + if not self._download_dirs: + logger.error("未配置下载目录,无法检测未做种无效源文件") + self.post_message( + mtype=NotificationType.SiteMessage, + title=f"【检测无效源文件】", + text="未配置下载目录,无法检测未做种无效源文件", + ) + return for path in self._download_dirs.split("\n"): mp_path, qb_path = path.split(":") source_path_map[mp_path] = qb_path @@ -489,6 +528,7 @@ class CleanInvalidSeed(_PluginBase): message += f"***已删除无效源文件,释放{StringUtils.str_filesize(total_size)}空间!***\n" logger.info(message) if self._notify: + message = message.replace('_', '\_') self.post_message( mtype=NotificationType.SiteMessage, title=f"【清理无效做种】", @@ -754,3 +794,23 @@ class CleanInvalidSeed(_PluginBase): self._scheduler = None except Exception as e: logger.error("退出插件失败:%s" % str(e)) + + +if __name__ == "__main__": + clean = CleanInvalidSeed() + config = { + "enabled": True, + "notify": True, + "download_dirs": "/sata16t/春天:/保种/春天\n/sata16t/观众:/保种/观众\n/sata16t/UB:/保种/UB\n/sata16t/听听歌:/保种/听听歌\n/ssd/Download/shualiu:/Downloads/shualiu", + "delete_invalid_torrents": False, + "delete_invalid_files": False, + "detect_invalid_files": True, + "notify_all": False, + "onlyonce": False, + "cron": "0 0 * * *", + "exclude_keywords": "ABF-075\nIPZZ-002-C_GG5\nIPZZ-061\n.!qB", + "exclude_categories": "电影", + "exclude_labels": "春天", + } + clean.init_plugin(config) + clean.clean_invalid_seed() \ No newline at end of file diff --git a/plugins/crossseed/__init__.py b/plugins/crossseed/__init__.py index 1ef301a..82fb138 100644 --- a/plugins/crossseed/__init__.py +++ b/plugins/crossseed/__init__.py @@ -294,7 +294,8 @@ class CrossSeed(_PluginBase): for site_key in self._token.strip().split("\n"): site_key_arr = re.split(r"[\s::]+", site_key.strip()) site_name = site_key_arr[0] - site_name_key_map[site_name] = site_key_arr[1] + if len(site_key_arr) > 1: + site_name_key_map[site_name] = site_key_arr[1] if len(site_key_arr) > 2: if str.isdigit(site_key_arr[2]): site_name_gap_map[site_name] = int(site_key_arr[2]) diff --git a/plugins/dailyword/__init__.py b/plugins/dailyword/__init__.py new file mode 100644 index 0000000..a64bc4e --- /dev/null +++ b/plugins/dailyword/__init__.py @@ -0,0 +1,250 @@ +from datetime import datetime +from functools import lru_cache +from typing import List, Tuple, Dict, Any, Optional + +from app.plugins import _PluginBase +from app.utils.http import RequestUtils + + +class DailyWord(_PluginBase): + # 插件名称 + plugin_name = "每日一言" + # 插件描述 + plugin_desc = "在仪表板中显示每日一言卡片。" + # 插件图标 + plugin_icon = "Calibre_B.png" + # 插件版本 + plugin_version = "1.1" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "dailyowrd_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + _enable: bool = False + _size: str = "mini" + + def init_plugin(self, config: dict = None): + self._enable = config.get("enable") + self._size = config.get("size") + + @staticmethod + def get_command() -> List[Dict[str, Any]]: + pass + + def get_api(self) -> List[Dict[str, Any]]: + pass + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enable', + 'label': '启用插件', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'size', + 'label': '组件规格', + 'items': [ + {"title": "迷你", "value": "mini"}, + {"title": "小型", "value": "small"}, + {"title": "中型", "value": "medium"}, + {"title": "大型", "value": "large"} + ] + } + } + ] + } + ] + } + ] + } + ], { + "enable": self._enable, + "size": self._size + } + + 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" + }] + """ + return [{ + "key": "dailyword_dashboard", + "name": "每日一言" + }] + + @lru_cache(maxsize=1) + def __get_youngam(self, **kwargs) -> Optional[dict]: + """ + 获取每日一言,缓存12小时 + """ + res = RequestUtils().get_res("https://apier.youngam.cn/essay/one") + if res: + datalist = res.json().get("dataList") + return datalist[0] if datalist else {} + return {} + + 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/ + """ + # 列配置 + if self._size == "mini": + cols = { + "cols": 12, + "md": 4 + } + height = 160 + elif self._size == "small": + cols = { + "cols": 12, + "md": 6 + } + height = 262 + elif self._size == "medium": + cols = { + "cols": 12, + "md": 8 + } + height = 335 + else: + cols = { + "cols": 12, + "md": 12 + } + height = 500 + # 全局配置 + attrs = { + "border": False + } + # 获取流行越势数据 + data = self.__get_youngam(today=datetime.now().strftime("%Y-%m-%d")) + if not data: + elements = [ + { + 'component': 'VCard', + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'text-center', + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': '无数据' + } + ] + } + ] + } + ] + else: + elements = [ + { + 'component': 'VCard', + 'props': { + 'class': 'p-0' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': data.get('src'), + 'cover': True, + 'height': height + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 pa-4', + }, + 'content': [ + { + 'component': 'h1', + 'props': { + 'class': 'mb-1 text-white text-shadow text-xl line-clamp-4 overflow-hidden text-ellipsis ...' + }, + 'html': data.get('text'), + }, + { + 'component': 'span', + 'props': { + 'class': 'text-right text-shadow line-clamp-2 overflow-hidden text-ellipsis ...' + }, + 'text': f"{data.get('year')}年{data.get('month')}月{data.get('day')}日", + } + ] + } + ] + } + ] + }] + + return cols, attrs, elements + + def get_state(self) -> bool: + return self._enable + + def stop_service(self): + pass diff --git a/plugins/dirmonitor/__init__.py b/plugins/dirmonitor/__init__.py index abf941a..82cb002 100644 --- a/plugins/dirmonitor/__init__.py +++ b/plugins/dirmonitor/__init__.py @@ -59,7 +59,7 @@ class DirMonitor(_PluginBase): # 插件图标 plugin_icon = "directory.png" # 插件版本 - plugin_version = "2.0" + plugin_version = "2.2" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -83,10 +83,11 @@ class DirMonitor(_PluginBase): _onlyonce = False _cron = None _size = 0 + _scrape = True # 模式 compatibility/fast _mode = "fast" # 转移方式 - _transfer_type = settings.TRANSFER_TYPE + _transfer_type = "link" _monitor_dirs = "" _exclude_keywords = "" _interval: int = 10 @@ -119,6 +120,7 @@ class DirMonitor(_PluginBase): self._interval = config.get("interval") or 10 self._cron = config.get("cron") self._size = config.get("size") or 0 + self._scrape = config.get("scrape") or False # 停止现有任务 self.stop_service() @@ -235,7 +237,8 @@ class DirMonitor(_PluginBase): "exclude_keywords": self._exclude_keywords, "interval": self._interval, "cron": self._cron, - "size": self._size + "size": self._size, + "scrape": self._scrape }) @eventmanager.register(EventType.PluginAction) @@ -457,7 +460,7 @@ class DirMonitor(_PluginBase): ) # 刮削单个文件 - if settings.SCRAP_METADATA: + if self._scrape: self.chain.scrape_metadata(path=transferinfo.target_path, mediainfo=mediainfo, transfer_type=transfer_type) @@ -756,7 +759,7 @@ class DirMonitor(_PluginBase): 'component': 'VSelect', 'props': { 'model': 'transfer_type', - 'label': '转移方式', + 'label': '整理方式', 'items': [ {'title': '移动', 'value': 'move'}, {'title': '复制', 'value': 'copy'}, @@ -824,6 +827,22 @@ class DirMonitor(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'scrape', + 'label': '刮削元数据', + } + } + ] } ] }, @@ -844,9 +863,9 @@ class DirMonitor(_PluginBase): 'rows': 5, 'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move:\n' '监控目录\n' - '监控目录#转移方式\n' - '监控目录:转移目的目录\n' - '监控目录:转移目的目录#转移方式' + '监控目录#整理方式\n' + '监控目录:整理目的目录\n' + '监控目录:整理目的目录#转移方式' } } ] @@ -889,7 +908,7 @@ class DirMonitor(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '监控目录不指定目的目录时,将转移到媒体库目录,并自动创建一级分类目录,同时按配置创建二级分类目录;监控目录指定了目的目录时,不会自动创建一级目录,但会根据配置创建二级分类目录。' + 'text': '支持4种配置方式:1、监控目录,2、监控目录#整理方式,3、监控目录:整理目的目录,4、监控目录:整理目的目录#转移方式。监控目录不指定目的目录时,将按媒体库目录设置整理到媒体库目录,并根据目录的分类设置自动创建一二级分类目录;监控目录指定了目的目录时,会尝试在媒体库目录设定中查找对应路径的目录配置,如存在则以目录设定的分类选项创建子目录,否则直接整理到该目的目录下。建议不设置目的目录,由系统根据目录设定自动分类整理。' } } ] @@ -945,12 +964,13 @@ class DirMonitor(_PluginBase): "notify": False, "onlyonce": False, "mode": "fast", - "transfer_type": settings.TRANSFER_TYPE, + "transfer_type": "link", "monitor_dirs": "", "exclude_keywords": "", "interval": 10, "cron": "", - "size": 0 + "size": 0, + "scrape": True } def get_page(self) -> List[dict]: diff --git a/plugins/downloaderhelper/__init__.py b/plugins/downloaderhelper/__init__.py index a6445f0..38bedc8 100644 --- a/plugins/downloaderhelper/__init__.py +++ b/plugins/downloaderhelper/__init__.py @@ -9,8 +9,10 @@ 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 @@ -20,7 +22,8 @@ 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 +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 @@ -33,7 +36,7 @@ class DownloaderHelper(_PluginBase): # 插件图标 plugin_icon = "DownloaderHelper.png" # 插件版本 - plugin_version = "2.2" + plugin_version = "2.6" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -43,7 +46,7 @@ class DownloaderHelper(_PluginBase): # 加载顺序 plugin_order = 66 # 可使用的用户级别 - auth_level = 2 + auth_level = 1 # 插件说明链接 __help_url = 'https://github.com/jxxghp/MoviePilot-Plugins/tree/main/plugins/downloaderhelper' @@ -55,6 +58,8 @@ class DownloaderHelper(_PluginBase): __exit_event: ThreadEvent = ThreadEvent() # 任务锁 __task_lock: RLock = RLock() + # 缓存 + __ttl_cache = TTLCache(maxsize=128, ttl=1800) # 配置相关 # 插件缺省配置 @@ -62,7 +67,7 @@ class DownloaderHelper(_PluginBase): 'site_name_priority': True, 'tag_prefix': '站点/', 'dashboard_widget_size': 12, - 'dashboard_widget_target_downloader': 'default', + 'dashboard_widget_target_downloaders': ['default'], 'dashboard_widget_display_fields': [ TorrentField.NAME.name, TorrentField.SELECT_SIZE.name, @@ -75,7 +80,8 @@ class DownloaderHelper(_PluginBase): TorrentField.TAGS.name, TorrentField.ADD_TIME.name, TorrentField.UPLOADED.name, - ] + ], + 'dashboard_speed_widget_target_downloaders': ['default'], } # 插件用户配置 __config: Dict[str, Any] = {} @@ -91,6 +97,12 @@ class DownloaderHelper(_PluginBase): __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', + } def init_plugin(self, config: dict = None): """ @@ -99,6 +111,9 @@ class DownloaderHelper(_PluginBase): # 停止现有服务 self.stop_service() + # 检查环境 + self.__check_environment() + # 修正配置 config = self.__fix_config(config=config) # 加载插件配置 @@ -138,7 +153,8 @@ class DownloaderHelper(_PluginBase): ) and self.__check_enable_any_task() ) - or self.__check_enable_dashboard_widget() + or self.__check_enable_dashboard_active_torrent_widget() + or self.__check_enable_dashboard_speed_widget() ) else False return state @@ -358,7 +374,7 @@ class DownloaderHelper(_PluginBase): 'props': { 'model': 'site_name_priority', 'label': '站点名称优先', - 'hint': '给种子添加站点标签时,是否优先以站点名称作为标签内容(否则将使用域名关键字)?' + 'hint': '给种子添加站点标签时,是否优先以站点名称作为标签内容(否则将使用域名关键字)?MoviePilot需要认证,否则将不生效。' } }] }] @@ -434,9 +450,23 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'VSwitch', 'props': { - 'model': '_config_dashboard_dialog_closed', + 'model': '_config_dashboard_active_torrent_dialog_closed', 'label': '配置仪表板活动种子组件', - 'hint': '点击展开仪表板组件配置窗口。' + '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': '点击展开仪表板实时速率组件配置窗口。' } }] }] @@ -484,7 +514,7 @@ class DownloaderHelper(_PluginBase): }, { 'component': 'VDialog', 'props': { - 'model': '_config_dashboard_dialog_closed', + 'model': '_config_dashboard_active_torrent_dialog_closed', 'max-width': '40rem' }, 'content': [{ @@ -498,7 +528,7 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'VDialogCloseBtn', 'props': { - 'model': '_config_dashboard_dialog_closed' + 'model': '_config_dashboard_active_torrent_dialog_closed' } }, { 'component': 'VRow', @@ -512,8 +542,8 @@ class DownloaderHelper(_PluginBase): 'component': 'VSwitch', 'props': { 'model': 'enable_dashboard_widget', - 'label': '启用仪表板组件', - 'hint': '是否启用仪表板组件。' + 'label': '启用组件', + 'hint': '是否启用仪表板活动种子组件。' } }] }, { @@ -561,8 +591,9 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'VSelect', 'props': { - 'model': 'dashboard_widget_target_downloader', + 'model': 'dashboard_widget_target_downloaders', 'label': '目标下载器', + 'multiple': True, 'items': [ {'title': '系统默认下载器', 'value': 'default'}, {'title': Downloader.QB.name_, 'value': Downloader.QB.id}, @@ -591,6 +622,83 @@ class DownloaderHelper(_PluginBase): }] }] }] + }, { + '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': [{ @@ -649,7 +757,44 @@ class DownloaderHelper(_PluginBase): def get_page(self) -> List[dict]: pass - def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + 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配置参考: @@ -661,14 +806,50 @@ class DownloaderHelper(_PluginBase): "refresh": 10 // 自动刷新时间,单位秒 } 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + + kwargs参数可获取的值:1、user_agent:浏览器UA + + :param key: 仪表盘key,根据指定的key返回相应的仪表盘数据,缺省时返回一个固定的仪表盘数据(兼容旧版) """ - if not self.get_state() or not self.__check_enable_dashboard_widget(): + 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') + # 列配置 + dashboard_widget_size = self.__get_config_item('dashboard_widget_size') cols = { 'cols': 12, 'xxl': dashboard_widget_size, @@ -678,14 +859,52 @@ class DownloaderHelper(_PluginBase): 'sm': 12, 'xs': 12 } + # 全局配置 attrs = { - 'title': '活动种子' + 'title': f'活动种子 #{downloader.short_name}' } - if self.__check_target_downloader(): + if self.__check_target_downloader(downloader_id=downloader_id): attrs['refresh'] = self.__get_config_item('dashboard_widget_refresh') + # 页面元素 - elements = self.__get_dashboard_elememts() + 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): @@ -696,12 +915,21 @@ class DownloaderHelper(_PluginBase): 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映射 @@ -792,6 +1020,20 @@ class DownloaderHelper(_PluginBase): 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: """ 修正配置 @@ -820,6 +1062,13 @@ class DownloaderHelper(_PluginBase): 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: """ 获取插件配置项 @@ -863,7 +1112,7 @@ class DownloaderHelper(_PluginBase): return value return None - def __get_site_info_by_domain(self, site_domain: str) -> Optional[str]: + def __get_site_info_by_domain(self, site_domain: str) -> CommentedMap: """ 根据站点域名从索引中获取站点信息 :param site_domain: 站点域名 @@ -923,13 +1172,20 @@ class DownloaderHelper(_PluginBase): return True if self.__check_enable_qb_task() \ or self.__check_enable_tr_task() else False - def __check_enable_dashboard_widget(self) -> bool: + def __check_enable_dashboard_active_torrent_widget(self) -> bool: """ - 判断是否启用了仪表板组件 - :return: 是否启用了仪表板组件 + 判断是否启用了仪表板活动种子组件 + :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]: """ @@ -1243,7 +1499,7 @@ class DownloaderHelper(_PluginBase): text = self.__build_notify_message(context=context) if not text: return - self.post_message(title=f'{self.plugin_name}任务执行结果', text=text, userid=context.get_username()) + self.post_message(title=f'{self.plugin_name}任务执行结果', text=text, mtype=NotificationType.Plugin) @staticmethod def __build_notify_message(context: TaskContext): @@ -1278,24 +1534,11 @@ class DownloaderHelper(_PluginBase): text += '\n————————————\n' return text - def __get_system_module_instance(self, module_id: str) -> Union[Qbittorrent, Transmission]: - """ - 获取系统模块实例 - """ - if not module_id: - return None - module_manager = ModuleManager() - running_modules = module_manager._running_modules - if not running_modules: - return None - module = running_modules.get(module_id) - return module if module else None - def __get_qbittorrent(self) -> Qbittorrent: """ 获取qb实例 """ - module = self.__get_system_module_instance(module_id='QbittorrentModule') + module = ModuleManager().get_running_module(module_id='QbittorrentModule') if not module: return None qbittorrent = getattr(module, 'qbittorrent') @@ -1307,7 +1550,7 @@ class DownloaderHelper(_PluginBase): """ 获取tr实例 """ - module = self.__get_system_module_instance(module_id='TransmissionModule') + module = ModuleManager().get_running_module(module_id='TransmissionModule') if not module: return None transmission = getattr(module, 'transmission') @@ -1811,13 +2054,13 @@ class DownloaderHelper(_PluginBase): result.append(field) return result - def __build_dashboard_widget_table_head_content(self, fields: List[Union[str, TorrentField]] = None) -> list: + def __build_dashboard_widget_torrent_table_head_content(self, + fields: List[TorrentField] = None) -> list: """ - 构造仪表板组件表头内容 + 构造仪表板组件种子表头内容 """ if not fields: - fields = self.__get_config_item('dashboard_widget_display_fields') - fields = self.__ensure_torrent_fields(fields=fields) + fields = self.__get_dashboard_active_torrent_widget_display_fields() if not fields: return [] return [{ @@ -1826,20 +2069,27 @@ class DownloaderHelper(_PluginBase): 'class': 'text-start ps-4' }, 'text': field.name_ - } for field in fields] + } for field in fields if field] - def __build_dashboard_widget_table_head(self, fields: List[Union[str, TorrentField]] = None) -> dict: + def __build_dashboard_widget_torrent_table_head(self, + fields: List[TorrentField] = None) -> dict: """ - 构造仪表板组件表头 + 构造仪表板组件种子表头 """ return { 'component': 'thead', - 'content': self.__build_dashboard_widget_table_head_content(fields=fields) + 'content': self.__build_dashboard_widget_torrent_table_head_content(fields=fields) } - def __build_dashboard_widget_table_body_content(self, data: List[List[Any]], fields: List[Union[str, TorrentField]] = None) -> list: + 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 [{ @@ -1856,7 +2106,7 @@ class DownloaderHelper(_PluginBase): } for col in row] } for row in data if row] else: - empty_text = '暂无数据' if self.__check_target_downloader() else '目标下载器配置无效' + empty_text = '暂无数据' if self.__check_target_downloader(downloader_id=downloader_id) else '目标下载器配置无效' return [{ 'component': 'tr', 'props': { @@ -1865,70 +2115,105 @@ class DownloaderHelper(_PluginBase): 'content': [{ 'component': 'td', 'props': { - 'colspan': len(fields), + 'colspan': field_count, 'class': 'text-center' }, 'text': empty_text }] }] - def __build_dashboard_widget_table_body(self, data: List[List[Any]], - fields: List[Union[str, TorrentField]] = None) -> dict: + 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_table_body_content(data=data, fields=fields) + 'content': self.__build_dashboard_widget_torrent_table_body_content(data=data, field_count=field_count, downloader_id=downloader_id) } - def __get_target_downloader_id(self) -> str: + def __get_dashboard_widget_target_downloader_ids(self, config_key: str) -> List[str]: """ - 获取目标下载器id + 获取仪表板组件目标下载器ids """ - target_downloader = self.__get_config_item('dashboard_widget_target_downloader') - if target_downloader == 'default': - target_downloader = settings.DEFAULT_DOWNLOADER - if not target_downloader: - return None - return target_downloader + 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 __check_target_downloader(self) -> bool: + 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: """ 检查目标下载器是否有效 """ - target_downloader = self.__get_target_downloader_id() - if not target_downloader: + if not downloader_id: return False - if target_downloader == Downloader.QB.id: + if downloader_id == Downloader.QB.id: return self.__get_qbittorrent() is not None - elif target_downloader == Downloader.TR.id: + elif downloader_id == Downloader.TR.id: return self.__get_transmission() is not None else: return False - def __get_downloader_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + def __get_downloader_active_torrent_data(self, + downloader_id: str, + fields: List[TorrentField] = None): """ - 获取下载器种子数据 + 获取下载器活动种子数据 """ - # 目标下载器 - target_downloader = self.__get_target_downloader_id() - if not target_downloader: + if not downloader_id: return None # 字段 if not fields: - fields = self.__get_config_item('dashboard_widget_display_fields') - fields = self.__ensure_torrent_fields(fields=fields) - if target_downloader == Downloader.QB.id: - return self.__get_qbittorrent_torrent_data(fields=fields) - elif target_downloader == Downloader.TR.id: - return self.__get_transmission_torrent_data(fields=fields) + 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_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + def __get_qbittorrent_active_torrent_data(self, + fields: List[TorrentField] = None): """ - 获取qb种子数据 + 获取qb活动种子数据 """ if self.__exit_event.is_set(): logger.warn('插件服务正在退出,操作取消') @@ -1938,8 +2223,7 @@ class DownloaderHelper(_PluginBase): return None # 字段 if not fields: - fields = self.__get_config_item('dashboard_widget_display_fields') - fields = self.__ensure_torrent_fields(fields=fields) + fields = self.__get_dashboard_active_torrent_widget_display_fields() # 活动种子 torrents, error = qbittorrent.get_torrents(status=['active']) if error: @@ -1968,7 +2252,8 @@ class DownloaderHelper(_PluginBase): torrent] @staticmethod - def __process_torrent_for_qbittorrent(torrent: TorrentDictionary, fields: List[TorrentField]): + def __process_torrent_for_qbittorrent(torrent: TorrentDictionary, + fields: List[TorrentField]): """ 加工qb种子 """ @@ -2005,7 +2290,8 @@ class DownloaderHelper(_PluginBase): logger.error(f'加工qb种子: {str(e)}, torrent = {str(torrent)}', exc_info=True) return None - def __convert_qbittorrent_torrent_data(self, torrent: TorrentDictionary, + def __convert_qbittorrent_torrent_data(self, + torrent: TorrentDictionary, fields: List[TorrentField]) -> Optional[List[Any]]: """ 转换qb种子数据 @@ -2021,7 +2307,8 @@ class DownloaderHelper(_PluginBase): return data @staticmethod - def __extract_torrent_value_for_qbittorrent(torrent: TorrentDictionary, field: TorrentField) -> Any: + def __extract_torrent_value_for_qbittorrent(torrent: TorrentDictionary, + field: TorrentField) -> Any: """ 从qb种子中提取值 """ @@ -2048,6 +2335,7 @@ class DownloaderHelper(_PluginBase): 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') @@ -2065,9 +2353,10 @@ class DownloaderHelper(_PluginBase): arguments.append('uploadLimited') return list(set(arguments)) - def __get_transmission_torrent_data(self, fields: List[Union[str, TorrentField]] = None): + def __get_transmission_active_torrent_data(self, + fields: List[TorrentField] = None): """ - 获取tr种子数据 + 获取tr活动种子数据 """ if self.__exit_event.is_set(): logger.warn('插件服务正在退出,操作取消') @@ -2077,8 +2366,7 @@ class DownloaderHelper(_PluginBase): return None # 字段 if not fields: - fields = self.__get_config_item('dashboard_widget_display_fields') - fields = self.__ensure_torrent_fields(fields=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 @@ -2086,7 +2374,8 @@ class DownloaderHelper(_PluginBase): torrents = sorted(torrents, key=lambda torrent: torrent.fields.get(TorrentField.ADD_TIME.tr), reverse=True) return self.__convert_transmission_torrents_data(torrents=torrents, fields=fields) - def __convert_transmission_torrents_data(self, torrents: List[Torrent], + def __convert_transmission_torrents_data(self, + torrents: List[Torrent], fields: List[TorrentField]) -> Optional[List[List[Any]]]: """ 转换tr种子数据 @@ -2097,13 +2386,14 @@ class DownloaderHelper(_PluginBase): torrent] @staticmethod - def __process_torrent_for_transmission(torrent: Torrent, fields: List[TorrentField]): + def __process_torrent_for_transmission(torrent: Torrent, + fields: List[TorrentField]): """ 加工tr种子 """ if not torrent or not fields: return - + def calculate_completed(torrent: Torrent): """ 计算已完成大小 @@ -2157,7 +2447,8 @@ class DownloaderHelper(_PluginBase): logger.error(f'加工tr种子异常: {str(e)}, torrent = {str(torrent.fields)}', exc_info=True) return None - def __convert_transmission_torrent_data(self, torrent: Torrent, + def __convert_transmission_torrent_data(self, + torrent: Torrent, fields: List[TorrentField]) -> Optional[List[Any]]: """ 转换tr种子数据 @@ -2173,7 +2464,8 @@ class DownloaderHelper(_PluginBase): return data @staticmethod - def __extract_torrent_value_for_transmission(torrent: Torrent, field: TorrentField) -> Any: + def __extract_torrent_value_for_transmission(torrent: Torrent, + field: TorrentField) -> Any: """ 从tr种子中提取值 """ @@ -2190,16 +2482,18 @@ class DownloaderHelper(_PluginBase): logger.error(f'从tr种子中提取值异常: {str(e)}, torrent = {str(torrent.fields)}', exc_info=True) return None - def __get_dashboard_elememts(self) -> list: + 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_config_item('dashboard_widget_display_fields') - fields = self.__ensure_torrent_fields(fields=fields) - data = self.__get_downloader_torrent_data(fields=fields) + 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 @@ -2214,11 +2508,232 @@ class DownloaderHelper(_PluginBase): } }, 'content': [ - self.__build_dashboard_widget_table_head(fields=fields), - self.__build_dashboard_widget_table_body(data=data, fields=fields) + 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-theme--light 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) -> dict: + """ + 构造仪表板实时速率组件列表item元素 + """ + if not mdi_icon or not label or not value: + return None + return { + 'component': 'div', + 'props': { + 'style': { + 'display': 'grid', + 'grid-template-areas': '"prepend content append"', + 'grid-template-columns': 'max-content 1fr auto', + 'padding-bottom': '16px' + } + }, + 'content': [{ + 'component': 'div', + 'props': { + 'style': { + 'grid-area': 'prepend', + 'height': '21px', + 'color': '#6a6670' + } + }, + '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), + ] + return [{ + 'component': 'div', + 'props': { + 'style': { + 'padding': '16px 0 20px 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): """ diff --git a/plugins/downloaderhelper/module.py b/plugins/downloaderhelper/module.py index 855f6b9..cbabf2d 100644 --- a/plugins/downloaderhelper/module.py +++ b/plugins/downloaderhelper/module.py @@ -17,6 +17,10 @@ class Downloader(Enum): self.short_name: str = short_name +# Downloader 映射 +DownloaderMap = dict((d.id, d) for d in Downloader) + + class TaskResult: """ 任务执行结果 @@ -304,3 +308,20 @@ class TorrentField(Enum): # 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/libraryscraper/__init__.py b/plugins/libraryscraper/__init__.py index 01f8adc..900fef9 100644 --- a/plugins/libraryscraper/__init__.py +++ b/plugins/libraryscraper/__init__.py @@ -25,7 +25,7 @@ class LibraryScraper(_PluginBase): # 插件图标 plugin_icon = "scraper.png" # 插件版本 - plugin_version = "1.4" + plugin_version = "1.4.1" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -410,14 +410,14 @@ class LibraryScraper(_PluginBase): "uniqueid[@type='TMDB']", "tmdbid" ] - reader = NfoReader(file_path) - for xpath in xpaths: - try: + try: + reader = NfoReader(file_path) + for xpath in xpaths: tmdbid = reader.get_element_value(xpath) if tmdbid: return tmdbid - except Exception as err: - print(str(err)) + except Exception as err: + logger.warn(f"从nfo文件中获取tmdbid失败:{str(err)}") return None def stop_service(self): diff --git a/plugins/mediasyncdel/__init__.py b/plugins/mediasyncdel/__init__.py index 7ca9ed6..2ff19c1 100644 --- a/plugins/mediasyncdel/__init__.py +++ b/plugins/mediasyncdel/__init__.py @@ -17,9 +17,6 @@ from app.db.models.transferhistory import TransferHistory from app.log import logger from app.modules.emby import Emby from app.modules.jellyfin import Jellyfin -from app.modules.qbittorrent import Qbittorrent -from app.modules.themoviedb.tmdbv3api import Episode -from app.modules.transmission import Transmission from app.plugins import _PluginBase from app.schemas.types import NotificationType, EventType, MediaType, MediaImageType @@ -32,7 +29,7 @@ class MediaSyncDel(_PluginBase): # 插件图标 plugin_icon = "mediasyncdel.png" # 插件版本 - plugin_version = "1.5" + plugin_version = "1.6" # 插件作者 plugin_author = "thsrite" # 作者主页 @@ -45,7 +42,6 @@ class MediaSyncDel(_PluginBase): auth_level = 1 # 私有属性 - episode = None _scheduler: Optional[BackgroundScheduler] = None _enabled = False _sync_type: str = "" @@ -58,16 +54,11 @@ class MediaSyncDel(_PluginBase): _transferchain = None _transferhis = None _downloadhis = None - qb = None - tr = None def init_plugin(self, config: dict = None): self._transferchain = TransferChain() self._transferhis = self._transferchain.transferhis self._downloadhis = self._transferchain.downloadhis - self.episode = Episode() - self.qb = Qbittorrent() - self.tr = Transmission() # 停止现有任务 self.stop_service() @@ -1199,12 +1190,8 @@ class MediaSyncDel(_PluginBase): # 删除转种后任务 logger.info(f"删除转种后下载任务:{download} - {download_id}") # 删除转种后下载任务 - if download == "transmission": - self.tr.delete_torrents(delete_file=True, - ids=download_id) - else: - self.qb.delete_torrents(delete_file=True, - ids=download_id) + self.chain.remove_torrents(hashs=torrent_hash, + downloader=download) handle_torrent_hashs.append(download_id) else: # 暂停种子 @@ -1219,10 +1206,7 @@ class MediaSyncDel(_PluginBase): logger.info(f"暂停转种后下载任务:{download} - {download_id}") # 删除转种后下载任务 - if download == "transmission": - self.tr.stop_torrents(ids=download_id) - else: - self.qb.stop_torrents(ids=download_id) + self.chain.stop_torrents(hashs=download_id, downloader=download) handle_torrent_hashs.append(download_id) else: # 未转种de情况 @@ -1237,8 +1221,7 @@ class MediaSyncDel(_PluginBase): handle_torrent_hashs.append(download_id) # 处理辅种 - handle_torrent_hashs = self.__del_seed(download=download, - download_id=download_id, + handle_torrent_hashs = self.__del_seed(download_id=download_id, delete_flag=delete_flag, handle_torrent_hashs=handle_torrent_hashs) # 处理合集 @@ -1284,27 +1267,19 @@ class MediaSyncDel(_PluginBase): # 删除合集种子 if delete_flag: - if str(download_file.downloader) == "transmission": - self.tr.delete_torrents(delete_file=True, - ids=download_file.download_hash) - else: - self.qb.delete_torrents(delete_file=True, - ids=download_file.download_hash) - + self.chain.remove_torrents(hashs=download_file.download_hash, + downloader=download_file.downloader) logger.info(f"删除合集种子 {download_file.downloader} {download_file.download_hash}") else: # 暂停合集种子 - if str(download_file.downloader) == "transmission": - self.tr.stop_torrents(ids=download_file.download_hash) - else: - self.qb.stop_torrents(ids=download_file.download_hash) + self.chain.stop_torrents(hashs=download_file.download_hash, + downloader=download_file.downloader) logger.info(f"暂停合集种子 {download_file.downloader} {download_file.download_hash}") # 已处理种子+1 handle_torrent_hashs.append(download_file.download_hash) # 处理合集辅种 - handle_torrent_hashs = self.__del_seed(download=download_file.downloader, - download_id=download_file.download_hash, + handle_torrent_hashs = self.__del_seed(download_id=download_file.download_hash, delete_flag=delete_flag, handle_torrent_hashs=handle_torrent_hashs) except Exception as e: @@ -1313,7 +1288,7 @@ class MediaSyncDel(_PluginBase): return handle_torrent_hashs - def __del_seed(self, download, download_id, delete_flag, handle_torrent_hashs): + def __del_seed(self, download_id, delete_flag, handle_torrent_hashs): """ 删除辅种 """ @@ -1337,30 +1312,18 @@ class MediaSyncDel(_PluginBase): # 删除辅种历史 for torrent in torrents: handle_torrent_hashs.append(torrent) - if str(download) == "qbittorrent": - # 删除辅种 - if delete_flag: - logger.info(f"删除辅种:{downloader} - {torrent}") - self.qb.delete_torrents(delete_file=True, - ids=torrent) - # 暂停辅种 - else: - self.qb.stop_torrents(ids=torrent) - logger.info(f"辅种:{downloader} - {torrent} 暂停") + # 删除辅种 + if delete_flag: + logger.info(f"删除辅种:{downloader} - {torrent}") + self.chain.remove_torrents(hashs=torrent, + downloader=downloader) + # 暂停辅种 else: - # 删除辅种 - if delete_flag: - logger.info(f"删除辅种:{downloader} - {torrent}") - self.tr.delete_torrents(delete_file=True, - ids=torrent) - # 暂停辅种 - else: - self.tr.stop_torrents(ids=torrent) - logger.info(f"辅种:{downloader} - {torrent} 暂停") + self.chain.stop_torrents(hashs=torrent, download=downloader) + logger.info(f"辅种:{downloader} - {torrent} 暂停") # 处理辅种的辅种 - handle_torrent_hashs = self.__del_seed(download=downloader, - download_id=torrent, + handle_torrent_hashs = self.__del_seed(download_id=torrent, delete_flag=delete_flag, handle_torrent_hashs=handle_torrent_hashs) diff --git a/plugins/pluginautoupgrade/__init__.py b/plugins/pluginautoupgrade/__init__.py index 5de5a10..20187c7 100644 --- a/plugins/pluginautoupgrade/__init__.py +++ b/plugins/pluginautoupgrade/__init__.py @@ -12,6 +12,7 @@ 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 @@ -23,7 +24,7 @@ class PluginAutoUpgrade(_PluginBase): # 插件图标 plugin_icon = "PluginAutoUpgrade.png" # 插件版本 - plugin_version = "1.8" + plugin_version = "1.9" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -665,7 +666,7 @@ class PluginAutoUpgrade(_PluginBase): text = self.__build_notify_message(results=results) if not text: return - self.post_message(title=f'{self.plugin_name}任务执行结果', text=text) + self.post_message(title=f'{self.plugin_name}任务执行结果', text=text, mtype=NotificationType.Plugin) @staticmethod def __build_notify_message(results: List[Dict[str, Any]]) -> str: diff --git a/plugins/rsssubscribe/__init__.py b/plugins/rsssubscribe/__init__.py index 39ec693..723dafc 100644 --- a/plugins/rsssubscribe/__init__.py +++ b/plugins/rsssubscribe/__init__.py @@ -19,6 +19,7 @@ from app.core.metainfo import MetaInfo from app.helper.rss import RssHelper from app.log import logger from app.plugins import _PluginBase +from app.schemas import ExistMediaInfo from app.schemas.types import SystemConfigKey, MediaType lock = Lock() @@ -32,7 +33,7 @@ class RssSubscribe(_PluginBase): # 插件图标 plugin_icon = "rss.png" # 插件版本 - plugin_version = "1.3" + plugin_version = "1.4" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -656,51 +657,49 @@ class RssSubscribe(_PluginBase): if not result: logger.info(f"{title} {description} 不匹配过滤规则") continue - # 查询缺失的媒体信息 - exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo) - if exist_flag: - logger.info(f'{mediainfo.title_year} 媒体库中已存在') + # 媒体库已存在的剧集 + exist_info: Optional[ExistMediaInfo] = self.chain.media_exists(mediainfo=mediainfo) + if mediainfo.type == MediaType.TV: + if exist_info: + exist_season = exist_info.seasons + if exist_season: + exist_episodes = exist_season.get(meta.begin_season) + if exist_episodes and set(meta.episode_list).issubset(set(exist_episodes)): + logger.info(f'{mediainfo.title_year} {meta.season_episode} 己存在') + continue + elif exist_info: + # 电影已存在 + logger.info(f'{mediainfo.title_year} 己存在') continue + # 下载或订阅 + if self._action == "download": + # 添加下载 + result = self.downloadchain.download_single( + context=Context( + meta_info=meta, + media_info=mediainfo, + torrent_info=torrentinfo, + ), + save_path=self._save_path, + username="RSS订阅" + ) + if not result: + logger.error(f'{title} 下载失败') + continue else: - if self._action == "download": - if mediainfo.type == MediaType.TV: - if no_exists: - exist_info = no_exists.get(mediainfo.tmdb_id) - season_info = exist_info.get(meta.begin_season or 1) - if not season_info: - logger.info(f'{mediainfo.title_year} {meta.season} 己存在') - continue - if (season_info.episodes - and not set(meta.episode_list).issubset(set(season_info.episodes))): - logger.info(f'{mediainfo.title_year} {meta.season_episode} 己存在') - continue - # 添加下载 - result = self.downloadchain.download_single( - context=Context( - meta_info=meta, - media_info=mediainfo, - torrent_info=torrentinfo, - ), - save_path=self._save_path, - username="RSS订阅" - ) - if not result: - logger.error(f'{title} 下载失败') - continue - else: - # 检查是否在订阅中 - subflag = self.subscribechain.exists(mediainfo=mediainfo, meta=meta) - if subflag: - logger.info(f'{mediainfo.title_year} {meta.season} 正在订阅中') - continue - # 添加订阅 - self.subscribechain.add(title=mediainfo.title, - year=mediainfo.year, - mtype=mediainfo.type, - tmdbid=mediainfo.tmdb_id, - season=meta.begin_season, - exist_ok=True, - username="RSS订阅") + # 检查是否在订阅中 + subflag = self.subscribechain.exists(mediainfo=mediainfo, meta=meta) + if subflag: + logger.info(f'{mediainfo.title_year} {meta.season} 正在订阅中') + continue + # 添加订阅 + self.subscribechain.add(title=mediainfo.title, + year=mediainfo.year, + mtype=mediainfo.type, + tmdbid=mediainfo.tmdb_id, + season=meta.begin_season, + exist_ok=True, + username="RSS订阅") # 存储历史记录 history.append({ "title": f"{mediainfo.title} {meta.season}", diff --git a/plugins/trendingshow/__init__.py b/plugins/trendingshow/__init__.py index a7e7df4..b443d8b 100644 --- a/plugins/trendingshow/__init__.py +++ b/plugins/trendingshow/__init__.py @@ -10,7 +10,7 @@ class TrendingShow(_PluginBase): # 插件描述 plugin_desc = "在仪表板中显示流行趋势海报轮播图。" # 插件图标 - plugin_icon = "Dsphoto_A.png" + plugin_icon = "TrendingShow.jpg" # 插件版本 plugin_version = "1.0" # 插件作者 @@ -183,7 +183,7 @@ class TrendingShow(_PluginBase): 'show-arrows': 'hover', 'hide-delimiters': True, 'cycle': True, - 'interval': 5000, + 'interval': 10000, 'height': height }, 'content': [ @@ -197,7 +197,7 @@ class TrendingShow(_PluginBase): { 'component': 'VCardText', 'props': { - 'class': 'w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-4', + 'class': 'w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 pa-4', }, 'content': [ { diff --git a/plugins/vcbanimemonitor/__init__.py b/plugins/vcbanimemonitor/__init__.py index 150b0e0..de3edd1 100644 --- a/plugins/vcbanimemonitor/__init__.py +++ b/plugins/vcbanimemonitor/__init__.py @@ -77,7 +77,7 @@ class VCBAnimeMonitor(_PluginBase): # 插件图标 plugin_icon = "vcbmonitor.png" # 插件版本 - plugin_version = "1.7.1" + plugin_version = "1.8" # 插件作者 plugin_author = "pixel@qingwa" # 作者主页 @@ -106,6 +106,7 @@ class VCBAnimeMonitor(_PluginBase): _onlyonce = False _cron = None _size = 0 + _scrape = True # 模式 compatibility/fast _mode = "fast" # 转移方式 @@ -142,6 +143,7 @@ class VCBAnimeMonitor(_PluginBase): self._interval = config.get("interval") or 10 self._cron = config.get("cron") self._size = config.get("size") or 0 + self._scrape = config.get("scrape") self._switch_ova = config.get("ova") self._high_mode = config.get("high_mode") self._torrents_path = config.get("torrents_path") or "" @@ -286,6 +288,7 @@ class VCBAnimeMonitor(_PluginBase): "interval": self._interval, "cron": self._cron, "size": self._size, + "scrape": self._scrape, "ova": self._switch_ova, "high_mode": self._high_mode, "torrents_path": self._torrents_path @@ -508,7 +511,7 @@ class VCBAnimeMonitor(_PluginBase): ) # 刮削单个文件 - if settings.SCRAP_METADATA: + if self._scrape: self.chain.scrape_metadata(path=transferinfo.target_path, mediainfo=mediainfo, transfer_type=transfer_type) @@ -826,6 +829,22 @@ class VCBAnimeMonitor(_PluginBase): } ] }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'scrape', + 'label': '刮削元数据', + } + } + ] + } ] }, {