Merge branch 'jxxghp_main'

* jxxghp_main: (42 commits)
  fix README
  更新文档
  优化了表单界面和一些逻辑。
  Update removelink to v2.1
  更新 package.json
  fix MPServerStatus
  fix MPServerStatus
  add MPServerStatus
  Update contractcheck to v1.4
  fix
  修复FileList做种数和做种体积错误的bug
  fix https://github.com/jxxghp/MoviePilot/issues/1991
  fix https://github.com/jxxghp/MoviePilot/issues/2045
  fixbug
  修正数字配置值提交为字符串导致的问题。
  【插件自动升级】插件优化
  SiteStatistic 支持选择仪表板组件规格
  Update contractcheck to v1.3
  Update README.md
  fix README
  ...

# Conflicts:
#	package.json
This commit is contained in:
杨玲辉
2024-05-14 00:19:48 +08:00
23 changed files with 3184 additions and 1157 deletions

View File

@@ -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`中的末尾,这样可被识别为最新增加,可用于用户排序。

View File

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

View File

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

View File

@@ -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
# 兼容假首页情况,假首页通常没有 <link rel="search" 属性
if '"search"' not in html_text and '"csrf-token"' not in html_text:
@@ -710,7 +874,7 @@ class ContractCheck(_PluginBase):
else:
num_gap = contract_info.num - current_seeding_size[0]
is_satisfied = is_size_satisfied and is_num_satisfied
duration = (datetime.now().date() - contract_info.date).days
duration = (datetime.now() - contract_info.date).days
if duration < contract_info.duration:
duration_gap = contract_info.duration - duration
return is_satisfied, size_gap, num_gap, duration_gap
@@ -759,7 +923,7 @@ class ContractCheck(_PluginBase):
),
"contract_num": contract_info.num,
"contract_duration": contract_info.duration,
"contract_start_on": contract_info.date,
"contract_start_on": str(contract_info.date),
"total_seed_num": site_user_info.total_seeding_size[0],
"total_seed_size": StringUtils.str_filesize(
site_user_info.total_seeding_size[1]
@@ -835,6 +999,9 @@ class ContractCheck(_PluginBase):
with ThreadPool(min(len(refresh_sites), int(self._queue_cnt or 5))) as p:
p.map(self.__refresh_site_data, refresh_sites)
# 保存数据
self.save_data("contractcheck", self._sites_data)
# 通知刷新完成
if self._notify:
notify_message = ""
@@ -882,5 +1049,6 @@ class ContractCheck(_PluginBase):
"notify": self._notify,
"queue_cnt": self._queue_cnt,
"contract_infos": self._contract_infos,
"dashboard_type": self._dashboard_type,
}
)

View File

@@ -69,6 +69,7 @@ class ISiteUserInfo(metaclass=ABCMeta):
"UBits": ["UBits"],
"听听歌": ["TTG", "WiKi", "DoA", "NGB", "ARiN"],
"馒头": ["MTeam", "MTeamTV"],
"朋友": ["FRDS"],
}
# 错误信息
@@ -201,6 +202,10 @@ class ISiteUserInfo(metaclass=ABCMeta):
self._torrent_seeding_page = self._user_detail_page
elif self.site_name == "馒头":
self._torrent_seeding_page = f"getusertorrentlist.php?userid={self.userid}&type=seeding"
elif self.site_name == "观众":
self._torrent_seeding_headers = {"Referer": urljoin(self._base_url, self._user_detail_page)}
logger.info(f" {self.site_name} {self._torrent_seeding_headers}")
# 第一页
next_page = self._parse_user_torrent_seeding_info(
self._get_page_content(urljoin(self._base_url, self._torrent_seeding_page),

View File

@@ -43,6 +43,7 @@
|定时执行周期|插件定时服务的cron表达式仅支持5位的缺省时不注册定时服务。|
|排除种子标签|多个标签通过英文逗号分割,具备配置的任意标签的种子不会进行自动做种、站点标签、自动删种操作。|
|站点标签前缀|站点标签的前缀,缺省时不添加前缀。|
|配置Tracker映射|该开关无实际业务意义仅用于触发展开配置Tracker映射窗口。|
|Tracker映射|站点标签的原理是根据tracker的域名去匹配站点但是有的PT站的tracker域名和站点域名不一致导致匹配不到站点因此需要对这些特殊站点的tracker做映射每行一个映射格式是 `tracker域名:站点域名`tracker域名可以是完整域名或者主域名。|
##### 2.1.2、下载器子任务配置项

View File

@@ -19,7 +19,7 @@ from app.log import logger
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.modules.transmission.transmission import Transmission
from app.plugins import _PluginBase
from app.plugins.downloaderhelper.module import TaskContext, TaskResult
from app.plugins.downloaderhelper.module import TaskContext, TaskResult, Downloader
from app.schemas.types import EventType
from app.utils.string import StringUtils
@@ -32,7 +32,7 @@ class DownloaderHelper(_PluginBase):
# 插件图标
plugin_icon = "DownloaderHelper.png"
# 插件版本
plugin_version = "1.6"
plugin_version = "1.7"
# 插件作者
plugin_author = "hotlcc"
# 作者主页
@@ -168,7 +168,82 @@ class DownloaderHelper(_PluginBase):
}
# 合并默认配置
config_suggest.update(self.__config_default)
# 下载器tabs
downloader_tabs = [{
'component': 'VTab',
'props': {
'value': d.id
},
'text': d.name_
} for d in Downloader if d]
# 下载器tab items
downloader_tab_items = [{
'component': 'VWindowItem',
'props': {
'value': d.id
},
'content': [{
'component': 'VRow',
'content': [{
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': f'{d.short_id}_enable',
'label': '任务开关',
'hint': '该下载器子任务的开关'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': f'{d.short_id}_enable_seeding',
'label': '自动做种',
'hint': '是否开启自动做种功能'
}
}]
}, {
'component': 'VCol',
'props': {
'cols': 12,
'xxl': 3, 'xl': 3, 'lg': 3, 'md': 3, 'sm': 6, 'xs': 12
},
'content': [{
'component': 'VSwitch',
'props': {
'model': f'{d.short_id}_enable_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': f'{d.short_id}_enable_delete',
'label': '自动删种',
'hint': '是否开启自动删种功能'
}
}]
}]
}]
} for d in Downloader if d]
# 返回form
return [{
'component': 'VForm',
'content': [{ # 业务无关总控
@@ -313,174 +388,83 @@ class DownloaderHelper(_PluginBase):
'content': [{
'component': 'VCol',
'props': {
'cols': 12
'cols': 12,
'xxl': 4, 'xl': 4, 'lg': 4, 'md': 4, 'sm': 6, 'xs': 12
},
'content': [{
'component': 'VTextarea',
'component': 'VSwitch',
'props': {
'model': 'tracker_mappings',
'label': 'Tracker映射',
'placeholder': '格式:\n'
'<tracker-domain>:<site-domain>\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'
'<tracker-domain>:<site-domain>\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
}]
}]
}, {

View File

@@ -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):
"""

View File

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

View File

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

View File

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

View File

@@ -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]
# 按时间倒序

View File

@@ -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": [

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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)
# 是否存在下页数据

View File

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

View File

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

View File

@@ -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"获取壁纸地址失败")

View File

@@ -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集数匹配'
}
}

View File

@@ -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("没有找到对应的媒体信息!")

View File

@@ -1 +0,0 @@
roman~=4.1