diff --git a/README.md b/README.md index 298b05d..80d13b6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ MoviePilot官方插件市场:https://github.com/jxxghp/MoviePilot-Plugins - 可在插件目录中放置`requirements.txt`文件,用于指定插件依赖的第三方库,MoviePilot会在插件安装时自动安装依赖库。 ### 5. 界面开发 -- 插件支持`插件配置`及`详情展示`两个展示页面,通过配置化的方式组装,使用 [Vuetify](https://vuetifyjs.com/) 组件库,所有该组件库有的组件都可以通过Json配置使用。 +- 插件支持`插件配置`、`详情展示`、`仪表板Widget`三个展示页面,通过配置化的方式组装,使用 [Vuetify](https://vuetifyjs.com/) 组件库,所有该组件库有的组件都可以通过Json配置使用。 ## 常见问题 @@ -435,8 +435,9 @@ class EventType(Enum): "click": { // 点击事件 "api": "plugin/DoubanSync/delete_history", // API的相对路径 "method": "get", // GET/POST - "params": { // API上送参数 - "doubanid": "" + "params": { + // API上送参数 + "doubanid": "" } } } @@ -444,7 +445,29 @@ class EventType(Enum): ``` - 每次API调用完成后,均会自动刷新一次插件数据页。 -### 8. 如何发布插件版本? +### 8. 如何将插件内容显示到仪表板? +- `v1.8.7+` 支持将插件的内容显示到仪表盘,并支持定义占据的单元格大小,插件产生的仪表板仅管理员可见。 +- 1. 根据插件需要展示的Widget内容规划展示内容的样式和规格,也可设计多个规格样式并提供配置项供用户选择。 +- 2. 实现 `get_dashboard` 方法,返回仪表盘的配置信息,包括仪表盘的cols列配置(适配不同屏幕),以及仪表盘的页面配置json,具体可参考插件`站点数据统计`: +```python +def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板cols配置字典;2、全局配置(自动刷新等);2、仪表板页面元素配置json(含数据) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、全局配置参考: + { + "refresh": 10, // 自动刷新时间,单位秒 + "border": True, // 是否显示边框,默认True,为False时取消组件边框和边距,由插件自行控制 + } + 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + """ + pass +``` + +### 9. 如何发布插件版本? - 修改插件代码后,需要修改`package.json`中的`version`版本号,MoviePilot才会提示用户有更新,注意版本号需要与`__init__.py`文件中的`plugin_version`保持一致。 - `package.json`中的`level`用于定义插件用户可见权限,`1`为所有用户可见,`2`为仅认证用户可见,`3`为需要密钥才可见(一般用于测试)。如果插件功能需要使用到站点则应该为2,否则即使插件对用户可见但因为用户未认证相关功能也无法正常使用。 - `package.json`中的`history`用于记录插件更新日志,格式如下: @@ -456,3 +479,4 @@ class EventType(Enum): } } ``` +- 新增加的插件请配置在`package.json`中的末尾,这样可被识别为最新增加,可用于用户排序。 diff --git a/package.json b/package.json index 4375733..fef90d9 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,699 @@ { - "MTeamHelper": { - "name": "馒头辅助工具", - "description": "用于解决MP不支持馒头新架构更新导致的一些异常。", - "labels": "馒头,MTeam", - "version": "1.0", - "icon": "https://raw.githubusercontent.com/goo4it/MoviePilot-Plugins/main/icons/m-team.png", - "author": "goo4it", - "level": 1, - "history": { - "v1.0": "支持馒头新架构辅种" + "AutoSignIn": { + "name": "站点自动签到", + "description": "自动模拟登录、签到站点。", + "labels": "站点", + "version": "2.2", + "icon": "signin.png", + "author": "thsrite", + "level": 2, + "history": { + "v2.2": "适配馒头最新变化,需要升级至v1.8.5+版本且维护好Authorization", + "v2.1": "增强API安全性", + "v2.0": "站点签到时更新站点使用统计信息,需要主程序升级至v1.8.3+版本", + "v1.9": "支持馒头新架构自动签到" + } + }, + "CustomSites": { + "name": "自定义站点", + "description": "增加自定义站点为签到和统计使用。", + "labels": "站点", + "version": "1.0", + "icon": "world.png", + "author": "lightolly", + "level": 2 + }, + "SiteStatistic": { + "name": "站点数据统计", + "description": "自动统计和展示站点数据。", + "labels": "站点", + "version": "3.4", + "icon": "statistic.png", + "author": "lightolly", + "level": 2, + "history": { + "v3.4": "修复馒头站点数据统计", + "v3.3": "支持选择仪表板组件规格", + "v3.2": "支持在仪表板中显示站点统计信息,需要主程序升级至v1.8.7+版本", + "v3.1": "修复观众无法统计做总数和做种体积的bug", + "v3.0": "适配馒头数据统计,需要升级至v1.8.5+版本,且在站点信息中维护好API Key", + "v2.9": "增强API安全性", + "v2.8": "修复馒头未读消息统计", + "v2.7": "修复憨憨种子信息只统计第一页的问题,增加移除失效统计选项", + "v2.6": "支持馒头新架构数据统计" + } + }, + "SiteRefresh": { + "name": "站点自动更新", + "description": "使用浏览器模拟登录站点获取Cookie和UA。", + "labels": "站点", + "version": "1.2", + "icon": "Chrome_A.png", + "author": "thsrite", + "level": 2 + }, + "DoubanSync": { + "name": "豆瓣想看", + "description": "同步豆瓣想看数据,自动添加订阅。", + "labels": "订阅", + "version": "1.8", + "icon": "douban.png", + "author": "jxxghp", + "level": 2, + "history": { + "v1.8": "不同步在看条目", + "v1.7": "增强API安全性", + "v1.6": "同步历史记录支持手动删除,需要主程序升级至v1.8.4+版本", + "v1.5": "豆瓣信息识别后直接添加订阅,不进行搜索下载" + } + }, + "DirMonitor": { + "name": "目录监控", + "description": "监控目录文件发生变化时实时整理到媒体库。", + "labels": "文件整理", + "version": "2.0", + "icon": "directory.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.0": "增强API安全性", + "v1.9": "修复目录监控不能正确获取下载历史记录进行识别的问题" + } + }, + "ChineseSubFinder": { + "name": "ChineseSubFinder", + "description": "整理入库时通知ChineseSubFinder下载字幕。", + "labels": "字幕", + "version": "1.1", + "icon": "chinesesubfinder.png", + "author": "jxxghp", + "level": 1 + }, + "DoubanRank": { + "name": "豆瓣榜单订阅", + "description": "监控豆瓣热门榜单,自动添加订阅。", + "labels": "订阅", + "version": "1.9", + "icon": "movie.jpg", + "author": "jxxghp", + "level": 2, + "history": { + "v1.9": "增强API安全性", + "v1.8": "订阅历史记录支持手动删除,需要主程序升级至v1.8.4+版本" + } + }, + "LibraryScraper": { + "name": "媒体库刮削", + "description": "定时对媒体库进行刮削,补齐缺失元数据和图片。", + "labels": "刮削", + "version": "1.4", + "icon": "scraper.png", + "author": "jxxghp", + "level": 1 + }, + "TorrentRemover": { + "name": "自动删种", + "description": "自动删除下载器中的下载任务。", + "labels": "做种", + "version": "1.2.2", + "icon": "delete.jpg", + "author": "jxxghp", + "level": 2 + }, + "MediaSyncDel": { + "name": "媒体文件同步删除", + "description": "同步删除历史记录、源文件和下载任务。", + "labels": "文件整理", + "version": "1.4", + "icon": "mediasyncdel.png", + "author": "thsrite", + "level": 1 + }, + "CustomHosts": { + "name": "自定义Hosts", + "description": "修改系统hosts文件,加速网络访问。", + "labels": "网络", + "version": "1.0", + "icon": "hosts.png", + "author": "thsrite", + "level": 1 + }, + "SpeedLimiter": { + "name": "播放限速", + "description": "外网播放媒体库视频时,自动对下载器进行限速。", + "labels": "网络", + "version": "1.1", + "icon": "Librespeed_A.png", + "author": "Shurelol", + "level": 1 + }, + "CloudflareSpeedTest": { + "name": "Cloudflare IP优选", + "description": "🌩 测试 Cloudflare CDN 延迟和速度,自动优选IP。", + "labels": "网络,站点", + "version": "1.2", + "icon": "cloudflare.jpg", + "author": "thsrite", + "level": 1, + "history": { + "v1.2": "增强API安全性" + } + }, + "BestFilmVersion": { + "name": "收藏洗版", + "description": "Jellyfin/Emby/Plex点击收藏电影后,自动订阅洗版。", + "labels": "订阅", + "version": "2.1", + "icon": "like.jpg", + "author": "wlj", + "level": 2 + }, + "MediaServerMsg": { + "name": "媒体库服务器通知", + "description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。", + "labels": "消息通知,媒体库", + "version": "1.1", + "icon": "mediaplay.png", + "author": "jxxghp", + "level": 1 + }, + "MediaServerRefresh": { + "name": "媒体库服务器刷新", + "description": "入库后自动刷新Emby/Jellyfin/Plex服务器海报墙。", + "labels": "媒体库", + "version": "1.2", + "icon": "refresh2.png", + "author": "jxxghp", + "level": 1 + }, + "WebHook": { + "name": "Webhook", + "description": "事件发生时向第三方地址发送请求。", + "version": "1.0", + "icon": "webhook.png", + "author": "jxxghp", + "level": 1 + }, + "ChatGPT": { + "name": "ChatGPT", + "description": "消息交互支持与ChatGPT对话。", + "labels": "消息通知,识别", + "version": "1.3", + "icon": "Chatgpt_A.png", + "author": "jxxghp", + "level": 1 + }, + "NAStoolSync": { + "name": "历史记录同步", + "description": "同步NAStool历史记录、下载记录、插件记录到MoviePilot。", + "version": "1.0", + "icon": "Nastools_A.png", + "author": "thsrite", + "level": 1 + }, + "MessageForward": { + "name": "消息转发", + "description": "根据正则转发通知到其他WeChat应用。", + "labels": "消息通知", + "version": "1.1", + "icon": "forward.png", + "author": "thsrite", + "level": 1 + }, + "AutoBackup": { + "name": "自动备份", + "description": "自动备份数据和配置文件。", + "labels": "系统设置", + "version": "1.2", + "icon": "Time_machine_B.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.2": "增强API安全性" + } + }, + "IYUUAutoSeed": { + "name": "IYUU自动辅种", + "description": "基于IYUU官方Api实现自动辅种。", + "labels": "做种,IYUU", + "version": "1.7", + "icon": "IYUU.png", + "author": "jxxghp", + "level": 2, + "history": { + "v1.7": "适配馒头最新变化,需要升级至v1.8.5+版本且维护好Authorization", + "v1.6": "增加不辅种小体积种子功能", + "v1.5": "支持馒头新架构辅种" + } + }, + "CrossSeed": { + "name": "青蛙辅种助手", + "description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。", + "labels": "做种", + "version": "2.3", + "icon": "qingwa.png", + "author": "233@qingwa", + "level": 2, + "history": { + "v2.2": "站点停用后会同步暂停对该站点的辅种", + "v2.3": "站点辅种支持代理" + } + }, + "VCBAnimeMonitor": { + "name": "整理VCB动漫压制组作品", + "description": "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理", + "labels": "文件整理,识别", + "version": "1.7.1", + "icon": "vcbmonitor.png", + "author": "pixel@qingwa", + "level": 2, + "history": { + "v1.7.1": "修复偶尔安装失败问题" + } + }, + "TorrentTransfer": { + "name": "自动转移做种", + "description": "定期转移下载器中的做种任务到另一个下载器。", + "labels": "做种", + "version": "1.3", + "icon": "seed.png", + "author": "jxxghp", + "level": 2 + }, + "RssSubscribe": { + "name": "自定义订阅", + "description": "定时刷新RSS报文,识别内容后添加订阅或直接下载。", + "labels": "订阅", + "version": "1.3", + "icon": "rss.png", + "author": "jxxghp", + "level": 2, + "history": { + "v1.3": "支持手动删除订阅历史记录" + } + }, + "SyncDownloadFiles": { + "name": "下载器文件同步", + "description": "同步下载器的文件信息到数据库,删除文件时联动删除下载任务。", + "labels": "下载管理", + "version": "1.1", + "icon": "Youtube-dl_A.png", + "author": "thsrite", + "level": 1 + }, + "BrushFlow": { + "name": "站点刷流", + "description": "自动托管刷流,将会提高对应站点的访问频率。", + "labels": "刷流", + "version": "3.1", + "icon": "brush.jpg", + "author": "jxxghp,InfinityPacer", + "level": 2, + "history": { + "v3.1": "支持仪表板显示站点刷流数据,需要主程序升级v1.8.7+版本", + "v3.0": "优化不同站点刷流到相同种子的逻辑,修复数据页滚动闪烁,部分日志优化", + "v2.9": "优化动态删除消息推送,优化配置页UI显示及部分配置项,支持配置种子分类以及开启自动分类管理,取消单独适配站点时区逻辑,可通过配置项「pubtime」自行适配", + "v2.8": "优化UI显示以及提升性能", + "v2.7": "动态删除种子规则调整(请注意查阅插件文档),站点独立配置样式优化、日志优化,修复部分配置项无法配置小数的问题,修复部分场景可能导致重复下载的问题", + "v2.6": "修复排除订阅功能", + "v2.5": "增加H&R做种时间、下载器监控配置项,刷流前置条件逻辑调整,代理下载种子默认为关闭" + } + }, + "DownloadingMsg": { + "name": "下载进度推送", + "description": "定时推送正在下载进度。", + "labels": "消息通知,下载管理", + "version": "1.1", + "icon": "downloadmsg.png", + "author": "thsrite", + "level": 2 + }, + "AutoClean": { + "name": "定时清理媒体库", + "description": "定时清理用户下载的种子、源文件、媒体库文件。", + "labels": "媒体库", + "version": "1.1", + "icon": "clean.png", + "author": "thsrite", + "level": 2 + }, + "InvitesSignin": { + "name": "药丸签到", + "description": "药丸论坛签到。", + "labels": "站点", + "version": "1.4", + "icon": "invites.png", + "author": "thsrite", + "level": 2, + "history": { + "v1.4": "自定义保留消息天数" + } + }, + "PersonMeta": { + "name": "演职人员刮削", + "description": "刮削演职人员图片以及中文名称。", + "labels": "媒体库,刮削", + "version": "1.3", + "icon": "actor.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.3": "修复v1.8.5版本后刮削报错问题" + } + }, + "MoviePilotUpdateNotify": { + "name": "MoviePilot更新推送", + "description": "MoviePilot推送release更新通知、自动重启。", + "labels": "消息通知,自动更新", + "version": "1.4", + "icon": "Moviepilot_A.png", + "author": "thsrite", + "level": 1, + "history": { + "v1.4": "兼容更新内容带版本号的情况", + "v1.3": "增加前端版本更新检查,需要主程序升级至v1.8.4+版本" + } + }, + "CloudDiskDel": { + "name": "云盘文件删除", + "description": "媒体库删除strm文件后同步删除云盘资源。", + "labels": "媒体库", + "version": "1.3", + "icon": "clouddisk.png", + "author": "thsrite", + "level": 1 + }, + "BarkMsg": { + "name": "Bark消息推送", + "description": "支持使用Bark发送消息通知。", + "labels": "消息通知", + "version": "1.1", + "icon": "Bark_A.png", + "author": "jxxghp", + "level": 1 + }, + "IyuuMsg": { + "name": "IYUU消息推送", + "description": "支持使用IYUU发送消息通知。", + "labels": "消息通知,IYUU", + "version": "1.2", + "icon": "Iyuu_A.png", + "author": "jxxghp", + "level": 1 + }, + "PushDeerMsg": { + "name": "PushDeer消息推送", + "description": "支持使用PushDeer发送消息通知。", + "labels": "消息通知", + "version": "1.1", + "icon": "pushdeer.png", + "author": "jxxghp", + "level": 1 + }, + "ConfigCenter": { + "name": "配置中心", + "description": "快速调整部分系统设定。", + "labels": "系统设置", + "version": "2.4", + "icon": "setting.png", + "author": "jxxghp", + "level": 1 + }, + "WorkWechatMsg": { + "name": "企微机器人消息推送", + "description": "支持使用企业微信群聊机器人发送消息通知。", + "labels": "消息通知", + "version": "1.0", + "icon": "Wecom_A.png", + "author": "叮叮当", + "level": 1 + }, + "EpisodeGroupMeta": { + "name": "TMDB剧集组刮削", + "description": "从TMDB剧集组刮削季集的实际顺序。", + "labels": "刮削", + "version": "1.1", + "icon": "Element_A.png", + "author": "叮叮当", + "level": 1 + }, + "CustomIndexer": { + "name": "自定义索引站点", + "description": "修改或扩展内建索引器支持的站点。", + "labels": "站点", + "version": "1.0", + "icon": "spider.png", + "author": "jxxghp", + "level": 1 + }, + "FFmpegThumb": { + "name": "FFmpeg缩略图", + "description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图", + "labels": "刮削", + "version": "1.2", + "icon": "ffmpeg.png", + "author": "jxxghp", + "level": 1 + }, + "PushPlusMsg": { + "name": "PushPlus消息推送", + "description": "支持使用PushPlus发送消息通知。", + "labels": "消息通知", + "version": "1.0", + "icon": "Pushplus_A.png", + "author": "cheng", + "level": 1 + }, + "DownloadSiteTag": { + "name": "下载任务分类与标签", + "description": "自动给下载任务分类与打站点标签、剧集名称标签", + "labels": "下载管理", + "version": "2.1", + "icon": "Youtube-dl_B.png", + "author": "叮叮当", + "level": 1, + "history": { + "v2.1": "修复错误的TmdbHelper模块引用" + } + }, + "RemoveLink": { + "name": "清理硬链接", + "description": "监控目录内文件被删除时,同步删除监控目录内所有和它硬链接的文件", + "labels": "文件整理", + "version": "2.0", + "icon": "Ombi_A.png", + "author": "DzAvril", + "level": 1, + "history": { + "v2.1": "联动删除历史记录", + "v2.0": "联动删除种子,需安装插件[下载器助手]并打开监听源文件事件", + "v1.9": "增加清理刮削文件功能(beta)", + "v1.8": "增加清理空目录功能(beta)", + "v1.7": "修复因未监测重命名事件导致的清理硬链接失败的问题", + "v1.6": "提升插件性能" + } + }, + "LinkMonitor": { + "name": "实时硬链接", + "description": "监控目录文件变化,实时硬链接。", + "labels": "文件整理", + "version": "1.6", + "icon": "Linkace_C.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.6": "增强API安全性" + } + }, + "CategoryEditor": { + "name": "二级分类策略", + "description": "编辑下载目录和媒体库目录的二级分类规则。", + "labels": "文件整理", + "version": "1.1", + "icon": "Bookstack_A.png", + "author": "jxxghp", + "level": 1 + }, + "RemoteIdentifiers": { + "name": "共享识别词", + "description": "从Github、Etherpad等远程文件中获取共享识别词并应用。", + "labels": "识别", + "version": "2.2", + "icon": "words.png", + "author": "honue", + "level": 1 + }, + "NeoDBSync": { + "name": "NeoDB 想看", + "description": "同步 NeoDB 想看条目,自动添加订阅。", + "labels": "订阅", + "version": "1.1", + "icon": "NeoDB.jpeg", + "author": "hcplantern", + "level": 1, + "history": { + "v1.1": "直接添加订阅,不提前进行搜索下载" + } + }, + "PlayletCategory": { + "name": "短剧自动分类", + "description": "网络短剧自动整理到独立的分类目录。", + "labels": "文件整理", + "version": "1.4", + "icon": "Amule_A.png", + "author": "jxxghp", + "level": 1 + }, + "DiagParamAdjust": { + "name": "诊断参数调整", + "description": "Emby专用插件|暂时性解决emby字幕偏移问题,需要emby安装Diagnostics插件。", + "labels": "Emby", + "version": "1.3", + "icon": "Gatus_A.png", + "author": "jeblove", + "level": 1 + }, + "QbCommand": { + "name": "QB远程操作", + "description": "通过定时任务或交互命令远程操作QB暂停/开始/限速等。", + "labels": "下载管理,Qbittorrent", + "version": "1.5", + "icon": "Qbittorrent_A.png", + "author": "DzAvril", + "level": 1, + "history": { + "v1.5": "可选特定路径下的做种不会被暂停", + "v1.4": "可选某些站点不再做种(暂停做种后不会被恢复)" + } + }, + "TrCommand": { + "name": "TR远程操作", + "description": "通过定时任务或交互命令远程操作TR暂停/开始/限速等。", + "labels": "下载管理,Transmission", + "version": "1.1", + "icon": "Transmission_A.png", + "author": "Hoey", + "level": 1 + }, + "IpDetect": { + "name": "本地IP检测", + "description": "如果QB、TR等服务在本地部署,当本地IP改变时自动修改其Server IP。", + "labels": "系统设置", + "version": "1.1", + "icon": "ipAddress.png", + "author": "DzAvril", + "level": 1 + }, + "TrackerEditor": { + "name": "Tracker替换", + "description": "批量替换种子tracker,支持周期性巡检(如为TR,仅支持4.0以上版本)。", + "labels": "做种", + "version": "1.5", + "icon": "trackereditor_A.png", + "author": "honue", + "level": 1 + }, + "ContractCheck": { + "name": "契约检查", + "description": "定时检查保种契约达成情况。", + "labels": "做种", + "version": "1.4", + "icon": "contract.png", + "author": "DzAvril", + "level": 1, + "history": { + "v1.4": "支持仪表板组件显示", + "v1.3": "修复观众做种数据异常问题", + "v1.2": "修复契约检查无数据返回的问题" + } + }, + "DownloaderHelper": { + "name": "下载器助手", + "description": "自动做种、站点标签、自动删种。", + "labels": "下载管理", + "version": "1.7", + "icon": "DownloaderHelper.png", + "author": "hotlcc", + "level": 2, + "history": { + "v1.7": "优化了表单界面和一些逻辑。", + "v1.6": "修复事件触发tr打标问题;表单界面优化。" + } + }, + "FeiShuMsg": { + "name": "飞书机器人消息通知", + "description": "支持使用飞书群聊机器人发送消息通知。", + "labels": "消息通知", + "version": "1.0", + "icon": "FeiShu_A.png", + "author": "InfinityPacer", + "level": 2 + }, + "IyuuAuth": { + "name": "IYUU站点绑定", + "description": "为IYUU账号绑定认证站点,以便用于用户认证和辅种。", + "labels": "IYUU", + "version": "1.1", + "icon": "Iyuu_A.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.1": "修复IYUU站点绑定失败问题" + } + }, + "NtfyMsg": { + "name": "ntfy消息推送", + "description": "支持使用ntfy发送消息通知。", + "labels": "消息通知", + "version": "1.0", + "icon": "Ntfy_A.png", + "author": "lethargicScribe", + "level": 1 + }, + "PluginAutoUpgrade": { + "name": "插件自动升级", + "description": "定时检测、升级插件。", + "labels": "自动更新", + "version": "1.6", + "icon": "PluginAutoUpgrade.png", + "author": "hotlcc", + "level": 1, + "history": { + "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的登录壁纸下载到本地。", + "labels": "工具", + "version": "1.1", + "icon": "Macos_Sierra.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.1": "修复下载Bing每日壁纸时文件名错乱的问题" + } + }, + "MPServerStatus": { + "name": "MoviePilot服务器监控", + "description": "在仪表板中实时显示MoviePilot公共服务器状态。", + "labels": "工具", + "version": "1.0", + "icon": "Duplicati_A.png", + "author": "jxxghp", + "level": 1 } - } -} \ No newline at end of file +} diff --git a/plugins/brushflow/__init__.py b/plugins/brushflow/__init__.py index 555aee0..c0432fa 100644 --- a/plugins/brushflow/__init__.py +++ b/plugins/brushflow/__init__.py @@ -10,6 +10,9 @@ from typing import Any, List, Dict, Tuple, Optional, Union, Set from urllib.parse import urlparse, parse_qs, unquote import pytz +from app.helper.sites import SitesHelper +from apscheduler.schedulers.background import BackgroundScheduler + from app import schemas from app.chain.torrents import TorrentsChain from app.core.config import settings @@ -17,7 +20,6 @@ from app.core.context import MediaInfo from app.core.metainfo import MetaInfo from app.db.site_oper import SiteOper from app.db.subscribe_oper import SubscribeOper -from app.helper.sites import SitesHelper from app.log import logger from app.modules.qbittorrent import Qbittorrent from app.modules.transmission import Transmission @@ -25,7 +27,6 @@ from app.plugins import _PluginBase from app.schemas import NotificationType, TorrentInfo, MediaType from app.utils.http import RequestUtils from app.utils.string import StringUtils -from apscheduler.schedulers.background import BackgroundScheduler lock = threading.Lock() @@ -146,9 +147,10 @@ class BrushConfig: @staticmethod def get_demo_site_config() -> str: - desc = ("// 以下为配置示例,请参考:https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md 进行配置\n" - "// 如与全局保持一致的配置项,请勿在站点配置中配置\n" - "// 注意无关内容需使用 // 注释\n") + desc = ( + "// 以下为配置示例,请参考:https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/plugins/brushflowlowfreq/README.md 进行配置\n" + "// 如与全局保持一致的配置项,请勿在站点配置中配置\n" + "// 注意无关内容需使用 // 注释\n") config = """[{ "sitename": "站点1", "seed_time": 96, @@ -249,7 +251,7 @@ class BrushFlow(_PluginBase): # 插件图标 plugin_icon = "brush.jpg" # 插件版本 - plugin_version = "2.9" + plugin_version = "3.1" # 插件作者 plugin_author = "jxxghp,InfinityPacer" # 作者主页 @@ -440,6 +442,340 @@ class BrushFlow(_PluginBase): return services + def __get_total_elements(self) -> List[dict]: + """ + 组装汇总元素 + """ + # 统计数据 + statistic_info = self.__get_statistic_info() + # 总上传量 + total_uploaded = StringUtils.str_filesize(statistic_info.get("uploaded") or 0) + # 总下载量 + total_downloaded = StringUtils.str_filesize(statistic_info.get("downloaded") or 0) + # 下载种子数 + total_count = statistic_info.get("count") or 0 + # 删除种子数 + total_deleted = statistic_info.get("deleted") or 0 + # 待归档种子数 + total_unarchived = statistic_info.get("unarchived") or 0 + # 活跃种子数 + total_active = statistic_info.get("active") or 0 + # 活跃上传量 + total_active_uploaded = StringUtils.str_filesize(statistic_info.get("active_uploaded") or 0) + # 活跃下载量 + total_active_downloaded = StringUtils.str_filesize(statistic_info.get("active_downloaded") or 0) + + return [ + # 总上传量 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3, + 'sm': 6 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/upload.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总上传量 / 活跃' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f"{total_uploaded} / {total_active_uploaded}" + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 总下载量 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3, + 'sm': 6 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/download.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总下载量 / 活跃' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f"{total_downloaded} / {total_active_downloaded}" + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 下载种子数 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3, + 'sm': 6 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/seed.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '下载种子数 / 活跃' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f"{total_count} / {total_active}" + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 删除种子数 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3, + 'sm': 6 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/delete.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '删除种子数 / 待归档' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f"{total_deleted} / {total_unarchived}" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + ] + + def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、全局配置参考: + { + "refresh": 10 // 自动刷新时间,单位秒 + } + 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + """ + # 列配置 + cols = { + "cols": 12 + } + # 全局配置 + attrs = {} + # 拼装页面元素 + elements = [ + { + 'component': 'VRow', + 'content': self.__get_total_elements() + } + ] + return cols, attrs, elements + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: """ 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 @@ -1239,7 +1575,7 @@ class BrushFlow(_PluginBase): "component": "VSwitch", "props": { "model": "dialog_closed", - "label": "设置站点" + "label": "打开站点配置窗口" } } ] @@ -1342,15 +1678,20 @@ class BrushFlow(_PluginBase): 'content': [ { 'component': 'span', - 'text': '部分配置项以及细节请参考:' + 'text': '注意:详细配置说明以及刷流规则请参考:' }, { 'component': 'a', 'props': { - 'href': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md', + 'href': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/plugins/brushflowlowfreq/README.md', 'target': '_blank' }, - 'text': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md' + 'content': [ + { + 'component': 'u', + 'text': 'README' + } + ] } ] } @@ -1449,10 +1790,15 @@ class BrushFlow(_PluginBase): { 'component': 'a', 'props': { - 'href': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md', + 'href': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/plugins/brushflowlowfreq/README.md', 'target': '_blank' }, - 'text': 'https://github.com/InfinityPacer/MoviePilot-Plugins/blob/main/README.md' + 'content': [ + { + 'component': 'u', + 'text': 'README' + } + ] } ] } @@ -1491,8 +1837,6 @@ class BrushFlow(_PluginBase): def get_page(self) -> List[dict]: # 种子明细 torrents = self.get_data("torrents") or {} - # 统计数据 - statistic_info = self.__get_statistic_info() if not torrents: return [ @@ -1508,22 +1852,7 @@ class BrushFlow(_PluginBase): data_list = torrents.values() # 按time倒序排序 data_list = sorted(data_list, key=lambda x: x.get("time") or 0, reverse=True) - # 总上传量 - total_uploaded = StringUtils.str_filesize(statistic_info.get("uploaded") or 0) - # 总下载量 - total_downloaded = StringUtils.str_filesize(statistic_info.get("downloaded") or 0) - # 下载种子数 - total_count = statistic_info.get("count") or 0 - # 删除种子数 - total_deleted = statistic_info.get("deleted") or 0 - # 待归档种子数 - total_unarchived = statistic_info.get("unarchived") or 0 - # 活跃种子数 - total_active = statistic_info.get("active") or 0 - # 活跃上传量 - total_active_uploaded = StringUtils.str_filesize(statistic_info.get("active_uploaded") or 0) - # 活跃下载量 - total_active_downloaded = StringUtils.str_filesize(statistic_info.get("active_downloaded") or 0) + # 表格标题 headers = [ {'title': '站点', 'key': 'site', 'sortable': True}, @@ -1551,287 +1880,12 @@ class BrushFlow(_PluginBase): return [ { 'component': 'VRow', - 'content': [ - # 总上传量 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/upload.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总上传量 / 活跃' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': f"{total_uploaded} / {total_active_uploaded}" - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 总下载量 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/download.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总下载量 / 活跃' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': f"{total_downloaded} / {total_active_downloaded}" - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 下载种子数 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/seed.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '下载种子数 / 活跃' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': f"{total_count} / {total_active}" - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 删除种子数 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/delete.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '删除种子数 / 待归档' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': f"{total_deleted} / {total_unarchived}" - } - ] - } - ] - } - ] - } - ] - } - ] - }, + 'props': { + 'style': { + 'overflow': 'hidden', + } + }, + 'content': self.__get_total_elements() + [ # 种子明细 { 'component': 'VRow', @@ -2149,6 +2203,12 @@ class BrushFlow(_PluginBase): torrent_tasks.values()): return False, "重复种子" + # 不同站点如果遇到相同种子,判断前一个种子是否已经在做种,否则排除处理 + if torrent.title: + if any(torrent.site_name != f"{task.get('site_name')}" and torrent.title == f"{task.get('title')}" + and not task.get("seed_time") for task in torrent_tasks.values()): + return False, "其他站点存在尚未下载完成的相同种子" + # 促销条件 if brush_config.freeleech and torrent.downloadvolumefactor != 0: return False, "非免费种子" @@ -2286,15 +2346,15 @@ class BrushFlow(_PluginBase): # 如果配置了动态删除以及删种阈值,则根据动态删种进行分组处理 if brush_config.proxy_delete and brush_config.delete_size_range: logger.info("已开启动态删种,按系统默认动态删种条件开始检查任务") - proxy_delete_hashs = self.__delete_torrent_for_proxy(torrents=check_torrents, - torrent_tasks=torrent_tasks) or [] - need_delete_hashes.extend(proxy_delete_hashs) + proxy_delete_hashes = self.__delete_torrent_for_proxy(torrents=check_torrents, + torrent_tasks=torrent_tasks) or [] + need_delete_hashes.extend(proxy_delete_hashes) # 否则均认为是没有开启动态删种 else: logger.info("没有开启动态删种,按用户设置删种条件开始检查任务") - not_proxy_delete_hashs = self.__delete_torrent_for_evaluate_conditions(torrents=check_torrents, - torrent_tasks=torrent_tasks) or [] - need_delete_hashes.extend(not_proxy_delete_hashs) + not_proxy_delete_hashes = self.__delete_torrent_for_evaluate_conditions(torrents=check_torrents, + torrent_tasks=torrent_tasks) or [] + need_delete_hashes.extend(not_proxy_delete_hashes) if need_delete_hashes: if downloader.delete_torrents(ids=need_delete_hashes, delete_file=True): @@ -2493,7 +2553,7 @@ class BrushFlow(_PluginBase): 根据条件删除种子并获取已删除列表 """ brush_config = self.__get_brush_config() - delete_hashs = [] + delete_hashes = [] for torrent in torrents: torrent_hash = self.__get_hash(torrent) @@ -2512,8 +2572,8 @@ class BrushFlow(_PluginBase): torrent_info=torrent_info, torrent_task=torrent_task) if should_delete: - delete_hashs.append(torrent_hash) - reason = "触发动态删除," + reason if proxy_delete else reason + delete_hashes.append(torrent_hash) + reason = "触发动态删除阈值," + reason if proxy_delete else reason self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, reason=reason) logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") @@ -2521,7 +2581,7 @@ class BrushFlow(_PluginBase): if brush_config.log_more: logger.info(f"站点:{site_name},{reason},不删除种子:{torrent_title}|{torrent_desc}") - return delete_hashs + return delete_hashes def __delete_torrent_for_evaluate_proxy_pre_conditions(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: @@ -2529,7 +2589,7 @@ class BrushFlow(_PluginBase): 根据动态删除前置条件排除H&R种子后删除种子并获取已删除列表 """ brush_config = self.__get_brush_config() - delete_hashs = [] + delete_hashes = [] for torrent in torrents: torrent_hash = self.__get_hash(torrent) @@ -2552,7 +2612,7 @@ class BrushFlow(_PluginBase): should_delete, reason = self.__evaluate_proxy_pre_conditions_for_delete(site_name=site_name, torrent_info=torrent_info) if should_delete: - delete_hashs.append(torrent_hash) + delete_hashes.append(torrent_hash) self.__send_delete_message(site_name=site_name, torrent_title=torrent_title, torrent_desc=torrent_desc, reason=reason) logger.info(f"站点:{site_name},{reason},删除种子:{torrent_title}|{torrent_desc}") @@ -2560,7 +2620,7 @@ class BrushFlow(_PluginBase): if brush_config.log_more: logger.info(f"站点:{site_name},{reason},不删除种子:{torrent_title}|{torrent_desc}") - return delete_hashs + return delete_hashes def __delete_torrent_for_proxy(self, torrents: List[Any], torrent_tasks: Dict[str, dict]) -> List: """ @@ -3557,7 +3617,7 @@ class BrushFlow(_PluginBase): now = datetime.now() return (now - pubdate).total_seconds() // 60 except Exception as e: - print(str(e)) + logger.error(f"发布时间 {pubdate} 获取分钟失败,错误详情: {e}") return 0 @staticmethod diff --git a/plugins/contractcheck/__init__.py b/plugins/contractcheck/__init__.py index 64ad475..00b98a1 100644 --- a/plugins/contractcheck/__init__.py +++ b/plugins/contractcheck/__init__.py @@ -39,7 +39,7 @@ class ContractCheck(_PluginBase): # 插件图标 plugin_icon = "contract.png" # 插件版本 - plugin_version = "1.1" + plugin_version = "1.4" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -84,6 +84,7 @@ class ContractCheck(_PluginBase): _notify: bool = False _queue_cnt: int = 5 _contract_infos: str = "" + _dashboard_type: str = "brief" def init_plugin(self, config: dict = None): self.sites = SitesHelper() @@ -99,7 +100,10 @@ class ContractCheck(_PluginBase): self._queue_cnt = config.get("queue_cnt") self._contract_infos = config.get("contract_infos") self.parse_contract_infos(self._contract_infos) + self._dashboard_type = config.get("dashboard_type") or "brief" + # 获取历史数据 + self._sites_data = self.get_data("contractcheck") if self._enabled or self._onlyonce: # 加载模块 self._site_schema = ModuleHelper.load( @@ -240,6 +244,295 @@ class ContractCheck(_PluginBase): return ret_jobs return [] + def __get_total_elements(self, dashboard_type: str) -> List[dict]: + if dashboard_type == "detail": + return self.__get_detail_report() + else: + return self.__get_brief_report() + + def __get_detail_report(self): + """ + 拼装插件详情页面,需要返回页面配置,同时附带数据 + """ + logger.info(f"self._sites_data: {self._sites_data} ") + if not self._sites_data: + return [ + { + "component": "div", + "text": "暂无数据", + "props": { + "class": "text-center", + }, + } + ] + + # 站点数据明细 + site_trs = [ + { + "component": "tr", + "props": {"class": "text-sm"}, + "content": [ + { + "component": "td", + "props": { + "class": "whitespace-nowrap break-keep text-high-emphasis" + }, + "text": site, + }, + {"component": "td", "text": data.get("is_official")}, + {"component": "td", "text": data.get("contract_size")}, + {"component": "td", "text": data.get("contract_num")}, + { + "component": "td", + "text": str(data.get("contract_duration")) + " 天", + }, + {"component": "td", "text": data.get("contract_start_on")}, + {"component": "td", "text": data.get("total_seed_size")}, + {"component": "td", "text": data.get("total_seed_num")}, + {"component": "td", "text": data.get("official_seed_size")}, + {"component": "td", "text": data.get("official_seed_num")}, + { + "component": "td", + "props": { + "class": ( + "text-success" + if data.get("is_satisfied") + else "text-error" + ) + }, + "text": "是" if data.get("is_satisfied") else "否", + }, + {"component": "td", "text": data.get("size_gap")}, + {"component": "td", "text": data.get("num_gap")}, + {"component": "td", "text": str(data.get("duration_gap")) + " 天"}, + ], + } + for site, data in self._sites_data.items() + if not data.get("err_msg") + ] + + # 拼装页面 + return [ + # 各站点数据明细 + { + "component": "VCol", + "props": { + "cols": 12, + }, + "content": [ + { + "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": "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": "th", + "props": {"class": "text-start ps-4"}, + "text": "需增数量", + }, + { + "component": "th", + "props": {"class": "text-start ps-4"}, + "text": "剩余时间", + }, + ], + }, + {"component": "tbody", "content": site_trs}, + ], + } + ], + } + ] + + def __get_brief_report(self): + """ + 拼装插件详情页面,需要返回页面配置,同时附带数据 + """ + logger.info(f"self._sites_data: {self._sites_data} ") + if not self._sites_data: + return [ + { + "component": "div", + "text": "暂无数据", + "props": { + "class": "text-center", + }, + } + ] + + # 站点数据明细 + site_trs = [ + { + "component": "tr", + "props": {"class": "text-sm"}, + "content": [ + { + "component": "td", + "props": { + "class": "whitespace-nowrap break-keep text-high-emphasis" + }, + "text": site, + }, + { + "component": "td", + "props": { + "class": ( + "text-success" + if data.get("is_satisfied") + else "text-error" + ) + }, + "text": "是" if data.get("is_satisfied") else "否", + }, + {"component": "td", "text": data.get("size_gap")}, + {"component": "td", "text": data.get("num_gap")}, + {"component": "td", "text": str(data.get("duration_gap")) + " 天"}, + ], + } + for site, data in self._sites_data.items() + if not data.get("err_msg") + ] + + # 拼装页面 + return [ + # 各站点数据明细 + { + "component": "VCol", + "props": { + "cols": 12, + }, + "content": [ + { + "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": "tbody", "content": site_trs}, + ], + } + ], + } + ] + + def get_dashboard( + self, + ) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、全局配置参考: + { + "refresh": 10 // 自动刷新时间,单位秒 + } + 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + """ + # 列配置 + cols = {"cols": 12} + # 全局配置 + attrs = {} + # 拼装页面元素 + elements = [ + { + "component": "VRow", + "content": self.__get_total_elements(self._dashboard_type), + } + ] + return cols, attrs, elements + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: """ 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 @@ -293,7 +586,7 @@ class ContractCheck(_PluginBase): }, { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VTextField", @@ -307,7 +600,7 @@ class ContractCheck(_PluginBase): }, { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VTextField", @@ -318,6 +611,26 @@ class ContractCheck(_PluginBase): } ], }, + { + "component": "VCol", + "props": {"cols": 12, "md": 3}, + "content": [ + { + "component": "VSelect", + "props": { + "model": "dashboard_type", + "label": "仪表板组件", + "items": [ + { + "title": "详细数据", + "value": "detail", + }, + {"title": "简洁数据", "value": "brief"}, + ], + }, + } + ], + }, { "component": "VCol", "props": {"cols": 12, "md": 12}, @@ -396,168 +709,16 @@ class ContractCheck(_PluginBase): "notify": True, "cron": "5 1 * * *", "queue_cnt": 5, + "dashboard_type": "brief", } def get_page(self) -> List[dict]: """ 拼装插件详情页面,需要返回页面配置,同时附带数据 """ - logger.info(f"self._sites_data: {self._sites_data} ") - if not self._sites_data: - return [ - { - "component": "div", - "text": "暂无数据", - "props": { - "class": "text-center", - }, - } - ] - - # 站点数据明细 - site_trs = [ - { - "component": "tr", - "props": {"class": "text-sm"}, - "content": [ - { - "component": "td", - "props": { - "class": "whitespace-nowrap break-keep text-high-emphasis" - }, - "text": site, - }, - {"component": "td", "text": data.get("is_official")}, - {"component": "td", "text": data.get("contract_size")}, - {"component": "td", "text": data.get("contract_num")}, - { - "component": "td", - "text": str(data.get("contract_duration")) + " 天", - }, - {"component": "td", "text": data.get("contract_start_on")}, - {"component": "td", "text": data.get("total_seed_size")}, - {"component": "td", "text": data.get("total_seed_num")}, - {"component": "td", "text": data.get("official_seed_size")}, - {"component": "td", "text": data.get("official_seed_num")}, - { - "component": "td", - "props": { - "class": ( - "text-success" - if data.get("is_satisfied") - else "text-error" - ) - }, - "text": "是" if data.get("is_satisfied") else "否", - }, - {"component": "td", "text": data.get("size_gap")}, - {"component": "td", "text": data.get("num_gap")}, - {"component": "td", "text": str(data.get("duration_gap")) + " 天"}, - ], - } - for site, data in self._sites_data.items() - if not data.get("err_msg") - ] # 拼装页面 - return [ - { - "component": "VRow", - "content": [ - # 各站点数据明细 - { - "component": "VCol", - "props": { - "cols": 12, - }, - "content": [ - { - "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": "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": "th", - "props": {"class": "text-start ps-4"}, - "text": "需增数量", - }, - { - "component": "th", - "props": {"class": "text-start ps-4"}, - "text": "剩余时间", - }, - ], - }, - {"component": "tbody", "content": site_trs}, - ], - } - ], - } - ], - } - ] + return [{"component": "VRow", "content": self.__get_detail_report()}] def stop_service(self): """ @@ -622,7 +783,7 @@ class ContractCheck(_PluginBase): i = html_text.find("window.location") if i == -1: return None - tmp_url = url + html_text[i: html_text.find(";")].replace( + tmp_url = url + html_text[i : html_text.find(";")].replace( '"', "" ).replace("+", "").replace(" ", "").replace( "window.location=", "" @@ -641,12 +802,15 @@ class ContractCheck(_PluginBase): html_text = res.text if not html_text: return None - else: + elif res is not None: logger.error( "站点 %s 被反爬限制:%s, 状态码:%s" % (site_name, url, res.status_code) ) return None + else: + logger.error(f"站点 {site_name} 无法访问:{url}") + return None # 兼容假首页情况,假首页通常没有 :\n\n' - '例如:\n' - 'chdbits.xyz:ptchdbits.co', - 'hint': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' + 'model': '_config_tracker_mappings_dialog_closed', + 'label': '配置Tracker映射', + 'hint': '点击展开Tracker映射配置窗口。' } }] }] }, { - 'component': 'VTabs', + 'component': 'VDialog', 'props': { - 'model': '_tabs', - 'height': 72, - 'style': { - 'margin-top': '20px', - 'margin-bottom': '20px' - } + 'model': '_config_tracker_mappings_dialog_closed', + 'max-width': '60rem' }, 'content': [{ - 'component': 'VTab', + 'component': 'VCard', 'props': { - 'value': 'qbittorrent' - }, - 'text': 'qbittorrent' - }, { - 'component': 'VTab', - 'props': { - 'value': 'transmission' - }, - 'text': 'transmission' - }] - }, { - 'component': 'VWindow', - 'props': { - 'model': '_tabs' - }, - 'content': [{ - 'component': 'VWindowItem', - 'props': { - 'value': 'qbittorrent' + '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, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 + 'cols': 12 }, 'content': [{ - 'component': 'VSwitch', + 'component': 'VTextarea', 'props': { - 'model': 'qb_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': 'qb_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': 'qb_enable_tagging', - 'label': '站点标签', - 'hint': '是否开启站点标签功能' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'qb_enable_delete', - 'label': '自动删种', - 'hint': '是否开启自动删种功能' + 'model': 'tracker_mappings', + 'label': 'Tracker映射', + 'placeholder': '格式:\n' + ':\n\n' + '例如:\n' + 'chdbits.xyz:ptchdbits.co', + 'hint': 'Tracker映射。用于在站点打标签时,指定tracker和站点域名不同的种子的域名对应关系;前面为tracker域名(完整域名或者主域名皆可),中间是英文冒号,后面是站点域名。' } }] }] }] - }, { - 'component': 'VWindowItem', + }] + }, { + 'component': 'VRow', + 'content': [{ + 'component': 'VCol', 'props': { - 'value': 'transmission' + 'cols': 12 }, 'content': [{ - 'component': 'VRow', - 'content': [{ - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'tr_enable', - 'label': '任务开关' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'tr_enable_seeding', - 'label': '自动做种' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'tr_enable_tagging', - 'label': '站点标签' - } - }] - }, { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12 - }, - 'content': [{ - 'component': 'VSwitch', - 'props': { - 'model': 'tr_enable_delete', - 'label': '自动删种' - } - }] - }] + '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 }] }] }, { diff --git a/plugins/downloaderhelper/module.py b/plugins/downloaderhelper/module.py index e94ad44..cab6158 100644 --- a/plugins/downloaderhelper/module.py +++ b/plugins/downloaderhelper/module.py @@ -1,15 +1,19 @@ from typing import Set, List, Optional +from enum import Enum -class Constants: +class Downloader(Enum): """ - 常量 + 下载器枚举 """ - # 下载器ID - # qb下载器id - qb_downloader_id: str = 'qbittorrent' - # tr下载器id - tr_downloader_id: str = 'transmission' + 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 class TaskResult: @@ -133,14 +137,14 @@ class TaskContext: 是否选择了qb下载器 :return: 是否选择了qb下载器 """ - return self.__is_selected_the_downloader(Constants.qb_downloader_id) + 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(Constants.tr_downloader_id) + return self.__is_selected_the_downloader(Downloader.TR.id) def enable_seeding(self, enable_seeding: bool = True): """ diff --git a/plugins/mergesiteswitch/__init__.py b/plugins/mergesiteswitch/__init__.py index 96456d9..a286915 100644 --- a/plugins/mergesiteswitch/__init__.py +++ b/plugins/mergesiteswitch/__init__.py @@ -3,11 +3,12 @@ import os from typing import Any, List, Dict, Tuple from app.core.event import eventmanager, Event -from app.db.models.site import Site +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 @@ -19,7 +20,7 @@ class MergeSiteSwitch(_PluginBase): # 插件图标 plugin_icon = "world.png" # 插件版本 - plugin_version = "1.0" + plugin_version = "1.1" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -46,6 +47,8 @@ class MergeSiteSwitch(_PluginBase): __plugin_id_iyuu_auto_seed: str = 'IYUUAutoSeed' # 站点刷流 __plugin_id_brush_flow: str = 'BrushFlow' + # 青蛙辅种助手 + __plugin_id_cross_seed: str = 'CrossSeed' # 配置相关 # 插件缺省配置 @@ -369,6 +372,43 @@ class MergeSiteSwitch(_PluginBase): }] }] }) + # 青蛙辅种助手 + 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', @@ -500,6 +540,12 @@ class MergeSiteSwitch(_PluginBase): """ 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: """ 判断是否开启任意跟随按钮 @@ -564,6 +610,10 @@ class MergeSiteSwitch(_PluginBase): 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 @@ -588,6 +638,8 @@ class MergeSiteSwitch(_PluginBase): 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: @@ -621,6 +673,8 @@ class MergeSiteSwitch(_PluginBase): 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]: @@ -687,6 +741,19 @@ class MergeSiteSwitch(_PluginBase): 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: """ 设置插件配置值 @@ -698,6 +765,7 @@ class MergeSiteSwitch(_PluginBase): 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]: """ @@ -769,6 +837,20 @@ class MergeSiteSwitch(_PluginBase): 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 @@ -846,6 +928,17 @@ class MergeSiteSwitch(_PluginBase): 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): """ 针对站点事件更新各项配置 @@ -868,6 +961,8 @@ class MergeSiteSwitch(_PluginBase): 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): diff --git a/plugins/mpserverstatus/__init__.py b/plugins/mpserverstatus/__init__.py new file mode 100644 index 0000000..0768b34 --- /dev/null +++ b/plugins/mpserverstatus/__init__.py @@ -0,0 +1,453 @@ +import re +import time +from typing import List, Tuple, Dict, Any, Optional + +from app.log import logger +from app.plugins import _PluginBase +from app.utils.http import RequestUtils + + +class MPServerStatus(_PluginBase): + # 插件名称 + plugin_name = "MoviePilot服务监控" + # 插件描述 + plugin_desc = "在仪表板中实时显示MoviePilot公共服务器状态。" + # 插件图标 + plugin_icon = "Duplicati_A.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "MPServer_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + _enable: bool = False + _server_base = "https://movie-pilot.org/status" + + def init_plugin(self, config: dict = None): + self._enable = config.get("enable") if config.get("enable") else False + + @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': '启用插件', + } + } + ] + } + ] + } + ] + } + ], { + "enable": self._enable, + } + + def get_page(self) -> List[dict]: + pass + + def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、全局配置(自动刷新等);3、仪表板页面元素配置json(含数据) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、全局配置参考: + { + "refresh": 10 // 自动刷新时间,单位秒 + } + 3、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + """ + # 列配置 + cols = { + "cols": 12, + "md": 8 + } + # 全局配置 + attrs = { + "refresh": 10 + } + # 读取服务器文本 + start_time = time.time() + logger.info(f"请求服务器状态 {self._server_base}...") + res = RequestUtils().get_res(self._server_base) + seconds = time.time() - start_time + logger.info(f"请求耗时:{seconds}秒") + if not res: + logger.warn(f"请求服务器状态失败:{res.status_code if res is not None else '网络错误'}") + elements = [ + { + 'component': 'div', + 'text': '无法连接服务器', + 'props': { + 'class': 'text-center', + } + } + ] + else: + """ + Active connections: 62 + server accepts handled requests + 468843 468843 1368256 + Reading: 0 Writing: 1 Waiting: 61 + """ + status_lines = res.text.strip().split('\n') + active_connections = int(status_lines[0].split(':')[1].strip()) + accepts, handled, requests = map(int, status_lines[2].split()) + reading, writing, waiting = map(int, re.findall(r'\d+', status_lines[3])) + elements = [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '连接耗时' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f"{seconds:.2f} 秒" + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '活跃连接' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': active_connections + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '等待连接' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': waiting + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '处理中连接' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': reading + writing + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总请求数' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': requests + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总连接数' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': accepts + } + ] + } + ] + } + ] + } + ] + }, + ] + } + ] + }] + + return cols, attrs, elements + + def get_state(self) -> bool: + return self._enable + + def stop_service(self): + pass diff --git a/plugins/personmeta/__init__.py b/plugins/personmeta/__init__.py index 5c769fb..e5a2cdd 100644 --- a/plugins/personmeta/__init__.py +++ b/plugins/personmeta/__init__.py @@ -14,6 +14,7 @@ from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from requests import RequestException +from app import schemas from app.chain.mediaserver import MediaServerChain from app.chain.tmdb import TmdbChain from app.core.config import settings @@ -39,7 +40,7 @@ class PersonMeta(_PluginBase): # 插件图标 plugin_icon = "actor.png" # 插件版本 - plugin_version = "1.2" + plugin_version = "1.3" # 插件作者 plugin_author = "jxxghp" # 作者主页 @@ -510,9 +511,9 @@ class PersonMeta(_PluginBase): # 从TMDB信息中更新人物信息 person_tmdbid, person_imdbid = __get_peopleid(personinfo) if person_tmdbid: - person_tmdbinfo = self.tmdbchain.person_detail(int(person_tmdbid)) - if person_tmdbinfo: - cn_name = self.__get_chinese_name(person_tmdbinfo) + person_detail = self.tmdbchain.person_detail(int(person_tmdbid)) + if person_detail: + cn_name = self.__get_chinese_name(person_detail) if cn_name: # 更新中文名 logger.debug(f"{people.get('Name')} 从TMDB获取到中文名:{cn_name}") @@ -520,13 +521,13 @@ class PersonMeta(_PluginBase): ret_people["Name"] = cn_name updated_name = True # 更新中文描述 - biography = person_tmdbinfo.get("biography") + biography = person_detail.biography if biography and StringUtils.is_chinese(biography): logger.debug(f"{people.get('Name')} 从TMDB获取到中文描述") personinfo["Overview"] = biography updated_overview = True # 图片 - profile_path = person_tmdbinfo.get('profile_path') + profile_path = person_detail.profile_path if profile_path: logger.debug(f"{people.get('Name')} 从TMDB获取到图片:{profile_path}") profile_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{profile_path}" @@ -994,12 +995,12 @@ class PersonMeta(_PluginBase): return None @staticmethod - def __get_chinese_name(personinfo: dict) -> str: + def __get_chinese_name(personinfo: schemas.MediaPerson) -> str: """ 获取TMDB别名中的中文名 """ try: - also_known_as = personinfo.get("also_known_as") or [] + also_known_as = personinfo.also_known_as or [] if also_known_as: for name in also_known_as: if name and StringUtils.is_chinese(name): diff --git a/plugins/pluginautoupgrade/__init__.py b/plugins/pluginautoupgrade/__init__.py index af63364..59c9afb 100644 --- a/plugins/pluginautoupgrade/__init__.py +++ b/plugins/pluginautoupgrade/__init__.py @@ -23,7 +23,7 @@ class PluginAutoUpgrade(_PluginBase): # 插件图标 plugin_icon = "PluginAutoUpgrade.png" # 插件版本 - plugin_version = "1.4" + plugin_version = "1.6" # 插件作者 plugin_author = "hotlcc" # 作者主页 @@ -52,7 +52,9 @@ class PluginAutoUpgrade(_PluginBase): # 配置相关 # 插件缺省配置 __config_default: Dict[str, Any] = { - 'cron': '* 0/4 * * *' + 'cron': '* 0/4 * * *', + 'save_record_quantity': 100, + 'display_record_quantity': 10, } # 插件用户配置 __config: Dict[str, Any] = {} @@ -61,6 +63,8 @@ class PluginAutoUpgrade(_PluginBase): """ 初始化插件 """ + # 修正配置 + config = self.__fix_config(config=config) # 加载插件配置 self.__config = config # 停止现有服务 @@ -128,6 +132,10 @@ class PluginAutoUpgrade(_PluginBase): 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 = [{ @@ -200,6 +208,41 @@ class PluginAutoUpgrade(_PluginBase): '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': { @@ -215,7 +258,7 @@ class PluginAutoUpgrade(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12 + 'xxl': 6, 'xl': 6, 'lg': 6, 'md': 6, 'sm': 6, 'xs': 12 }, 'content': [{ 'component': 'VSelect', @@ -355,6 +398,19 @@ class PluginAutoUpgrade(_PluginBase): finally: self.__exit_event.clear() + def __fix_config(self, config: dict) -> dict: + """ + 修正配置 + """ + if not config: + config = {} + save_record_quantity = config.get("save_record_quantity") + config['save_record_quantity'] = int(save_record_quantity) if save_record_quantity else None + 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: """ 获取插件配置项 @@ -366,7 +422,7 @@ class PluginAutoUpgrade(_PluginBase): return None config = self.__config if self.__config else {} config_value = config.get(config_key) - if config_value is None and use_default: + 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 @@ -627,8 +683,9 @@ class PluginAutoUpgrade(_PluginBase): if not upgrade_records: upgrade_records = [] upgrade_records.extend(records) - # 最多保存100条 - upgrade_records = upgrade_records[-100:] + # 最多保存多少条 + 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 @@ -646,8 +703,9 @@ class PluginAutoUpgrade(_PluginBase): upgrade_records = self.get_data(self.__data_key_upgrade_records) if not upgrade_records: return [] - # 只展示最近10条 - upgrade_records = upgrade_records[-10:] + # 只展示最近多少条 + 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] # 按时间倒序 diff --git a/plugins/qbcommand/__init__.py b/plugins/qbcommand/__init__.py index 89466c2..78f6916 100644 --- a/plugins/qbcommand/__init__.py +++ b/plugins/qbcommand/__init__.py @@ -27,7 +27,7 @@ class QbCommand(_PluginBase): # 插件图标 plugin_icon = "Qbittorrent_A.png" # 插件版本 - plugin_version = "1.4" + plugin_version = "1.5" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -60,7 +60,7 @@ class QbCommand(_PluginBase): _op_sites = [] _multi_level_root_domain = ["edu.cn", "com.cn", "net.cn", "org.cn"] _scheduler = None - + _exclude_dirs = "" def init_plugin(self, config: dict = None): self._sites = SitesHelper() self._siteoper = SiteOper() @@ -87,6 +87,7 @@ class QbCommand(_PluginBase): all_sites = [site for site in self._sites.get_indexers() if not site.get("public")] + self.__custom_sites() # 过滤掉没有选中的站点 self._op_sites = [site for site in all_sites if site.get("id") in self._op_site_ids] + self._exclude_dirs = config.get("exclude_dirs") or "" if self._only_pause_once or self._only_resume_once: if self._only_pause_once and self._only_resume_once: @@ -121,6 +122,7 @@ class QbCommand(_PluginBase): "pause_cron": self._pause_cron, "resume_cron": self._resume_cron, "op_site_ids": self._op_site_ids, + "exclude_dirs": self._exclude_dirs, } ) @@ -430,14 +432,6 @@ class QbCommand(_PluginBase): hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( self.get_torrents_status(all_torrents) ) - if type == self.TorrentType.DOWNLOADING: - to_be_paused = hash_downloading - elif type == self.TorrentType.UPLOADING: - to_be_paused = hash_uploading - elif type == self.TorrentType.CHECKING: - to_be_paused = hash_checking - else: - to_be_paused = hash_downloading + hash_uploading + hash_checking logger.info( f"暂定任务启动 \n" @@ -461,6 +455,19 @@ class QbCommand(_PluginBase): f"错误数量: {len(hash_error)}\n" f"暂停操作中请稍等...\n", ) + pause_torrents = self.filter_pause_torrents(all_torrents) + hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( + self.get_torrents_status(pause_torrents) + ) + if type == self.TorrentType.DOWNLOADING: + to_be_paused = hash_downloading + elif type == self.TorrentType.UPLOADING: + to_be_paused = hash_uploading + elif type == self.TorrentType.CHECKING: + to_be_paused = hash_checking + else: + to_be_paused = hash_downloading + hash_uploading + hash_checking + if len(to_be_paused) > 0: if self._qb.stop_torrents(ids=to_be_paused): logger.info(f"暂停了{len(to_be_paused)}个种子") @@ -501,6 +508,22 @@ class QbCommand(_PluginBase): f"错误数量: {len(hash_error)}\n", ) + def __is_excluded(self, file_path) -> bool: + """ + 是否排除目录 + """ + for exclude_dir in self._exclude_dirs.split("\n"): + if exclude_dir and exclude_dir in str(file_path): + return True + return False + def filter_pause_torrents(self, all_torrents): + torrents = [] + for torrent in all_torrents: + if self.__is_excluded(torrent.get("content_path")): + continue + torrents.append(torrent) + return torrents + @eventmanager.register(EventType.PluginAction) def handle_resume_torrent(self, event: Event): if not self._enabled: @@ -516,7 +539,6 @@ class QbCommand(_PluginBase): return all_torrents = self.get_all_torrents() - all_torrents = self.filter_torrents(all_torrents) hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( self.get_torrents_status(all_torrents) ) @@ -542,6 +564,11 @@ class QbCommand(_PluginBase): f"错误数量: {len(hash_error)}\n" f"开始操作中请稍等...\n", ) + + resume_torrents = self.filter_resume_torrents(all_torrents) + hash_downloading, hash_uploading, hash_paused, hash_checking, hash_error = ( + self.get_torrents_status(resume_torrents) + ) if not self._qb.start_torrents(ids=hash_paused): logger.error(f"开始种子失败") if self._notify: @@ -579,7 +606,7 @@ class QbCommand(_PluginBase): f"错误数量: {len(hash_error)}\n", ) - def filter_torrents(self, all_torrents): + def filter_resume_torrents(self, all_torrents): """ 过滤掉不参与保种的种子 """ @@ -644,14 +671,13 @@ class QbCommand(_PluginBase): if self._notify: self.post_message( mtype=NotificationType.SiteMessage, - title=f"【QB开始任务启动】", + title=f"【QB任务状态】", text=f"种子总数: {len(all_torrents)} \n" f"做种数量: {len(hash_uploading)}\n" f"下载数量: {len(hash_downloading)}\n" f"检查数量: {len(hash_checking)}\n" f"暂停数量: {len(hash_paused)}\n" f"错误数量: {len(hash_error)}\n" - f"开始操作中请稍等...\n", ) @eventmanager.register(EventType.PluginAction) @@ -747,7 +773,7 @@ class QbCommand(_PluginBase): elif flag is None and self._enabled and self._enable_upload_limit: flag = self.set_upload_limit(upload_limit) - if flag: + if flag == True: logger.info(f"设置QB限速成功") if self._notify: if upload_limit == 0: @@ -763,7 +789,7 @@ class QbCommand(_PluginBase): title=f"【QB远程操作】", text=text, ) - elif not flag: + elif flag == False: logger.error(f"QB设置限速失败") if self._notify: self.post_message( @@ -1054,6 +1080,26 @@ class QbCommand(_PluginBase): } ], }, + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12}, + "content": [ + { + "component": "VTextarea", + "props": { + "model": "exclude_dirs", + "label": "不暂停保种目录", + "rows": 5, + "placeholder": "该目录下的做种不会暂停,一行一个目录", + }, + } + ], + } + ], + }, { "component": "VRow", "content": [ diff --git a/plugins/removelink/__init__.py b/plugins/removelink/__init__.py index d3e4969..45fc27a 100644 --- a/plugins/removelink/__init__.py +++ b/plugins/removelink/__init__.py @@ -7,7 +7,7 @@ from typing import List, Tuple, Dict, Any from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer - +from app.db.transferhistory_oper import TransferHistoryOper from app.log import logger from app.plugins import _PluginBase from app.schemas import NotificationType @@ -60,9 +60,16 @@ class FileMonitorHandler(FileSystemEventHandler): self.sync.state_set[str(file_path)] = file_path.stat().st_ino def on_deleted(self, event): - if event.is_directory: - return file_path = Path(event.src_path) + if event.is_directory: + # 单独处理文件夹删除触发删除种子 + if self.sync._delete_torrents: + # 发送事件 + logger.info(f"监测到删除文件夹:{file_path}") + eventmanager.send_event( + EventType.DownloadFileDeleted, {"src": str(file_path)} + ) + return if file_path.suffix in [".!qB", ".part", ".mp"]: return logger.info(f"监测到删除文件:{file_path}") @@ -108,7 +115,7 @@ class RemoveLink(_PluginBase): # 插件图标 plugin_icon = "Ombi_A.png" # 插件版本 - plugin_version = "2.0" + plugin_version = "2.1" # 插件作者 plugin_author = "DzAvril" # 作者主页 @@ -128,12 +135,15 @@ class RemoveLink(_PluginBase): _notify = False _delete_scrap_infos = False _delete_torrents = False + _delete_history = False + _transferhistory = None _observer = [] # 监控目录的文件列表 state_set: Dict[str, int] = {} def init_plugin(self, config: dict = None): logger.info(f"Hello, RemoveLink! config {config}") + self._transferhistory = TransferHistoryOper() if config: self._enabled = config.get("enabled") self._notify = config.get("notify") @@ -142,6 +152,7 @@ class RemoveLink(_PluginBase): self.exclude_keywords = config.get("exclude_keywords") or "" self._delete_scrap_infos = config.get("delete_scrap_infos") self._delete_torrents = config.get("delete_torrents") + self._delete_history = config.get("delete_history") # 停止现有任务 self.stop_service() @@ -206,7 +217,7 @@ class RemoveLink(_PluginBase): "content": [ { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VSwitch", @@ -219,7 +230,7 @@ class RemoveLink(_PluginBase): }, { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VSwitch", @@ -237,7 +248,7 @@ class RemoveLink(_PluginBase): "content": [ { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VSwitch", @@ -250,7 +261,7 @@ class RemoveLink(_PluginBase): }, { "component": "VCol", - "props": {"cols": 12, "md": 6}, + "props": {"cols": 12, "md": 4}, "content": [ { "component": "VSwitch", @@ -261,6 +272,19 @@ class RemoveLink(_PluginBase): } ], }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [ + { + "component": "VSwitch", + "props": { + "model": "delete_history", + "label": "删除历史记录", + }, + } + ], + }, ], }, { @@ -441,7 +465,7 @@ class RemoveLink(_PluginBase): # 文件所在目录已被删除则退出 if not os.path.exists(path.parent): return - logger.info(f"清理刮削文件: {path}") + # logger.info(f"清理刮削文件: {path}") if not path.suffix.lower() in [ ".jpg", ".nfo", @@ -455,11 +479,25 @@ class RemoveLink(_PluginBase): # 清理空目录 self.delete_empty_folders(path) + def delete_history(self, path): + """ + 清理path相关的历史记录 + """ + if not self._delete_history: + return + # 查找历史记录 + transfer_history = self._transferhistory.get_by_src(path) + if transfer_history: + # 删除历史记录 + self._transferhistory.delete(transfer_history.id) + logger.info(f"删除历史记录:{transfer_history.id}") + + def delete_empty_folders(self, path): """ 从指定路径开始,逐级向上层目录检测并删除空目录,直到遇到非空目录或到达指定监控目录为止 """ - logger.info(f"清理空目录: {path}") + # logger.info(f"清理空目录: {path}") while True: parent_path = path.parent if self.__is_excluded(parent_path): @@ -505,6 +543,8 @@ class RemoveLink(_PluginBase): eventmanager.send_event( EventType.DownloadFileDeleted, {"src": str(file_path)} ) + # 删除历史记录 + self.delete_history(str(file_path)) # 删除的文件inode deleted_inode = self.state_set.get(str(file_path)) if not deleted_inode: @@ -530,6 +570,8 @@ class RemoveLink(_PluginBase): eventmanager.send_event( EventType.DownloadFileDeleted, {"src": str(file_path)} ) + # 删除历史记录 + self.delete_history(str(file_path)) if self._notify: self.post_message( mtype=NotificationType.SiteMessage, diff --git a/plugins/sitestatistic/__init__.py b/plugins/sitestatistic/__init__.py index 3736a27..99e95ee 100644 --- a/plugins/sitestatistic/__init__.py +++ b/plugins/sitestatistic/__init__.py @@ -43,7 +43,7 @@ class SiteStatistic(_PluginBase): # 插件图标 plugin_icon = "statistic.png" # 插件版本 - plugin_version = "2.9" + plugin_version = "3.4" # 插件作者 plugin_author = "lightolly" # 作者主页 @@ -73,6 +73,7 @@ class SiteStatistic(_PluginBase): _remove_failed: bool = False _statistic_type: str = None _statistic_sites: list = [] + _dashboard_type: str = "today" def init_plugin(self, config: dict = None): self.sites = SitesHelper() @@ -91,6 +92,7 @@ class SiteStatistic(_PluginBase): self._remove_failed = config.get("remove_failed") self._statistic_type = config.get("statistic_type") or "all" self._statistic_sites = config.get("statistic_sites") or [] + self._dashboard_type = config.get("dashboard_type") or "today" # 过滤掉已删除的站点 all_sites = [site.id for site in self.siteoper.list_order_by_pri()] + [site.get("id") for site in @@ -282,7 +284,7 @@ class SiteStatistic(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -299,7 +301,7 @@ class SiteStatistic(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -315,7 +317,7 @@ class SiteStatistic(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 4 + 'md': 3 }, 'content': [ { @@ -330,6 +332,27 @@ class SiteStatistic(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'dashboard_type', + 'label': '仪表板组件', + 'items': [ + {'title': '今日数据', 'value': 'today'}, + {'title': '汇总数据', 'value': 'total'}, + {'title': '所有数据', 'value': 'all'} + ] + } + } + ] } ] }, @@ -401,12 +424,46 @@ class SiteStatistic(_PluginBase): "queue_cnt": 5, "remove_failed": False, "statistic_type": "all", - "statistic_sites": [] + "statistic_sites": [], + "dashboard_type": 'today' } - def get_page(self) -> List[dict]: + def __get_data(self) -> Tuple[str, dict, dict]: """ - 拼装插件详情页面,需要返回页面配置,同时附带数据 + 获取今天的日期、今天的站点数据、昨天的站点数据 + """ + # 最近一天的签到数据 + stattistic_data: Dict[str, Dict[str, Any]] = {} + # 昨天数据 + yesterday_sites_data: Dict[str, Dict[str, Any]] = {} + # 获取最近所有数据 + data_list: List[PluginData] = self.get_data(key=None) + if not data_list: + return "", {}, {} + # 取key符合日期格式的数据 + data_list = [data for data in data_list if re.match(r"\d{4}-\d{2}-\d{2}", data.key)] + # 按日期倒序排序 + data_list.sort(key=lambda x: x.key, reverse=True) + # 今天的日期 + today = data_list[0].key + # 数据按时间降序排序 + datas = [json.loads(data.value) for data in data_list if ObjectUtils.is_obj(data.value)] + if len(data_list) > 0: + stattistic_data = datas[0] + if len(data_list) > 1: + yesterday_sites_data = datas[1] + + # 数据按时间降序排序 + stattistic_data = dict(sorted(stattistic_data.items(), + key=lambda item: item[1].get('upload') or 0, + reverse=True)) + return today, stattistic_data, yesterday_sites_data + + @staticmethod + def __get_total_elements(today: str, stattistic_data: dict, yesterday_sites_data: dict, + dashboard: str = "today") -> List[dict]: + """ + 获取统计元素 """ def __gb(value: int) -> float: @@ -433,13 +490,450 @@ class SiteStatistic(_PluginBase): d[k] = 0 return d - # 最近一天的签到数据 - stattistic_data: Dict[str, Dict[str, Any]] = {} - # 昨天数据 - yesterday_sites_data: Dict[str, Dict[str, Any]] = {} - # 获取最近所有数据 - data_list: List[PluginData] = self.get_data(key=None) - if not data_list: + if dashboard in ['total', 'all']: + # 总上传量 + total_upload = sum([int(data.get("upload")) + for data in stattistic_data.values() if data.get("upload")]) + # 总下载量 + total_download = sum([int(data.get("download")) + for data in stattistic_data.values() if data.get("download")]) + # 总做种数 + total_seed = sum([int(data.get("seeding")) + for data in stattistic_data.values() if data.get("seeding")]) + # 总做种体积 + total_seed_size = sum([int(data.get("seeding_size")) + for data in stattistic_data.values() if data.get("seeding_size")]) + + total_elements = [ + # 总上传量 + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/upload.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总上传量' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': StringUtils.str_filesize(total_upload) + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 总下载量 + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3, + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/download.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总下载量' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': StringUtils.str_filesize(total_download) + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 总做种数 + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/seed.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总做种数' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': f'{"{:,}".format(total_seed)}' + } + ] + } + ] + } + ] + } + ] + }, + ] + }, + # 总做种体积 + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3 + }, + 'content': [ + { + 'component': 'VCard', + 'props': { + 'variant': 'tonal', + }, + 'content': [ + { + 'component': 'VCardText', + 'props': { + 'class': 'd-flex align-center', + }, + 'content': [ + { + 'component': 'VAvatar', + 'props': { + 'rounded': True, + 'variant': 'text', + 'class': 'me-3' + }, + 'content': [ + { + 'component': 'VImg', + 'props': { + 'src': '/plugin_icon/database.png' + } + } + ] + }, + { + 'component': 'div', + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-caption' + }, + 'text': '总做种体积' + }, + { + 'component': 'div', + 'props': { + 'class': 'd-flex align-center flex-wrap' + }, + 'content': [ + { + 'component': 'span', + 'props': { + 'class': 'text-h6' + }, + 'text': StringUtils.str_filesize(total_seed_size) + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + else: + total_elements = [] + + if dashboard in ["today", "all"]: + # 计算增量数据集 + inc_data = {} + for site, data in stattistic_data.items(): + inc = __sub_dict(data, yesterday_sites_data.get(site)) + if inc: + inc_data[site] = inc + # 今日上传 + uploads = {k: v for k, v in inc_data.items() if v.get("upload")} + # 今日上传站点 + upload_sites = [site for site in uploads.keys()] + # 今日上传数据 + upload_datas = [__gb(data.get("upload")) for data in uploads.values()] + # 今日上传总量 + today_upload = round(sum(upload_datas), 2) + # 今日下载 + downloads = {k: v for k, v in inc_data.items() if v.get("download")} + # 今日下载站点 + download_sites = [site for site in downloads.keys()] + # 今日下载数据 + download_datas = [__gb(data.get("download")) for data in downloads.values()] + # 今日下载总量 + today_download = round(sum(download_datas), 2) + # 今日上传下载元素 + today_elements = [ + # 上传量图表 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VApexChart', + 'props': { + 'height': 300, + 'options': { + 'chart': { + 'type': 'pie', + }, + 'labels': upload_sites, + 'title': { + 'text': f'今日上传({today})共 {today_upload} GB' + }, + 'legend': { + 'show': True + }, + 'plotOptions': { + 'pie': { + 'expandOnClick': False + } + }, + 'noData': { + 'text': '暂无数据' + } + }, + 'series': upload_datas + } + } + ] + }, + # 下载量图表 + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VApexChart', + 'props': { + 'height': 300, + 'options': { + 'chart': { + 'type': 'pie', + }, + 'labels': download_sites, + 'title': { + 'text': f'今日下载({today})共 {today_download} GB' + }, + 'legend': { + 'show': True + }, + 'plotOptions': { + 'pie': { + 'expandOnClick': False + } + }, + 'noData': { + 'text': '暂无数据' + } + }, + 'series': download_datas + } + } + ] + } + ] + else: + today_elements = [] + # 合并返回 + return total_elements + today_elements + + def get_dashboard(self) -> Optional[Tuple[Dict[str, Any], Dict[str, Any], List[dict]]]: + """ + 获取插件仪表盘页面,需要返回:1、仪表板col配置字典;2、仪表板页面元素配置json(含数据);3、全局配置(自动刷新等) + 1、col配置参考: + { + "cols": 12, "md": 6 + } + 2、页面配置使用Vuetify组件拼装,参考:https://vuetifyjs.com/ + 3、全局配置参考: + { + "refresh": 10 // 自动刷新时间,单位秒 + } + """ + # 列配置 + cols = { + "cols": 12 + } + # 全局配置 + attrs = {} + # 获取数据 + today, stattistic_data, yesterday_sites_data = self.__get_data() + # 汇总 + # 站点统计 + elements = [ + { + 'component': 'VRow', + 'content': self.__get_total_elements( + today=today, + stattistic_data=stattistic_data, + yesterday_sites_data=yesterday_sites_data, + dashboard=self._dashboard_type + ) + } + ] + return cols, attrs, elements + + def get_page(self) -> List[dict]: + """ + 拼装插件详情页面,需要返回页面配置,同时附带数据 + """ + + # 获取数据 + today, stattistic_data, yesterday_sites_data = self.__get_data() + if not stattistic_data: return [ { 'component': 'div', @@ -449,35 +943,14 @@ class SiteStatistic(_PluginBase): } } ] - # 取key符合日期格式的数据 - data_list = [data for data in data_list if re.match(r"\d{4}-\d{2}-\d{2}", data.key)] - # 按日期倒序排序 - data_list.sort(key=lambda x: x.key, reverse=True) - # 今天的日期 - today = data_list[0].key - # 数据按时间降序排序 - datas = [json.loads(data.value) for data in data_list if ObjectUtils.is_obj(data.value)] - if len(data_list) > 0: - stattistic_data = datas[0] - if len(data_list) > 1: - yesterday_sites_data = datas[1] - # 数据按时间降序排序 - stattistic_data = dict(sorted(stattistic_data.items(), - key=lambda item: item[1].get('upload') or 0, - reverse=True)) - # 总上传量 - total_upload = sum([int(data.get("upload")) - for data in stattistic_data.values() if data.get("upload")]) - # 总下载量 - total_download = sum([int(data.get("download")) - for data in stattistic_data.values() if data.get("download")]) - # 总做种数 - total_seed = sum([int(data.get("seeding")) - for data in stattistic_data.values() if data.get("seeding")]) - # 总做种体积 - total_seed_size = sum([int(data.get("seeding_size")) - for data in stattistic_data.values() if data.get("seeding_size")]) + # 站点统计 + site_totals = self.__get_total_elements( + today=today, + stattistic_data=stattistic_data, + yesterday_sites_data=yesterday_sites_data, + dashboard='all' + ) # 站点数据明细 site_trs = [ @@ -536,388 +1009,11 @@ class SiteStatistic(_PluginBase): } for site, data in stattistic_data.items() if not data.get("err_msg") ] - # 计算增量数据集 - inc_data = {} - for site, data in stattistic_data.items(): - inc = __sub_dict(data, yesterday_sites_data.get(site)) - if inc: - inc_data[site] = inc - # 今日上传 - uploads = {k: v for k, v in inc_data.items() if v.get("upload")} - # 今日上传站点 - upload_sites = [site for site in uploads.keys()] - # 今日上传数据 - upload_datas = [__gb(data.get("upload")) for data in uploads.values()] - # 今日上传总量 - today_upload = round(sum(upload_datas), 2) - # 今日下载 - downloads = {k: v for k, v in inc_data.items() if v.get("download")} - # 今日下载站点 - download_sites = [site for site in downloads.keys()] - # 今日下载数据 - download_datas = [__gb(data.get("download")) for data in downloads.values()] - # 今日下载总量 - today_download = round(sum(download_datas), 2) - # 拼装页面 return [ { 'component': 'VRow', - 'content': [ - # 总上传量 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/upload.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总上传量' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': StringUtils.str_filesize(total_upload) - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 总下载量 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/download.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总下载量' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': StringUtils.str_filesize(total_download) - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 总做种数 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/seed.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总做种数' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': f'{"{:,}".format(total_seed)}' - } - ] - } - ] - } - ] - } - ] - }, - ] - }, - # 总做种体积 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 3, - 'sm': 6 - }, - 'content': [ - { - 'component': 'VCard', - 'props': { - 'variant': 'tonal', - }, - 'content': [ - { - 'component': 'VCardText', - 'props': { - 'class': 'd-flex align-center', - }, - 'content': [ - { - 'component': 'VAvatar', - 'props': { - 'rounded': True, - 'variant': 'text', - 'class': 'me-3' - }, - 'content': [ - { - 'component': 'VImg', - 'props': { - 'src': '/plugin_icon/database.png' - } - } - ] - }, - { - 'component': 'div', - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-caption' - }, - 'text': '总做种体积' - }, - { - 'component': 'div', - 'props': { - 'class': 'd-flex align-center flex-wrap' - }, - 'content': [ - { - 'component': 'span', - 'props': { - 'class': 'text-h6' - }, - 'text': StringUtils.str_filesize(total_seed_size) - } - ] - } - ] - } - ] - } - ] - } - ] - }, - # 上传量图表 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VApexChart', - 'props': { - 'height': 300, - 'options': { - 'chart': { - 'type': 'pie', - }, - 'labels': upload_sites, - 'title': { - 'text': f'今日上传({today})共 {today_upload} GB' - }, - 'legend': { - 'show': True - }, - 'plotOptions': { - 'pie': { - 'expandOnClick': False - } - }, - 'noData': { - 'text': '暂无数据' - } - }, - 'series': upload_datas - } - } - ] - }, - # 下载量图表 - { - 'component': 'VCol', - 'props': { - 'cols': 12, - 'md': 6 - }, - 'content': [ - { - 'component': 'VApexChart', - 'props': { - 'height': 300, - 'options': { - 'chart': { - 'type': 'pie', - }, - 'labels': download_sites, - 'title': { - 'text': f'今日下载({today})共 {today_download} GB' - }, - 'legend': { - 'show': True - }, - 'plotOptions': { - 'pie': { - 'expandOnClick': False - } - }, - 'noData': { - 'text': '暂无数据' - } - }, - 'series': download_datas - } - } - ] - }, + 'content': site_totals + [ # 各站点数据明细 { 'component': 'VCol', @@ -1037,10 +1133,12 @@ class SiteStatistic(_PluginBase): """ 构建站点信息 """ - site_cookie = site_info.get("cookie") - if not site_cookie: - return None site_name = site_info.get("name") + site_cookie = site_info.get("cookie") + apikey = site_info.get("apikey") + token = site_info.get("token") + if not site_cookie and not apikey and not token: + return None url = site_info.get("url") proxy = site_info.get("proxy") ua = site_info.get("ua") @@ -1049,8 +1147,7 @@ class SiteStatistic(_PluginBase): proxies = settings.PROXY if proxy else None proxy_server = settings.PROXY_SERVER if proxy else None render = site_info.get("render") - - logger.debug(f"站点 {site_name} url={url} site_cookie={site_cookie} ua={ua}") + logger.debug(f"站点 {site_name} url={url},site_cookie={site_cookie},ua={ua},api_key={apikey},token={token},proxy={proxy}") if render: # 演染模式 html_text = PlaywrightHelper().get_page_source(url=url, @@ -1093,9 +1190,12 @@ class SiteStatistic(_PluginBase): html_text = res.text if not html_text: return None - else: + elif res is not None: logger.error("站点 %s 被反爬限制:%s, 状态码:%s" % (site_name, url, res.status_code)) return None + else: + logger.error("站点 %s 无法访问:%s" % (site_name, url)) + return None # 兼容假首页情况,假首页通常没有 schemas.Response: @@ -1351,6 +1460,7 @@ class SiteStatistic(_PluginBase): "remove_failed": self._remove_failed, "statistic_type": self._statistic_type, "statistic_sites": self._statistic_sites, + "dashboard_type": self._dashboard_type }) @eventmanager.register(EventType.SiteDeleted) diff --git a/plugins/sitestatistic/siteuserinfo/__init__.py b/plugins/sitestatistic/siteuserinfo/__init__.py index e98a1e2..7be0b61 100644 --- a/plugins/sitestatistic/siteuserinfo/__init__.py +++ b/plugins/sitestatistic/siteuserinfo/__init__.py @@ -26,6 +26,7 @@ class SiteSchema(Enum): NexusProject = "NexusProject" NexusRabbit = "NexusRabbit" NexusHhanclub = "NexusHhanclub" + NexusAudiences = "NexusAudiences" SmallHorse = "Small Horse" Unit3d = "Unit3d" TorrentLeech = "TorrentLeech" @@ -45,6 +46,8 @@ class ISiteUserInfo(metaclass=ABCMeta): def __init__(self, site_name: str, url: str, site_cookie: str, + apikey: str, + token: str, index_html: str, session: Session = None, ua: str = None, @@ -54,6 +57,8 @@ class ISiteUserInfo(metaclass=ABCMeta): # 站点信息 self.site_name = None self.site_url = None + self.apikey = apikey + self.token = token # 用户信息 self.username = None self.userid = None diff --git a/plugins/sitestatistic/siteuserinfo/file_list.py b/plugins/sitestatistic/siteuserinfo/file_list.py index 611a4dd..9bf6f31 100644 --- a/plugins/sitestatistic/siteuserinfo/file_list.py +++ b/plugins/sitestatistic/siteuserinfo/file_list.py @@ -58,7 +58,15 @@ class FileListSiteUserInfo(ISiteUserInfo): if download_html: self.download = StringUtils.num_filesize(download_html[0]) - self.ratio = 0 if self.download == 0 else self.upload / self.download + ratio_html = html.xpath('//table//tr/td[text()="Share ratio"]/following-sibling::td//text()') + if ratio_html: + share_ratio = StringUtils.str_float(ratio_html[0]) + self.ratio = 0 if self.download == 0 else share_ratio + + seed_html = html.xpath('//table//tr/td[text()="Seed bonus"]/following-sibling::td//text()') + if seed_html: + self.seeding = StringUtils.str_int(seed_html[1]) + self.seeding_size = StringUtils.num_filesize(seed_html[3]) user_level_html = html.xpath('//table//tr/td[text()="Class"]/following-sibling::td//text()') if user_level_html: @@ -66,7 +74,8 @@ class FileListSiteUserInfo(ISiteUserInfo): join_at_html = html.xpath('//table//tr/td[contains(text(), "Join")]/following-sibling::td//text()') if join_at_html: - self.join_at = StringUtils.unify_datetime_str(join_at_html[0].strip()) + join_at = (join_at_html[0].split("("))[0].strip() + self.join_at = StringUtils.unify_datetime_str(join_at) bonus_html = html.xpath('//a[contains(@href, "shop.php")]') if bonus_html: @@ -102,8 +111,8 @@ class FileListSiteUserInfo(ISiteUserInfo): page_seeding_size += size page_seeding_info.append([seeders, size]) - self.seeding += page_seeding - self.seeding_size += page_seeding_size + # self.seeding += page_seeding + # self.seeding_size += page_seeding_size self.seeding_info.extend(page_seeding_info) # 是否存在下页数据 diff --git a/plugins/sitestatistic/siteuserinfo/mtorrent.py b/plugins/sitestatistic/siteuserinfo/mtorrent.py index 4881418..6dda16d 100644 --- a/plugins/sitestatistic/siteuserinfo/mtorrent.py +++ b/plugins/sitestatistic/siteuserinfo/mtorrent.py @@ -65,13 +65,12 @@ class MTorrentSiteUserInfo(ISiteUserInfo): "pageSize": 100 } self._torrent_seeding_page = "api/member/getUserTorrentList" - domain = StringUtils.get_url_host(self.site_url) self._torrent_seeding_headers = { "Content-Type": "application/json", "Accept": "application/json, text/plain, */*" } self._addition_headers = { - "x-api-key": SystemConfigOper().get(f"site.{domain}.apikey"), + "x-api-key": self.apikey, } def _parse_logged_in(self, html_text): diff --git a/plugins/sitestatistic/siteuserinfo/nexus_audiences.py b/plugins/sitestatistic/siteuserinfo/nexus_audiences.py new file mode 100644 index 0000000..9a1c369 --- /dev/null +++ b/plugins/sitestatistic/siteuserinfo/nexus_audiences.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from urllib.parse import urljoin + +from app.plugins.sitestatistic.siteuserinfo import SITE_BASE_ORDER, SiteSchema +from app.plugins.sitestatistic.siteuserinfo.nexus_php import NexusPhpSiteUserInfo + + +class NexusAudiencesSiteUserInfo(NexusPhpSiteUserInfo): + schema = SiteSchema.NexusAudiences + order = SITE_BASE_ORDER + 5 + + @classmethod + def match(cls, html_text: str) -> bool: + return 'audiences.me' in html_text + + def _parse_seeding_pages(self): + self._torrent_seeding_headers = {"Referer": urljoin(self._base_url, self._user_detail_page)} + super()._parse_seeding_pages() diff --git a/plugins/tmdbwallpaper/__init__.py b/plugins/tmdbwallpaper/__init__.py new file mode 100644 index 0000000..28dc6eb --- /dev/null +++ b/plugins/tmdbwallpaper/__init__.py @@ -0,0 +1,243 @@ +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, List, Dict, Tuple + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler + +from app.chain.tmdb import TmdbChain +from app.core.config import settings +from app.log import logger +from app.plugins import _PluginBase +from app.utils.http import RequestUtils +from app.utils.web import WebUtils + + +class TmdbWallpaper(_PluginBase): + # 插件名称 + plugin_name = "登录壁纸本地化" + # 插件描述 + plugin_desc = "将MoviePilot的登录壁纸下载到本地。" + # 插件图标 + plugin_icon = "Macos_Sierra.png" + # 插件版本 + plugin_version = "1.1" + # 插件作者 + plugin_author = "jxxghp" + # 作者主页 + author_url = "https://github.com/jxxghp" + # 插件配置项ID前缀 + plugin_config_prefix = "tmdbwallpaper_" + # 加载顺序 + plugin_order = 99 + # 可使用的用户级别 + auth_level = 1 + + # 私有属性 + _hours = None + _savepath = None + _enabled = False + _onlyonce = False + _scheduler = None + + def init_plugin(self, config: dict = None): + if config: + self._enabled = config.get("enabled") + self._hours = int(config.get("hours")) if config.get("hours") else None + self._savepath = config.get('savepath') + self._onlyonce = config.get("onlyonce") + if self._enabled or self._onlyonce: + savepath = Path(self._savepath) + if self._savepath and not savepath.exists(): + logger.info(f"创建保存目录:{self._savepath}") + savepath.mkdir(parents=True, exist_ok=True) + # 立即运行一次 + if self._onlyonce: + # 定时服务 + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + logger.info(f"登录壁纸本地化服务启动,立即运行一次") + self._scheduler.add_job(self.wallpaper_local, 'date', + run_date=datetime.now( + tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3) + ) + # 关闭一次性开关 + self._onlyonce = False + + # 保存配置 + self.update_config({ + "enabled": self._enabled, + "hours": self._hours, + "savepath": self._savepath, + "onlyonce": self._onlyonce + }) + if self._scheduler.get_jobs(): + # 启动服务 + self._scheduler.print_jobs() + self._scheduler.start() + + def get_state(self) -> bool: + return True if self._enabled and self._hours and self._savepath else False + + @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]]: + """ + 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 + """ + return [ + { + 'component': 'VForm', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'enabled', + 'label': '启用插件', + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 4 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'hours', + 'label': '更新频率(小时)', + 'placeholder': '1' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 8 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'savepath', + 'label': '保存路径', + 'placeholder': '/config/wallpapers' + } + } + ] + } + ] + }, + ] + } + ], { + "enabled": False, + "hours": 1, + "savepath": "/config/wallpapers" + } + + def get_page(self) -> List[dict]: + pass + + def get_service(self) -> List[Dict[str, Any]]: + """ + 注册插件公共服务 + [{ + "id": "服务ID", + "name": "服务名称", + "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", + "func": self.xxx, + "kwargs": {} # 定时器参数 + }] + """ + if self.get_state(): + return [{ + "id": "TmdbWallpaper", + "name": "登录壁纸本地化服务", + "trigger": "interval", + "func": self.wallpaper_local, + "kwargs": { + "minutes": self._hours * 60 + } + }] + return [] + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + print(str(e)) + + def wallpaper_local(self): + """ + 下载MoviePilot的登录壁纸到本地 + """ + if not self._savepath: + return + if settings.WALLPAPER == "tmdb": + url = TmdbChain().get_random_wallpager() + filename = url.split("/")[-1] + else: + url = WebUtils.get_bing_wallpaper() + filename = f"{datetime.now().strftime('%Y%m%d')}.jpg" + # 下载壁纸 + if url: + try: + savepath = Path(self._savepath) + logger.info(f"下载壁纸:{url}") + with RequestUtils().get_res(url) as r: + if r and r.status_code == 200: + with open(savepath / filename, "wb") as f: + f.write(r.content) + except Exception as e: + logger.error(f"下载壁纸失败:{str(e)}") + else: + logger.error(f"获取壁纸地址失败") diff --git a/plugins/vcbanimemonitor/__init__.py b/plugins/vcbanimemonitor/__init__.py index a6883d0..56eb939 100644 --- a/plugins/vcbanimemonitor/__init__.py +++ b/plugins/vcbanimemonitor/__init__.py @@ -5,8 +5,8 @@ import threading import time import traceback from pathlib import Path +from time import sleep from typing import List, Tuple, Dict, Any, Optional - import pytz import qbittorrentapi from apscheduler.schedulers.background import BackgroundScheduler @@ -14,12 +14,13 @@ from apscheduler.triggers.cron import CronTrigger from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer from watchdog.observers.polling import PollingObserver - from app import schemas from app.chain.tmdb import TmdbChain from app.chain.transfer import TransferChain from app.core.config import settings from app.core.context import MediaInfo +from app.core.event import eventmanager, Event +from app.core.metainfo import MetaInfoPath from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.transferhistory_oper import TransferHistoryOper from app.log import logger @@ -76,7 +77,7 @@ class VCBAnimeMonitor(_PluginBase): # 插件图标 plugin_icon = "vcbmonitor.png" # 插件版本 - plugin_version = "1.7" + plugin_version = "1.7.1" # 插件作者 plugin_author = "pixel@qingwa" # 作者主页 @@ -155,15 +156,20 @@ class VCBAnimeMonitor(_PluginBase): self._scheduler.add_job(self.send_msg, trigger='interval', seconds=15) self.qb = Qbittorrent() + # 读取目录配置 + monitor_dirs = self._monitor_dirs.split("\n") + if not monitor_dirs: + return + # 启用种子目录监控 if self._torrents_path is not None and Path(self._torrents_path).exists() and self._enabled: # 只取第一个目录作为新的保存 + first_path = monitor_dirs[0] if SystemUtils.is_windows(): - self.new_save_path = self._monitor_dirs.split(':')[0] + ":" + self._monitor_dirs.split(':')[1] + self.new_save_path = first_path.split(':')[0] + ":" + first_path.split(':')[1] else: - self.new_save_path = self._monitor_dirs.split(':')[0] - print(self.new_save_path) - + self.new_save_path = first_path.split(':')[0] + # print(self.new_save_path) try: observer = Observer() self._observer.append(observer) @@ -177,10 +183,6 @@ class VCBAnimeMonitor(_PluginBase): else: logger.info("种子目录为空,不转移qb中正在下载的VCB-Studio文件") - # 读取目录配置 - monitor_dirs = self._monitor_dirs.split("\n") - if not monitor_dirs: - return for mon_path in monitor_dirs: # 格式源目录:目的目录 if not mon_path: @@ -594,6 +596,7 @@ class VCBAnimeMonitor(_PluginBase): def torrent_event(self, event, mon_path: str, text: str): """ 处理种子文件 + :param mon_path: 种子目录 """ evc_path = Path(event.src_path) if not event.is_directory and (evc_path.suffix == ".torrent" or str(evc_path).split('.')[1] == "torrent"): @@ -1009,8 +1012,7 @@ class VCBAnimeMonitor(_PluginBase): 'props': { 'type': 'info', 'variant': 'tonal', - 'text': '核心用法与目录同步插件相同,不同点在于只识别处理VCB-Studio资源,' - '避免与目录同步插件的监控目录相同(否则会同时进行识别)' + 'text': '核心用法与目录同步插件相同,不同点在于只识别处理VCB-Studio资源,\n' '不处理SPs目录下的文件,OVA/OAD集数根据入库顺序累加命名,不保证与TMDB集数匹配' } } diff --git a/plugins/vcbanimemonitor/remeta.py b/plugins/vcbanimemonitor/remeta.py index ea931b0..4624cc7 100644 --- a/plugins/vcbanimemonitor/remeta.py +++ b/plugins/vcbanimemonitor/remeta.py @@ -2,9 +2,6 @@ import concurrent import re from pathlib import Path from typing import List - -import roman - from app.chain.media import MediaChain from app.chain.tmdb import TmdbChain from app.core.metainfo import MetaInfoPath @@ -12,6 +9,26 @@ from app.log import logger from app.schemas import MediaType +def roman_to_int(s) -> int: + """ + :param s: 罗马数字字符串 + 罗马数字转整数 + """ + roman_dict = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} + total = 0 + prev_value = 0 + + for char in reversed(s): # 反向遍历罗马数字字符串 + current_value = roman_dict[char] + if current_value >= prev_value: + total += current_value # 如果当前值大于等于前一个值,加上当前值 + else: + total -= current_value # 如果当前值小于前一个值,减去当前值 + prev_value = current_value + + return total + + class ReMeta: # 解析之后的标题: title: str = None @@ -41,7 +58,7 @@ class ReMeta: ] _ova_patterns = [re.compile(r"\[.*?(OVA|OAD).*?]"), re.compile(r"\[\d+\.5]"), - re.compile(r"\[00]")] + re.compile(r"\[00\]")] final_season_patterns = [re.compile('final season', re.IGNORECASE), re.compile('The Final', re.IGNORECASE), @@ -57,13 +74,14 @@ class ReMeta: def handel_file(self, file_path: Path): meta = MetaInfoPath(file_path) self.title = meta.title + self.title = Path(self.title).stem.strip() if 'VCB-Studio' not in meta.title: logger.warn("不属于VCB的作品,不处理!") return None if meta.title.count("[") != 4 and meta.title.count("]") != 4: # 可能是电影,电影只有三组[],因此去除所有[]后只剩下电影名 logger.warn("不符合VCB-Studio的剧集命名规范,跳过剧集模块处理!交给默认处理逻辑") - meta.title = re.sub(r'\[.*?]', '', meta.title).strip() + meta.title = re.sub(r'\[.*?\]', '', meta.title).strip() meta.en_name = meta.title return meta split_title: List[str] | None = self.split_season_ep(self.title) @@ -84,13 +102,12 @@ class ReMeta: return meta # 分离季度部分和集数部分 - @staticmethod - def split_season_ep(pre_title: str): + def split_season_ep(self, pre_title: str): split_ep = re.findall(r"(\[.*?])", pre_title)[1] if not split_ep: logger.warn("未识别出集数位置信息,结束识别!") return None - split_title = re.sub(r"\[.*?]", "", pre_title).strip() + split_title = re.sub(r"\[.*?\]", "", pre_title).strip() logger.info(f"分离出包含季度的部分:{split_title} \n 分离出包含集数的部分: {split_ep}") return [split_title, split_ep] @@ -126,7 +143,7 @@ class ReMeta: match = pattern.search(pre_title) if match: if type(group) == str: - title_season["season"] = int(roman.fromRoman(match.group(int(group)))) + title_season["season"] = roman_to_int(match.group(int(group))) title_season["title"] = re.sub(pattern, "", pre_title).strip() else: title_season["season"] = int(match.group(group)) @@ -142,8 +159,7 @@ class ReMeta: return title_season # 处理存在“Final”字样命名的季度 - @staticmethod - def handle_final_season(title: str) -> int | None: + def handle_final_season(self, title: str) -> int | None: medias = MediaChain().search(title=title)[1] if not medias: logger.warn("没有找到对应的媒体信息!") diff --git a/plugins/vcbanimemonitor/requirements.txt b/plugins/vcbanimemonitor/requirements.txt deleted file mode 100644 index 0eed2d7..0000000 --- a/plugins/vcbanimemonitor/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -roman~=4.1