Compare commits

...

203 Commits

Author SHA1 Message Date
jxxghp
b1db95a925 v2.4.4 2025-05-07 08:26:06 +08:00
jxxghp
9dac9850b6 fix plugin file api 2025-05-06 23:56:35 +08:00
jxxghp
abe091254a fix plugin file api 2025-05-06 23:30:26 +08:00
jxxghp
d2e5367dc6 fix plugins 2025-05-06 11:44:23 +08:00
jxxghp
8ccd1f5fe4 Merge pull request #4229 from wikrin/v2 2025-05-06 06:34:16 +08:00
Attente
50bc865dd2 fix(database): improve message template
- Fix syntax error in downloadAdded message template
2025-05-05 23:14:58 +08:00
jxxghp
74a6ee7066 fix 2025-05-05 19:50:15 +08:00
jxxghp
89e76bcb48 fix 2025-05-05 19:49:30 +08:00
jxxghp
c55f6baf67 Merge pull request #4228 from wikrin/format_notification
Format notification
2025-05-05 19:28:44 +08:00
Attente
ae154489e1 上下文构建并非复杂任务, 移除缓存 2025-05-05 14:08:41 +08:00
Attente
fdc79033ce Merge https://github.com/jxxghp/MoviePilot into format_notification 2025-05-05 13:21:58 +08:00
jxxghp
9a8aa5e632 更新 subscribe.py 2025-05-05 13:16:14 +08:00
Attente
6b81f3ce5f feat(template):实现缓存机制以提升性能
- 在 `TemplateHelper` 和 `TemplateContextBuilder` 中集成 TTLCache(带过期时间的缓存),提升数据复用能力
- 引入 `build_context_cache` 装饰器,统一管理上下文构建的缓存逻辑
对媒体信息、剧集详情、种子信息、传输信息及原始对象启用缓存,减少重复计算
- 新增上下文缓存支持,为异步广播事件 NoticeMessage 提供所需上下文(可通过消息 title 与 text 内容重新获取上下文)
- 支持插件通过自定义模板灵活重构消息体,提升扩展性与灵活性
2025-05-05 13:14:45 +08:00
Attente
aeaddfe36b feat(database): add notification templates for version 2.1.4
- Add new Alembic migration script for version 2.1.4
- Implement notification templates for various events:
  - Organize success
  - Download added
  - Subscribe added
  - Subscribe complete
- Store notification templates in system configuration
2025-05-05 05:27:59 +08:00
Attente
20c1f30877 feat(message): 实现自定义消息模板功能
- 新增 MessageTemplateHelper 类用于渲染消息模板
- 在 ChainBase 中集成消息模板渲染功能
- 修改 DownloadChain、SubscribeChain 和 TransferChain 以使用新消息模板
- 新增 TemplateHelper 类用于处理模板格式
- 在 SystemConfigKey 中添加 NotificationTemplates 配置项
- 更新 Notification 模型以支持 ctype 字段
2025-05-05 05:27:48 +08:00
jxxghp
52ce6ff38e fix plugin file api 2025-05-03 22:14:39 +08:00
jxxghp
c692a3c80e feat:支持vue原生插件页面 2025-05-03 10:03:44 +08:00
jxxghp
491009636a fix bug 2025-05-02 22:57:29 +08:00
jxxghp
ed16ee14ea fix bug 2025-05-02 21:57:19 +08:00
jxxghp
7f2ed09267 fix storage 2025-05-02 20:49:38 +08:00
jxxghp
c0976897ef fix bug 2025-05-02 13:30:39 +08:00
jxxghp
85b55aa924 fix bug 2025-05-02 08:31:38 +08:00
jxxghp
91d0f76783 feat:支持新增存储类型 2025-05-02 08:11:48 +08:00
jxxghp
741badf9e6 feat:支持文件整理存储操作事件 2025-05-01 21:16:21 +08:00
jxxghp
ca1f3ac377 feat:文件整理支持操作类入参 2025-05-01 20:56:17 +08:00
jxxghp
e13e1c9ca3 fix run_module 2025-05-01 11:36:43 +08:00
jxxghp
06ad042443 fix typo 2025-05-01 11:20:56 +08:00
jxxghp
9d333b855c feat:支持插件协持系统模块实现 2025-05-01 11:03:28 +08:00
jxxghp
f46e2acd56 v2.4.3
- 用户界面支持多语言
- 支持设定TheMovieDb元数据语言
- 订阅成功消息增加了演员和简介
- 修复问题

提醒:如升级后页面空白,请强制刷新或者清理浏览器缓存
2025-04-29 17:32:40 +08:00
jxxghp
5ac4d3f4ae fix wallpaper api 2025-04-29 15:26:10 +08:00
jxxghp
1614eebc47 fix 2025-04-29 14:53:04 +08:00
jxxghp
b50599b71f fix:增加安全性 2025-04-29 14:30:34 +08:00
jxxghp
0459025bf8 Merge pull request #4207 from monster-fire/v2 2025-04-28 19:37:52 +08:00
monster-fire
0bd37da8c7 Update __init__.py 添加空值检查 2025-04-28 18:46:48 +08:00
jxxghp
da969dde53 fix:TMDB支持设置语种 2025-04-28 12:11:48 +08:00
jxxghp
33fdd6cafa feat:TMDB支持设置语种 2025-04-28 09:10:38 +08:00
jxxghp
2fe68766eb Merge remote-tracking branch 'origin/v2' into v2 2025-04-28 09:07:42 +08:00
jxxghp
205348697c fix #4188 2025-04-27 12:26:49 +08:00
jxxghp
9b3533c1da Merge pull request #4199 from cddjr/fix_bing 2025-04-27 06:53:00 +08:00
景大侠
c3584e838e fix: 开启全局图片缓存后无法显示来自Bing的壁纸 2025-04-27 00:17:29 +08:00
jxxghp
16d8b3fb58 Merge pull request #4187 from thsrite/v2 2025-04-23 11:53:29 +08:00
thsrite
686bbdc16b fix 添加订阅成功消息增加演员名称、简介 2025-04-23 11:44:44 +08:00
jxxghp
65b17e4f2b v2.4.2
- 修复普通用户通过媒体卡片跳转搜索时无法选择站点的问题,普通用户不能修改搜索站点,会按管理员预设站点直接搜索
2025-04-22 17:35:30 +08:00
jxxghp
23c6898789 更新 nginx.template.conf 2025-04-21 21:42:12 +08:00
jxxghp
df2a1be2a2 更新 nginx.template.conf 2025-04-21 21:33:00 +08:00
jxxghp
2db628a2ba v2.4.1
本版本更新主要调整了用户界面:
- 新增透明主题风格
- PWA模式下全新设计了底部导航栏
- 优化了多处UI细节
2025-04-21 20:05:53 +08:00
jxxghp
b6c40436c9 Merge pull request #4165 from wikrin/v2 2025-04-19 22:36:48 +08:00
Attente
a8a70cac08 refactor(db): optimize download history query logic
- 使用`TransferHistory.list_by`相同逻辑
2025-04-19 20:22:37 +08:00
jxxghp
3eefbf97b1 更新 plex.py 2025-04-19 15:14:47 +08:00
jxxghp
3c423e0838 更新 jellyfin.py 2025-04-19 15:14:14 +08:00
jxxghp
99cde43954 更新 emby.py 2025-04-19 15:13:33 +08:00
jxxghp
fa3a787bf7 更新 mediaserver.py 2025-04-19 15:12:42 +08:00
jxxghp
c776dc8036 feat: WebhookMessage.json 2025-04-19 07:59:59 +08:00
jxxghp
1ef068351d fix docker 2025-04-17 19:36:54 +08:00
jxxghp
6abe0a1862 fix version 2025-04-17 19:15:18 +08:00
jxxghp
ff13045f52 fix build 2025-04-17 12:44:22 +08:00
jxxghp
59c09681cb fix build 2025-04-17 11:49:07 +08:00
jxxghp
f664cf6fa5 remove built-lite 2025-04-17 11:47:24 +08:00
jxxghp
01a847a9c2 test beta 2025-04-17 11:43:42 +08:00
jxxghp
6da655f67f Merge pull request #4154 from TimoYoung/v2 2025-04-16 12:41:15 +08:00
TimoYoung
21df7dced1 fix: 同步cookiecloud站点执行失败问题 2025-04-16 10:26:43 +08:00
jxxghp
7fc257ea79 v2.4.0 2025-04-16 08:11:31 +08:00
jxxghp
24f170ff72 fix 搜索缓存 2025-04-16 08:10:48 +08:00
jxxghp
39999c9ee4 更新 Dockerfile 2025-04-15 06:54:11 +08:00
jxxghp
27a5188e4e 更新 Dockerfile.lite 2025-04-15 06:52:53 +08:00
jxxghp
a5af0786aa - 修复UI错误 2025-04-13 16:03:40 +08:00
jxxghp
e9c9cfaa72 Merge pull request #4137 from lddsb/patch-1 2025-04-11 16:06:29 +08:00
Dee Luo
8ca4ea0f3f perf: 优化qb下载器端口获取逻辑 2025-04-11 15:43:40 +08:00
jxxghp
86e1f9a9d6 Merge pull request #4136 from lddsb/patch-3 2025-04-11 11:43:26 +08:00
Dee Luo
b36ceda585 fix: Rename groups to groups.py 2025-04-11 11:22:29 +08:00
Dee Luo
27a3e6c6db feat: 增加制作组的单元测试 2025-04-11 11:21:39 +08:00
Dee Luo
a731327c00 feat: 增加制作组的单元测试cases 2025-04-11 11:20:36 +08:00
Dee Luo
737c00978e perf: 优化制作组匹配逻辑,解决部分Web组匹配不到的问题
增加两个站制作组的匹配规则
2025-04-11 11:18:15 +08:00
jxxghp
18bcb3a067 fix #4118 2025-04-10 19:40:22 +08:00
jxxghp
f49f55576f Merge pull request #4128 from lddsb/patch-2 2025-04-10 11:09:12 +08:00
Dee Luo
1bef4f9a4d perf: 优化制作组读取自定义制作组的逻辑,避免被空字符串的list影响最终结果 2025-04-10 11:00:46 +08:00
Dee Luo
ab1df59f7a fix: 修复前端传递了[""]这样的空list导致判空时逻辑异常的问题 2025-04-10 10:51:40 +08:00
jxxghp
bcd235521e v2.3.9
- 优化多处UI细节
- 修复了订阅分享参数传递问题,开放了订阅分享管理功能
2025-04-10 08:34:16 +08:00
jxxghp
31a2eac302 fix:订阅分享参数传递 2025-04-10 08:19:59 +08:00
jxxghp
7e6b7e5dd5 更新 subscribe.py 2025-04-09 17:32:07 +08:00
jxxghp
9ec9f48425 feat:增加订阅管理员 #4123 2025-04-09 13:26:58 +08:00
jxxghp
a3bec43eab feat:增加订阅管理员 #4123 2025-04-09 13:26:10 +08:00
jxxghp
f429b6397e fix RecommendMediaSource 2025-04-08 18:52:54 +08:00
jxxghp
9d6e7dc288 Merge pull request #4115 from lddsb/patch-1 2025-04-08 17:58:36 +08:00
Dee Luo
a27c09c1e8 perf: 放宽制作组后缀匹配
支持 制作组xxx 这样的后缀匹配
2025-04-08 16:35:38 +08:00
jxxghp
ceb0697c73 - 适配馒头API变动 2025-04-07 21:30:41 +08:00
jxxghp
6ad6a08bf1 Merge pull request #4110 from cddjr/trimemedia
提升飞牛服务端地址的兼容性
2025-04-07 21:15:38 +08:00
jxxghp
fac6ad7116 Merge pull request #4109 from cddjr/fix_mteam
修复馒头请求参数错误的问题
2025-04-07 21:14:42 +08:00
景大侠
7d8cda0457 修复馒头请求参数错误的问题 2025-04-07 21:04:21 +08:00
景大侠
33fc3fd63b 新增删除媒体的api 2025-04-07 17:20:47 +08:00
景大侠
8d39cc87f7 提升服务端地址的兼容性 2025-04-07 16:37:41 +08:00
景大侠
d0b1348c96 fix some warnings 2025-04-07 16:21:39 +08:00
jxxghp
0afc38f6b8 Merge pull request #4103 from wikrin/v2 2025-04-07 11:07:11 +08:00
Attente
264896ba17 fix: 剧集组刮削 2025-04-07 09:25:06 +08:00
jxxghp
08decf0b82 feat:新增默认插件库 2025-04-07 08:06:59 +08:00
jxxghp
98381265e6 更新 u115.py 2025-04-07 07:37:00 +08:00
DDSRem
d323159719 Update requirements.in 2025-04-06 13:10:56 +08:00
jxxghp
7ef21e1d1c Merge pull request #4098 from DDS-Derek/dev 2025-04-06 12:02:01 +08:00
DDSRem
2d6b2ab7d7 bump: python environment upgrade 3.12
links https://github.com/jxxghp/MoviePilot/issues/3543
2025-04-06 11:56:00 +08:00
jxxghp
a1e6fd88a9 更新 version.py 2025-04-06 07:53:29 +08:00
jxxghp
e72ff867fc fix 115 pickcode 2025-04-05 09:29:08 +08:00
jxxghp
8512641984 更新 scraper.py 2025-04-04 22:13:14 +08:00
jxxghp
f1aa64d191 fix episodes group 2025-04-04 12:17:42 +08:00
jxxghp
347262538f fix episodes group 2025-04-04 08:59:12 +08:00
jxxghp
82510d60ca 更新 __init__.py 2025-04-03 22:48:29 +08:00
jxxghp
6104cd04c3 更新 context.py 2025-04-03 20:32:56 +08:00
jxxghp
44eb58426a feat:支持指定剧集组识别和刮削 2025-04-03 18:43:04 +08:00
jxxghp
078b60cc1e feat:支持指定剧集组识别和刮削 2025-04-03 18:35:02 +08:00
jxxghp
21e120a4f8 refactor:减少一次接口查询 2025-04-03 10:43:31 +08:00
jxxghp
439b834aa8 更新 version.py 2025-04-02 18:39:50 +08:00
jxxghp
ddbe8324be README增加开发说明 2025-03-30 11:36:19 +08:00
jxxghp
8ffe93113b README增加开发说明 2025-03-30 09:53:34 +08:00
jxxghp
8b31b7cb8a v2.3.6-1
- 修复媒体服务器库存检索问题
- 继续优化搜索页面
2025-03-30 09:23:46 +08:00
jxxghp
e09e21caa9 Merge pull request #4067 from cddjr/fix_media_exists 2025-03-30 02:48:19 +08:00
景大侠
20b145c679 继续修复媒体缺失问题 2025-03-30 02:41:24 +08:00
jxxghp
c5730cf1ad Merge pull request #4065 from cddjr/fix_v235_emby_bug 2025-03-29 23:18:34 +08:00
景大侠
f16b038463 修复v2.3.5引入的emby误报媒体缺失的bug 2025-03-29 23:15:58 +08:00
jxxghp
c08beec232 fix:优化未扫码报错 2025-03-29 22:02:59 +08:00
jxxghp
946361e0ae 更新 requirements.in 2025-03-29 20:30:57 +08:00
jxxghp
97cf65a231 更新 version.py 2025-03-29 20:21:54 +08:00
jxxghp
d7eb6ac15d 更新 alipan.py 2025-03-29 19:30:22 +08:00
jxxghp
075afdbb77 fix alipan upload 2025-03-29 15:39:29 +08:00
jxxghp
2ac047504a fix alipan 2025-03-29 14:52:49 +08:00
jxxghp
c44aa50ef5 fix 上传进度条 2025-03-29 14:33:45 +08:00
jxxghp
7ffafb49c4 fix alipan upload 2025-03-29 10:26:59 +08:00
jxxghp
9b7d57a853 fix alipan api 2025-03-29 09:42:23 +08:00
jxxghp
ac19b3b512 fix alipan api 2025-03-28 21:22:02 +08:00
jxxghp
b030317186 fix: 减少115遍历 2025-03-28 20:58:35 +08:00
jxxghp
b506059874 Merge pull request #4059 from cddjr/trimemedia 2025-03-28 20:13:16 +08:00
景大侠
cf7ba6e17f 移除测试代码 2025-03-28 19:54:47 +08:00
jxxghp
b7ce5663a3 fix ide warnings 2025-03-28 19:43:55 +08:00
jxxghp
58fa8064ad Merge pull request #4058 from cddjr/trimemedia
初步支持飞牛影视
2025-03-28 19:28:35 +08:00
jxxghp
ed48f56526 fix alipan 2025-03-28 17:48:30 +08:00
景大侠
896eb13f7d 初步支持飞牛影视 2025-03-28 16:26:40 +08:00
jxxghp
b8cd1c46c1 feat:Alipan Open Api 2025-03-28 13:40:29 +08:00
jxxghp
c5e84273c0 fix 115目录创建 2025-03-27 19:55:01 +08:00
jxxghp
f21653ffb7 修复115列表异常问题 2025-03-27 17:27:01 +08:00
jxxghp
65c8116cc9 fix 115列表异常处理 2025-03-27 17:26:07 +08:00
jxxghp
5e442433e5 fix 115列表出错时抛出异常 2025-03-27 12:48:19 +08:00
jxxghp
7041347e76 更新 version.py 2025-03-27 12:13:19 +08:00
jxxghp
810c205709 fix 115 2025-03-27 12:04:49 +08:00
jxxghp
ec7035990a fix 2025-03-26 20:12:08 +08:00
jxxghp
da6d9bb2bd fix 115 upload 2025-03-26 18:31:20 +08:00
jxxghp
e009043c63 fix log 2025-03-26 14:00:41 +08:00
jxxghp
79020e9338 hack fix 115 callback format error 2025-03-26 10:39:40 +08:00
jxxghp
2020244cae fix _path_to_id 2025-03-26 08:54:51 +08:00
jxxghp
43fe8f25f8 fix _path_to_id 2025-03-26 08:50:25 +08:00
jxxghp
9522888a60 fix 115 2025-03-26 08:30:30 +08:00
jxxghp
70c183ae2b try fix 115 upload 2025-03-26 07:15:31 +08:00
jxxghp
5d56eb9bef fix 115 upload 2025-03-25 21:33:29 +08:00
jxxghp
a461414a04 fix 115 callback encode 2025-03-25 20:37:46 +08:00
jxxghp
5737c3dca6 fix 115日志频率 2025-03-25 20:00:44 +08:00
jxxghp
57ea50e59c fix 115 callback 2025-03-25 19:38:39 +08:00
jxxghp
7f630e8460 fix 115 callback 2025-03-25 19:37:00 +08:00
jxxghp
108e8502e1 fix 115 上传进度 2025-03-25 19:27:53 +08:00
jxxghp
4aa986d122 fix 115 秒传检测 2025-03-25 18:26:45 +08:00
jxxghp
60239bbfc4 fix bug 2025-03-25 13:57:39 +08:00
jxxghp
93ef3b1f1a add debug logging 2025-03-25 13:48:00 +08:00
jxxghp
d9ed135be4 fix 115 2025-03-25 12:58:03 +08:00
jxxghp
e83fe0aabe fix storage logging 2025-03-25 08:34:36 +08:00
jxxghp
4be7426ae7 fix 115 2025-03-24 22:57:16 +08:00
jxxghp
0ce5ef7f56 fix 115 upload 2025-03-24 21:49:27 +08:00
jxxghp
c2c0946423 fix 115 upload 2025-03-24 21:39:03 +08:00
jxxghp
63049f61f7 fix typing 2025-03-24 19:14:04 +08:00
jxxghp
1918b0f192 fix 115 api 2025-03-24 19:11:18 +08:00
jxxghp
a3ad49b1fa fix 115 api 2025-03-24 19:03:57 +08:00
jxxghp
bed63d1e2b fix 115 api 2025-03-24 19:02:24 +08:00
jxxghp
4a8e739686 fix 115 api 2025-03-24 13:11:23 +08:00
jxxghp
d502f33041 fix 115 open api 2025-03-24 12:04:23 +08:00
jxxghp
4a0ecf36c7 fix typing 2025-03-24 08:40:18 +08:00
jxxghp
afb9e49755 fix typing 2025-03-24 08:11:02 +08:00
jxxghp
18f65e5597 fix year type 2025-03-23 23:16:11 +08:00
jxxghp
22b69f7dac fix blanke 2025-03-23 22:35:37 +08:00
jxxghp
15df062825 更新 discover.py 2025-03-23 22:23:31 +08:00
jxxghp
ed607d3895 更新 recommend.py 2025-03-23 21:57:48 +08:00
jxxghp
f9b0db623d fix cython type error 2025-03-23 21:39:37 +08:00
jxxghp
740cf12c11 fix cython errors 2025-03-23 19:09:48 +08:00
jxxghp
4c4bf698b1 更新 scheduler.py 2025-03-23 18:26:36 +08:00
jxxghp
dc74e749c9 更新 bulit-lite.yml 2025-03-23 18:03:30 +08:00
jxxghp
fa52c542d7 fix lite Dockfile 2025-03-23 15:55:02 +08:00
jxxghp
850d480c7c fix:build lite 2025-03-23 14:48:20 +08:00
jxxghp
a92cc9dce9 更新 bulit-lite.yml 2025-03-23 14:31:29 +08:00
jxxghp
4944a0a456 更新 Dockerfile.lite 2025-03-23 14:28:45 +08:00
jxxghp
13c40058a8 fix:build lite 2025-03-23 13:00:07 +08:00
jxxghp
1410c03c26 feat:build lite 2025-03-23 12:40:14 +08:00
jxxghp
2f38b3040d fix:修复代码兼容性写法 2025-03-23 12:10:21 +08:00
jxxghp
79411a7350 fix:修复代码兼容性写法 2025-03-23 09:00:24 +08:00
jxxghp
ee94c2af32 Merge pull request #4034 from DDS-Derek/dev 2025-03-22 11:31:25 +08:00
DDSRem
d46e5c8d86 bump: docker version 6.1.3 to 7.1.0 2025-03-22 11:13:06 +08:00
jxxghp
95cd10bfba fix #4014 2025-03-22 08:15:58 +08:00
jxxghp
59ed08b92d fix 115 api 2025-03-21 21:08:14 +08:00
jxxghp
2b9f7bca51 fix 115 api 2025-03-21 21:01:37 +08:00
jxxghp
a860a8c02b fix 115 open api 2025-03-21 19:06:53 +08:00
jxxghp
f2cbb8d2f7 fix 115 open api 2025-03-21 18:53:26 +08:00
jxxghp
ea61599589 add 115 open api 2025-03-21 13:27:31 +08:00
jxxghp
0b59c95f63 fix #4029 2025-03-21 11:24:08 +08:00
jxxghp
66d4308810 fix https://github.com/jxxghp/MoviePilot-Frontend/issues/312 2025-03-21 11:19:29 +08:00
jxxghp
f2648df2ad add special domains 2025-03-20 13:00:53 +08:00
jxxghp
d20f68e897 remove setup.py 2025-03-20 08:53:02 +08:00
jxxghp
338021645d 更新 requirements.in 2025-03-19 21:50:26 +08:00
jxxghp
a0a11842cb fix workflow count 2025-03-15 10:16:25 +08:00
jxxghp
f5832d6a25 Merge pull request #4012 from fanrongbin/v2 2025-03-14 17:22:23 +08:00
Robin-PC-X1C
8fa6d9de39 20250314 修改rss.py
修改原因:管理员在mp添加多个豆瓣id时,不同的豆瓣用户订阅内容,发送通知时统一为“豆瓣想看”,无法区分
修改后:增加豆瓣昵称获取,便于推送订阅通知消息时,区分豆瓣用户名称
2025-03-14 16:42:41 +08:00
171 changed files with 6810 additions and 2640 deletions

View File

@@ -46,7 +46,7 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
file: docker/Dockerfile
platforms: |
linux/amd64
linux/arm64/v8

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
.idea/
*.c
*.so
*.pyd
build/
cython_cache/
dist/
nginx/
test.py

View File

@@ -26,6 +26,34 @@
访问官方Wikihttps://wiki.movie-pilot.org
## 参与开发
需要 `Python 3.12``Node JS v20.12.1`
- 克隆主项目 [MoviePilot](https://github.com/jxxghp/MoviePilot)
```shell
git clone https://github.com/jxxghp/MoviePilot
```
- 克隆资源项目 [MoviePilot-Resources](https://github.com/jxxghp/MoviePilot-Resources) ,将 `resources` 目录下对应平台及版本的库 `.so`/`.pyd`/`.bin` 文件复制到 `app/helper` 目录
```shell
git clone https://github.com/jxxghp/MoviePilot-Resources
```
- 安装后端依赖,设置`app`为源代码根目录,运行 `main.py` 启动后端服务,默认监听端口:`3001`API文档地址`http://localhost:3001/docs`
```shell
pip install -r requirements.txt
python3 main.py
```
- 克隆前端项目 [MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
```shell
git clone https://github.com/jxxghp/MoviePilot-Frontend
```
- 安装前端依赖,运行前端项目,访问:`http://localhost:5173`
```shell
yarn
yarn dev
```
- 参考 [插件开发指引](https://wiki.movie-pilot.org/zh/plugindev) 在 `app/plugins` 目录下开发插件代码
## 贡献者
<a href="https://github.com/jxxghp/MoviePilot/graphs/contributors">

View File

@@ -62,7 +62,7 @@ class FetchTorrentsAction(BaseAction):
params = FetchTorrentsParams(**params)
if params.search_type == "keyword":
# 按关键字搜索
torrents = self.searchchain.search_by_title(title=params.name, sites=params.sites, cache_local=False)
torrents = self.searchchain.search_by_title(title=params.name, sites=params.sites)
for torrent in torrents:
if global_vars.is_workflow_stopped(workflow_id):
break

View File

@@ -1,4 +1,4 @@
from typing import List, Any
from typing import List, Any, Optional
from fastapi import APIRouter, Depends
@@ -12,8 +12,8 @@ router = APIRouter()
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
def bangumi_credits(bangumiid: int,
page: int = 1,
count: int = 20,
page: Optional[int] = 1,
count: Optional[int] = 20,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询Bangumi演职员表
@@ -26,8 +26,8 @@ def bangumi_credits(bangumiid: int,
@router.get("/recommend/{bangumiid}", summary="查询Bangumi推荐", response_model=List[schemas.MediaInfo])
def bangumi_recommend(bangumiid: int,
page: int = 1,
count: int = 20,
page: Optional[int] = 1,
count: Optional[int] = 20,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询Bangumi推荐
@@ -49,8 +49,8 @@ def bangumi_person(person_id: int,
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
def bangumi_person_credits(person_id: int,
page: int = 1,
count: int = 20,
page: Optional[int] = 1,
count: Optional[int] = 20,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物参演作品

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, List, Optional
from typing import Any, List, Optional, Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get("/statistic", summary="媒体数量统计", response_model=schemas.Statistic)
def statistic(name: str = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
def statistic(name: Optional[str] = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询媒体数量统计信息
"""
@@ -37,7 +37,7 @@ def statistic(name: str = None, _: schemas.TokenPayload = Depends(verify_token))
@router.get("/statistic2", summary="媒体数量统计API_TOKEN", response_model=schemas.Statistic)
def statistic2(_: str = Depends(verify_apitoken)) -> Any:
def statistic2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
查询媒体数量统计信息 API_TOKEN认证?token=xxx
"""
@@ -66,7 +66,7 @@ def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/storage2", summary="本地存储空间API_TOKEN", response_model=schemas.Storage)
def storage2(_: str = Depends(verify_apitoken)) -> Any:
def storage2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
查询本地存储空间信息 API_TOKEN认证?token=xxx
"""
@@ -82,7 +82,7 @@ def processes(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/downloader", summary="下载器信息", response_model=schemas.DownloaderInfo)
def downloader(name: str = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
def downloader(name: Optional[str] = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询下载器信息
"""
@@ -103,7 +103,7 @@ def downloader(name: str = None, _: schemas.TokenPayload = Depends(verify_token)
@router.get("/downloader2", summary="下载器信息API_TOKEN", response_model=schemas.DownloaderInfo)
def downloader2(_: str = Depends(verify_apitoken)) -> Any:
def downloader2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
查询下载器信息 API_TOKEN认证?token=xxx
"""
@@ -119,7 +119,7 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/schedule2", summary="后台服务API_TOKEN", response_model=List[schemas.ScheduleInfo])
def schedule2(_: str = Depends(verify_apitoken)) -> Any:
def schedule2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
查询下载器信息 API_TOKEN认证?token=xxx
"""
@@ -127,7 +127,7 @@ def schedule2(_: str = Depends(verify_apitoken)) -> Any:
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
def transfer(days: int = 7, db: Session = Depends(get_db),
def transfer(days: Optional[int] = 7, db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询文件整理统计信息
@@ -145,7 +145,7 @@ def cpu(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/cpu2", summary="获取当前CPU使用率API_TOKEN", response_model=int)
def cpu2(_: str = Depends(verify_apitoken)) -> Any:
def cpu2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
获取当前CPU使用率 API_TOKEN认证?token=xxx
"""
@@ -161,7 +161,7 @@ def memory(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/memory2", summary="获取当前内存使用量和使用率API_TOKEN", response_model=List[int])
def memory2(_: str = Depends(verify_apitoken)) -> Any:
def memory2(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
获取当前内存使用率 API_TOKEN认证?token=xxx
"""

View File

@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Optional
from fastapi import APIRouter, Depends
@@ -31,12 +31,12 @@ def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/bangumi", summary="探索Bangumi", response_model=List[schemas.MediaInfo])
def bangumi(type: int = 2,
cat: int = None,
sort: str = 'rank',
year: int = None,
page: int = 1,
count: int = 30,
def bangumi(type: Optional[int] = 2,
cat: Optional[int] = None,
sort: Optional[str] = 'rank',
year: Optional[str] = None,
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
探索Bangumi
@@ -49,10 +49,10 @@ def bangumi(type: int = 2,
@router.get("/douban_movies", summary="探索豆瓣电影", response_model=List[schemas.MediaInfo])
def douban_movies(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
def douban_movies(sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣电影信息
@@ -63,10 +63,10 @@ def douban_movies(sort: str = "R",
@router.get("/douban_tvs", summary="探索豆瓣剧集", response_model=List[schemas.MediaInfo])
def douban_tvs(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
def douban_tvs(sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
@@ -77,15 +77,15 @@ def douban_tvs(sort: str = "R",
@router.get("/tmdb_movies", summary="探索TMDB电影", response_model=List[schemas.MediaInfo])
def tmdb_movies(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1,
def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB电影信息
@@ -104,15 +104,15 @@ def tmdb_movies(sort_by: str = "popularity.desc",
@router.get("/tmdb_tvs", summary="探索TMDB剧集", response_model=List[schemas.MediaInfo])
def tmdb_tvs(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1,
def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息

View File

@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Optional
from fastapi import APIRouter, Depends
@@ -22,7 +22,7 @@ def douban_person(person_id: int,
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
def douban_person_credits(person_id: int,
page: int = 1,
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物参演作品

View File

@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Annotated, Optional
from fastapi import APIRouter, Depends, Body
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get("/", summary="正在下载", response_model=List[schemas.DownloadingTorrent])
def current(
name: str = None,
name: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询正在下载的任务
@@ -30,8 +30,8 @@ def current(
def download(
media_in: schemas.MediaInfo,
torrent_in: schemas.TorrentInfo,
downloader: str = Body(None),
save_path: str = Body(None),
downloader: Annotated[str | None, Body()] = None,
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user)) -> Any:
"""
添加下载任务(含媒体信息)
@@ -62,8 +62,8 @@ def download(
@router.post("/add", summary="添加下载(不含媒体信息)", response_model=schemas.Response)
def add(
torrent_in: schemas.TorrentInfo,
downloader: str = Body(None),
save_path: str = Body(None),
downloader: Annotated[str | None, Body()] = None,
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user)) -> Any:
"""
添加下载任务(不含媒体信息)

View File

@@ -1,4 +1,4 @@
from typing import List, Any
from typing import List, Any, Optional
import jieba
from fastapi import APIRouter, Depends
@@ -20,8 +20,8 @@ router = APIRouter()
@router.get("/download", summary="查询下载历史记录", response_model=List[schemas.DownloadHistory])
def download_history(page: int = 1,
count: int = 30,
def download_history(page: Optional[int] = 1,
count: Optional[int] = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -42,10 +42,10 @@ def delete_download_history(history_in: schemas.DownloadHistory,
@router.get("/transfer", summary="查询整理记录", response_model=schemas.Response)
def transfer_history(title: str = None,
page: int = 1,
count: int = 30,
status: bool = None,
def transfer_history(title: Optional[str] = None,
page: Optional[int] = 1,
count: Optional[int] = 30,
status: Optional[bool] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -78,8 +78,8 @@ def transfer_history(title: str = None,
@router.delete("/transfer", summary="删除整理记录", response_model=schemas.Response)
def delete_transfer_history(history_in: schemas.TransferHistory,
deletesrc: bool = False,
deletedest: bool = False,
deletesrc: Optional[bool] = False,
deletedest: Optional[bool] = False,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""

View File

@@ -1,5 +1,5 @@
from datetime import timedelta
from typing import Any, List
from typing import Any, List, Annotated
from fastapi import APIRouter, Depends, Form, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
@@ -18,8 +18,8 @@ router = APIRouter()
@router.post("/access-token", summary="获取token", response_model=schemas.Token)
def login_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
otp_password: str = Form(None)
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
otp_password: Annotated[str | None, Form()] = None
) -> Any:
"""
获取认证Token
@@ -77,5 +77,7 @@ def wallpapers() -> Any:
return WebUtils.get_bing_wallpapers()
elif settings.WALLPAPER == "mediaserver":
return MediaServerChain().get_latest_wallpapers()
else:
elif settings.WALLPAPER == "tmdb":
return TmdbChain().get_trending_wallpapers()
else:
return []

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import List, Any, Union
from typing import List, Any, Union, Annotated, Optional
from fastapi import APIRouter, Depends
@@ -19,7 +19,7 @@ router = APIRouter()
@router.get("/recognize", summary="识别媒体信息(种子)", response_model=schemas.Context)
def recognize(title: str,
subtitle: str = None,
subtitle: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据标题、副标题识别媒体信息
@@ -33,9 +33,10 @@ def recognize(title: str,
@router.get("/recognize2", summary="识别种子媒体信息API_TOKEN", response_model=schemas.Context)
def recognize2(title: str,
subtitle: str = None,
_: str = Depends(verify_apitoken)) -> Any:
def recognize2(_: Annotated[str, Depends(verify_apitoken)],
title: str,
subtitle: Optional[str] = None
) -> Any:
"""
根据标题、副标题识别媒体信息 API_TOKEN认证?token=xxx
"""
@@ -58,7 +59,7 @@ def recognize_file(path: str,
@router.get("/recognize_file2", summary="识别文件媒体信息API_TOKEN", response_model=schemas.Context)
def recognize_file2(path: str,
_: str = Depends(verify_apitoken)) -> Any:
_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
根据文件路径识别媒体信息 API_TOKEN认证?token=xxx
"""
@@ -68,7 +69,7 @@ def recognize_file2(path: str,
@router.get("/search", summary="搜索媒体/人物信息", response_model=List[dict])
def search(title: str,
type: str = "media",
type: Optional[str] = "media",
page: int = 1,
count: int = 8,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@@ -105,7 +106,7 @@ def search(title: str,
@router.post("/scrape/{storage}", summary="刮削媒体信息", response_model=schemas.Response)
def scrape(fileitem: schemas.FileItem,
storage: str = "local",
storage: Optional[str] = "local",
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
刮削媒体信息
@@ -135,10 +136,28 @@ def category(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return MediaChain().media_category() or {}
@router.get("/group/seasons/{episode_group}", summary="查询剧集组季信息", response_model=List[schemas.MediaSeason])
def group_seasons(episode_group: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询剧集组季信息themoviedb
"""
return TmdbChain().tmdb_group_seasons(group_id=episode_group)
@router.get("/groups/{tmdbid}", summary="查询媒体剧集组", response_model=List[dict])
def seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询媒体剧集组列表themoviedb
"""
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, mtype=MediaType.TV)
if not mediainfo:
return []
return mediainfo.episode_groups
@router.get("/seasons", summary="查询媒体季信息", response_model=List[schemas.MediaSeason])
def seasons(mediaid: str = None,
title: str = None,
year: int = None,
def seasons(mediaid: Optional[str] = None,
title: Optional[str] = None,
year: str = None,
season: int = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -179,7 +198,7 @@ def seasons(mediaid: str = None,
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
def detail(mediaid: str, type_name: str, title: str = None, year: int = None,
def detail(mediaid: str, type_name: str, title: Optional[str] = None, year: str = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧

View File

@@ -1,4 +1,4 @@
from typing import Any, List, Dict
from typing import Any, List, Dict, Optional
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
@@ -43,11 +43,11 @@ def play_item(itemid: str, _: schemas.TokenPayload = Depends(verify_token)) -> s
@router.get("/exists", summary="查询本地是否存在(数据库)", response_model=schemas.Response)
def exists_local(title: str = None,
year: int = None,
mtype: str = None,
tmdbid: int = None,
season: int = None,
def exists_local(title: Optional[str] = None,
year: Optional[str] = None,
mtype: Optional[str] = None,
tmdbid: Optional[int] = None,
season: Optional[int] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -121,7 +121,7 @@ def not_exists(media_in: schemas.MediaInfo,
@router.get("/latest", summary="最新入库条目", response_model=List[schemas.MediaServerPlayItem])
def latest(server: str, count: int = 18,
def latest(server: str, count: Optional[int] = 18,
userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取媒体服务器最新入库条目
@@ -130,7 +130,7 @@ def latest(server: str, count: int = 18,
@router.get("/playing", summary="正在播放条目", response_model=List[schemas.MediaServerPlayItem])
def playing(server: str, count: int = 12,
def playing(server: str, count: Optional[int] = 12,
userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取媒体服务器正在播放条目
@@ -139,7 +139,7 @@ def playing(server: str, count: int = 12,
@router.get("/library", summary="媒体库列表", response_model=List[schemas.MediaServerLibrary])
def library(server: str, hidden: bool = False,
def library(server: str, hidden: Optional[bool] = False,
userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取媒体服务器媒体库列表

View File

@@ -1,5 +1,5 @@
import json
from typing import Union, Any, List
from typing import Union, Any, List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Request
from pywebpush import WebPushException, webpush
@@ -60,8 +60,8 @@ def web_message(text: str, current_user: User = Depends(get_current_active_super
@router.get("/web", summary="获取WEB消息", response_model=List[dict])
def get_web_message(_: schemas.TokenPayload = Depends(verify_token),
db: Session = Depends(get_db),
page: int = 1,
count: int = 20):
page: Optional[int] = 1,
count: Optional[int] = 20):
"""
获取WEB消息列表
"""
@@ -77,7 +77,7 @@ def get_web_message(_: schemas.TokenPayload = Depends(verify_token),
def wechat_verify(echostr: str, msg_signature: str, timestamp: Union[str, int], nonce: str,
source: str = None) -> Any:
source: Optional[str] = None) -> Any:
"""
微信验证响应
"""
@@ -114,8 +114,8 @@ def vocechat_verify() -> Any:
@router.get("/", summary="回调请求验证")
def incoming_verify(token: str = None, echostr: str = None, msg_signature: str = None,
timestamp: Union[str, int] = None, nonce: str = None, source: str = None,
def incoming_verify(token: Optional[str] = None, echostr: Optional[str] = None, msg_signature: Optional[str] = None,
timestamp: Union[str, int] = None, nonce: Optional[str] = None, source: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_apitoken)) -> Any:
"""
微信/VoceChat等验证响应

View File

@@ -1,12 +1,15 @@
import mimetypes
from typing import Annotated, Any, List, Optional
from fastapi import APIRouter, Depends, Header
from fastapi import APIRouter, Depends, Header, HTTPException
from starlette import status
from starlette.responses import FileResponse
from app import schemas
from app.command import Command
from app.core.config import settings
from app.core.plugin import PluginManager
from app.core.security import verify_apikey, verify_token
from app.core.security import verify_apikey, verify_token, verify_apitoken
from app.db.systemconfig_oper import SystemConfigOper
from app.db.user_oper import get_current_active_superuser
from app.factory import app
@@ -16,7 +19,6 @@ from app.scheduler import Scheduler
from app.schemas.types import SystemConfigKey
PROTECTED_ROUTES = {"/api/v1/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc"}
PLUGIN_PREFIX = f"{settings.API_V1_STR}/plugin"
router = APIRouter()
@@ -118,7 +120,7 @@ def _clean_protected_routes(existing_paths: dict):
@router.get("/", summary="所有插件", response_model=List[schemas.Plugin])
def all_plugins(_: schemas.TokenPayload = Depends(get_current_active_superuser),
state: str = "all") -> List[schemas.Plugin]:
state: Optional[str] = "all") -> List[schemas.Plugin]:
"""
查询所有插件清单包括本地插件和在线插件插件状态installed, market, all
"""
@@ -181,8 +183,8 @@ def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/install/{plugin_id}", summary="安装插件", response_model=schemas.Response)
def install(plugin_id: str,
repo_url: str = "",
force: bool = False,
repo_url: Optional[str] = "",
force: Optional[bool] = False,
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""
安装插件
@@ -218,25 +220,60 @@ def install(plugin_id: str,
return schemas.Response(success=True)
@router.get("/remotes", summary="获取插件联邦组件列表", response_model=List[dict])
def remotes(token: str) -> Any:
"""
获取插件联邦组件列表
"""
if token != "moviepilot":
raise HTTPException(status_code=403, detail="Forbidden")
return PluginManager().get_plugin_remotes()
@router.get("/form/{plugin_id}", summary="获取插件表单页面")
def plugin_form(plugin_id: str,
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> dict:
"""
根据插件ID获取插件配置表单
根据插件ID获取插件配置表单或Vue组件URL
"""
conf, model = PluginManager().get_plugin_form(plugin_id)
return {
"conf": conf,
"model": model
}
plugin_instance = PluginManager().running_plugins.get(plugin_id)
if not plugin_instance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载")
# 渲染模式
render_mode, _ = plugin_instance.get_render_mode()
try:
conf, model = plugin_instance.get_form()
return {
"render_mode": render_mode,
"conf": conf,
"model": PluginManager().get_plugin_config(plugin_id) or model
}
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_form 出错: {str(e)}")
return {}
@router.get("/page/{plugin_id}", summary="获取插件数据页面")
def plugin_page(plugin_id: str, _: schemas.TokenPayload = Depends(get_current_active_superuser)) -> List[dict]:
def plugin_page(plugin_id: str, _: schemas.TokenPayload = Depends(get_current_active_superuser)) -> dict:
"""
根据插件ID获取插件数据页面
"""
return PluginManager().get_plugin_page(plugin_id)
plugin_instance = PluginManager().running_plugins.get(plugin_id)
if not plugin_instance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {plugin_id} 不存在或未加载")
# 渲染模式
render_mode, _ = plugin_instance.get_render_mode()
try:
page = plugin_instance.get_page()
return {
"render_mode": render_mode,
"page": page or []
}
except Exception as e:
logger.error(f"插件 {plugin_id} 调用方法 get_page 出错: {str(e)}")
return {}
@router.get("/dashboard/meta", summary="获取所有插件仪表板元信息")
@@ -247,22 +284,22 @@ def plugin_dashboard_meta(_: schemas.TokenPayload = Depends(verify_token)) -> Li
return PluginManager().get_plugin_dashboard_meta()
@router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置")
def plugin_dashboard_by_key(plugin_id: str, key: str, user_agent: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Optional[schemas.PluginDashboard]:
"""
根据插件ID获取插件仪表板
"""
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
@router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置")
def plugin_dashboard(plugin_id: str, user_agent: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> schemas.PluginDashboard:
"""
根据插件ID获取插件仪表板
"""
return PluginManager().get_plugin_dashboard(plugin_id, user_agent=user_agent)
@router.get("/dashboard/{plugin_id}/{key}", summary="获取插件仪表板配置")
def plugin_dashboard(plugin_id: str, key: str, user_agent: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> schemas.PluginDashboard:
"""
根据插件ID获取插件仪表板
"""
return PluginManager().get_plugin_dashboard(plugin_id, key=key, user_agent=user_agent)
return plugin_dashboard_by_key(plugin_id, "", user_agent)
@router.get("/reset/{plugin_id}", summary="重置插件配置及数据", response_model=schemas.Response)
@@ -286,6 +323,41 @@ def reset_plugin(plugin_id: str,
return schemas.Response(success=True)
@router.get("/file/{plugin_id}/{filepath:path}", summary="获取插件静态文件")
def plugin_static_file(plugin_id: str, filepath: str):
"""
获取插件静态文件
"""
# 基础安全检查
if ".." in filepath or ".." in filepath:
logger.warning(f"Static File API: Path traversal attempt detected: {plugin_id}/{filepath}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
plugin_base_dir = settings.ROOT_PATH / "app" / "plugins" / plugin_id.lower()
plugin_file_path = plugin_base_dir / filepath
if not plugin_file_path.exists():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"{plugin_file_path} 不存在")
if not plugin_file_path.is_file():
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"{plugin_file_path} 不是文件")
# 判断 MIME 类型
response_type, _ = mimetypes.guess_type(str(plugin_file_path))
suffix = plugin_file_path.suffix.lower()
# 强制修正 .mjs 和 .js 的 MIME 类型
if suffix in ['.js', '.mjs']:
response_type = 'application/javascript'
elif suffix == '.css' and not response_type: # 如果 guess_type 没猜对 css也修正
response_type = 'text/css'
elif not response_type: # 对于其他猜不出的类型
response_type = 'application/octet-stream'
try:
return FileResponse(plugin_file_path, media_type=response_type)
except Exception as e:
logger.error(f"Error creating/sending FileResponse for {plugin_file_path}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal Server Error")
@router.get("/{plugin_id}", summary="获取插件配置")
def plugin_config(plugin_id: str,
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> dict:

View File

@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Optional
from fastapi import APIRouter, Depends
@@ -6,8 +6,8 @@ from app import schemas
from app.core.event import eventmanager
from app.core.security import verify_token
from app.schemas.types import ChainEventType
from chain.recommend import RecommendChain
from schemas import RecommendSourceEventData
from app.chain.recommend import RecommendChain
from app.schemas import RecommendSourceEventData
router = APIRouter()
@@ -29,8 +29,8 @@ def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/bangumi_calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
def bangumi_calendar(page: int = 1,
count: int = 30,
def bangumi_calendar(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览Bangumi每日放送
@@ -39,8 +39,8 @@ def bangumi_calendar(page: int = 1,
@router.get("/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
def douban_showing(page: int = 1,
count: int = 30,
def douban_showing(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣正在热映
@@ -49,10 +49,10 @@ def douban_showing(page: int = 1,
@router.get("/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
def douban_movies(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
def douban_movies(sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣电影信息
@@ -61,10 +61,10 @@ def douban_movies(sort: str = "R",
@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
def douban_tvs(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
def douban_tvs(sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
@@ -73,8 +73,8 @@ def douban_tvs(sort: str = "R",
@router.get("/douban_movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
def douban_movie_top250(page: int = 1,
count: int = 30,
def douban_movie_top250(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
@@ -83,8 +83,8 @@ def douban_movie_top250(page: int = 1,
@router.get("/douban_tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
def douban_tv_weekly_chinese(page: int = 1,
count: int = 30,
def douban_tv_weekly_chinese(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
中国每周剧集口碑榜
@@ -93,8 +93,8 @@ def douban_tv_weekly_chinese(page: int = 1,
@router.get("/douban_tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
def douban_tv_weekly_global(page: int = 1,
count: int = 30,
def douban_tv_weekly_global(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
全球每周剧集口碑榜
@@ -103,8 +103,8 @@ def douban_tv_weekly_global(page: int = 1,
@router.get("/douban_tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
def douban_tv_animation(page: int = 1,
count: int = 30,
def douban_tv_animation(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门动画剧集
@@ -113,8 +113,8 @@ def douban_tv_animation(page: int = 1,
@router.get("/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
def douban_movie_hot(page: int = 1,
count: int = 30,
def douban_movie_hot(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门电影
@@ -123,8 +123,8 @@ def douban_movie_hot(page: int = 1,
@router.get("/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
def douban_tv_hot(page: int = 1,
count: int = 30,
def douban_tv_hot(page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门电视剧
@@ -133,15 +133,15 @@ def douban_tv_hot(page: int = 1,
@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
def tmdb_movies(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1,
def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB电影信息
@@ -158,15 +158,15 @@ def tmdb_movies(sort_by: str = "popularity.desc",
@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
def tmdb_tvs(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1,
def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
@@ -183,7 +183,7 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
@router.get("/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
def tmdb_trending(page: int = 1,
def tmdb_trending(page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
TMDB流行趋势

View File

@@ -1,4 +1,4 @@
from typing import List, Any
from typing import List, Any, Optional
from fastapi import APIRouter, Depends
@@ -26,20 +26,24 @@ def search_latest(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/media/{mediaid}", summary="精确搜索资源", response_model=schemas.Response)
def search_by_id(mediaid: str,
mtype: str = None,
area: str = "title",
title: str = None,
year: str = None,
season: str = None,
sites: str = None,
mtype: Optional[str] = None,
area: Optional[str] = "title",
title: Optional[str] = None,
year: Optional[str] = None,
season: Optional[str] = None,
sites: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID/豆瓣ID精确搜索站点资源 tmdb:/douban:/bangumi:
"""
if mtype:
mtype = MediaType(mtype)
media_type = MediaType(mtype)
else:
media_type = None
if season:
season = int(season)
media_season = int(season)
else:
media_season = None
if sites:
site_list = [int(site) for site in sites.split(",") if site]
else:
@@ -50,32 +54,32 @@ def search_by_id(mediaid: str,
tmdbid = int(mediaid.replace("tmdb:", ""))
if settings.RECOGNIZE_SOURCE == "douban":
# 通过TMDBID识别豆瓣ID
doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype)
doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=media_type)
if doubaninfo:
torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"),
mtype=mtype, area=area, season=season,
sites=site_list)
mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
else:
return schemas.Response(success=False, message="未识别到豆瓣媒体信息")
else:
torrents = SearchChain().search_by_id(tmdbid=tmdbid, mtype=mtype, area=area, season=season,
sites=site_list)
torrents = SearchChain().search_by_id(tmdbid=tmdbid, mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
elif mediaid.startswith("douban:"):
doubanid = mediaid.replace("douban:", "")
if settings.RECOGNIZE_SOURCE == "themoviedb":
# 通过豆瓣ID识别TMDBID
tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=media_type)
if tmdbinfo:
if tmdbinfo.get('season') and not season:
season = tmdbinfo.get('season')
if tmdbinfo.get('season') and not media_season:
media_season = tmdbinfo.get('season')
torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"),
mtype=mtype, area=area, season=season,
sites=site_list)
mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
else:
return schemas.Response(success=False, message="未识别到TMDB媒体信息")
else:
torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=mtype, area=area, season=season,
sites=site_list)
torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
elif mediaid.startswith("bangumi:"):
bangumiid = int(mediaid.replace("bangumi:", ""))
if settings.RECOGNIZE_SOURCE == "themoviedb":
@@ -83,8 +87,8 @@ def search_by_id(mediaid: str,
tmdbinfo = MediaChain().get_tmdbinfo_by_bangumiid(bangumiid=bangumiid)
if tmdbinfo:
torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"),
mtype=mtype, area=area, season=season,
sites=site_list)
mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
else:
return schemas.Response(success=False, message="未识别到TMDB媒体信息")
else:
@@ -92,8 +96,8 @@ def search_by_id(mediaid: str,
doubaninfo = MediaChain().get_doubaninfo_by_bangumiid(bangumiid=bangumiid)
if doubaninfo:
torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"),
mtype=mtype, area=area, season=season,
sites=site_list)
mtype=media_type, area=area, season=media_season,
sites=site_list, cache_local=True)
else:
return schemas.Response(success=False, message="未识别到豆瓣媒体信息")
else:
@@ -109,11 +113,11 @@ def search_by_id(mediaid: str,
if event_data.media_dict:
search_id = event_data.media_dict.get("id")
if event_data.convert_type == "themoviedb":
torrents = SearchChain().search_by_id(tmdbid=search_id,
mtype=mtype, area=area, season=season)
torrents = SearchChain().search_by_id(tmdbid=search_id, mtype=media_type, area=area,
season=media_season, cache_local=True)
elif event_data.convert_type == "douban":
torrents = SearchChain().search_by_id(doubanid=search_id,
mtype=mtype, area=area, season=season)
torrents = SearchChain().search_by_id(doubanid=search_id, mtype=media_type, area=area,
season=media_season, cache_local=True)
else:
if not title:
return schemas.Response(success=False, message="未知的媒体ID")
@@ -121,19 +125,19 @@ def search_by_id(mediaid: str,
meta = MetaInfo(title)
if year:
meta.year = year
if mtype:
meta.type = mtype
if season:
if media_type:
meta.type = media_type
if media_season:
meta.type = MediaType.TV
meta.begin_season = season
meta.begin_season = media_season
mediainfo = MediaChain().recognize_media(meta=meta)
if mediainfo:
if settings.RECOGNIZE_SOURCE == "themoviedb":
torrents = SearchChain().search_by_id(tmdbid=mediainfo.tmdb_id,
mtype=mtype, area=area, season=season)
torrents = SearchChain().search_by_id(tmdbid=mediainfo.tmdb_id, mtype=media_type, area=area,
season=media_season, cache_local=True)
else:
torrents = SearchChain().search_by_id(doubanid=mediainfo.douban_id,
mtype=mtype, area=area, season=season)
torrents = SearchChain().search_by_id(doubanid=mediainfo.douban_id, mtype=media_type, area=area,
season=media_season, cache_local=True)
# 返回搜索结果
if not torrents:
return schemas.Response(success=False, message="未搜索到任何资源")
@@ -142,15 +146,16 @@ def search_by_id(mediaid: str,
@router.get("/title", summary="模糊搜索资源", response_model=schemas.Response)
def search_by_title(keyword: str = None,
page: int = 0,
sites: str = None,
def search_by_title(keyword: Optional[str] = None,
page: Optional[int] = 0,
sites: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据名称模糊搜索站点资源,支持分页,关键词为空是返回首页资源
"""
torrents = SearchChain().search_by_title(title=keyword, page=page,
sites=[int(site) for site in sites.split(",") if site] if sites else None)
sites=[int(site) for site in sites.split(",") if site] if sites else None,
cache_local=True)
if not torrents:
return schemas.Response(success=False, message="未搜索到任何资源")
return schemas.Response(success=True, data=[torrent.to_dict() for torrent in torrents])

View File

@@ -1,4 +1,4 @@
from typing import List, Any, Dict
from typing import List, Any, Dict, Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
@@ -145,7 +145,7 @@ def update_cookie(
site_id: int,
username: str,
password: str,
code: str = None,
code: Optional[str] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""
@@ -203,7 +203,7 @@ def read_userdata_latest(
@router.get("/userdata/{site_id}", summary="查询某站点用户数据", response_model=schemas.Response)
def read_userdata(
site_id: int,
workdate: str = None,
workdate: Optional[str] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""
@@ -291,9 +291,9 @@ def site_category(site_id: int,
@router.get("/resource/{site_id}", summary="站点资源", response_model=List[schemas.TorrentInfo])
def site_resource(site_id: int,
keyword: str = None,
cat: str = None,
page: int = 0,
keyword: Optional[str] = None,
cat: Optional[str] = None,
page: Optional[int] = 0,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from pathlib import Path
from typing import Any, List
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from starlette.responses import FileResponse, Response
@@ -27,11 +27,12 @@ def qrcode(name: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
qrcode_data, errmsg = StorageChain().generate_qrcode(name)
if qrcode_data:
return schemas.Response(success=True, data=qrcode_data, message=errmsg)
return schemas.Response(success=False)
return schemas.Response(success=False, message=errmsg)
@router.get("/check/{name}", summary="二维码登录确认", response_model=schemas.Response)
def check(name: str, ck: str = None, t: str = None, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
def check(name: str, ck: Optional[str] = None, t: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
二维码登录确认
"""
@@ -57,7 +58,7 @@ def save(name: str,
@router.post("/list", summary="所有目录和文件", response_model=List[schemas.FileItem])
def list_files(fileitem: schemas.FileItem,
sort: str = 'updated_at',
sort: Optional[str] = 'updated_at',
_: User = Depends(get_current_active_superuser)) -> Any:
"""
查询当前目录下所有目录和文件
@@ -140,7 +141,7 @@ def image(fileitem: schemas.FileItem,
@router.post("/rename", summary="重命名文件或目录", response_model=schemas.Response)
def rename(fileitem: schemas.FileItem,
new_name: str,
recursive: bool = False,
recursive: Optional[bool] = False,
_: User = Depends(get_current_active_superuser)) -> Any:
"""
重命名文件或目录

View File

@@ -1,4 +1,4 @@
from typing import List, Any
from typing import List, Any, Annotated, Optional
import cn2an
from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header
@@ -44,7 +44,7 @@ def read_subscribes(
@router.get("/list", summary="查询所有订阅API_TOKEN", response_model=List[schemas.Subscribe])
def list_subscribes(_: str = Depends(verify_apitoken)) -> Any:
def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
查询所有订阅 API_TOKEN认证?token=xxx
"""
@@ -75,22 +75,12 @@ def create_subscribe(
title = subscribe_in.name
else:
title = None
# 订阅用户
subscribe_in.username = current_user.name
sid, message = SubscribeChain().add(mtype=mtype,
title=title,
year=subscribe_in.year,
tmdbid=subscribe_in.tmdbid,
season=subscribe_in.season,
doubanid=subscribe_in.doubanid,
bangumiid=subscribe_in.bangumiid,
mediaid=subscribe_in.mediaid,
username=current_user.name,
best_version=subscribe_in.best_version,
save_path=subscribe_in.save_path,
search_imdbid=subscribe_in.search_imdbid,
custom_words=subscribe_in.custom_words,
media_category=subscribe_in.media_category,
filter_groups=subscribe_in.filter_groups,
exist_ok=True)
exist_ok=True,
**subscribe_in.dict())
return schemas.Response(
success=bool(sid), message=message, data={"id": sid}
)
@@ -165,8 +155,8 @@ def update_subscribe_status(
@router.get("/media/{mediaid}", summary="查询订阅", response_model=schemas.Subscribe)
def subscribe_mediaid(
mediaid: str,
season: int = None,
title: str = None,
season: Optional[int] = None,
title: Optional[str] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -294,7 +284,7 @@ def search_subscribe(
@router.delete("/media/{mediaid}", summary="删除订阅", response_model=schemas.Response)
def delete_subscribe_by_mediaid(
mediaid: str,
season: int = None,
season: Optional[int] = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)
) -> Any:
@@ -331,7 +321,7 @@ def delete_subscribe_by_mediaid(
@router.post("/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=schemas.Response)
async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
authorization: str = Header(None)) -> Any:
authorization: Annotated[str | None, Header()] = None) -> Any:
"""
Jellyseerr/Overseerr网络勾子通知订阅
"""
@@ -385,8 +375,8 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
@router.get("/history/{mtype}", summary="查询订阅历史", response_model=List[schemas.Subscribe])
def subscribe_history(
mtype: str,
page: int = 1,
count: int = 30,
page: Optional[int] = 1,
count: Optional[int] = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -411,9 +401,9 @@ def delete_subscribe(
@router.get("/popular", summary="热门订阅(基于用户共享数据)", response_model=List[schemas.MediaInfo])
def popular_subscribes(
stype: str,
page: int = 1,
count: int = 30,
min_sub: int = None,
page: Optional[int] = 1,
count: Optional[int] = 30,
min_sub: Optional[int] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询热门订阅
@@ -532,7 +522,7 @@ def followed_subscribers(_: schemas.TokenPayload = Depends(verify_token)) -> Any
@router.post("/follow", summary="Follow订阅分享人", response_model=schemas.Response)
def follow_subscriber(
share_uid: str = None,
share_uid: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
Follow订阅分享人
@@ -546,7 +536,7 @@ def follow_subscriber(
@router.delete("/follow", summary="取消Follow订阅分享人", response_model=schemas.Response)
def unfollow_subscriber(
share_uid: str = None,
share_uid: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
取消Follow订阅分享人
@@ -560,9 +550,9 @@ def unfollow_subscriber(
@router.get("/shares", summary="查询分享的订阅", response_model=List[schemas.SubscribeShare])
def popular_subscribes(
name: str = None,
page: int = 1,
count: int = 30,
name: Optional[str] = None,
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询分享的订阅

View File

@@ -5,7 +5,7 @@ import tempfile
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Optional, Union
from typing import Optional, Union, Annotated
import aiofiles
import pillow_avif # noqa 用于自动注册AVIF支持
@@ -28,6 +28,7 @@ from app.helper.message import MessageHelper, MessageQueueManager
from app.helper.progress import ProgressHelper
from app.helper.rule import RuleHelper
from app.helper.sites import SitesHelper
from app.helper.subscribe import SubscribeHelper
from app.log import logger
from app.monitor import Monitor
from app.scheduler import Scheduler
@@ -141,7 +142,7 @@ def fetch_image(
def proxy_img(
imgurl: str,
proxy: bool = False,
if_none_match: Optional[str] = Header(None),
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token)
) -> Response:
"""
@@ -158,7 +159,7 @@ def proxy_img(
@router.get("/cache/image", summary="图片缓存")
def cache_img(
url: str,
if_none_match: Optional[str] = Header(None),
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token)
) -> Response:
"""
@@ -170,18 +171,22 @@ def cache_img(
@router.get("/global", summary="查询非敏感系统设置", response_model=schemas.Response)
def get_global_setting():
def get_global_setting(token: str):
"""
查询非敏感系统设置(无需鉴权)
查询非敏感系统设置(默认鉴权)
"""
if token != "moviepilot":
raise HTTPException(status_code=403, detail="Forbidden")
# FIXME: 新增敏感配置项时要在此处添加排除项
info = settings.dict(
exclude={"SECRET_KEY", "RESOURCE_SECRET_KEY", "API_TOKEN", "TMDB_API_KEY", "TVDB_API_KEY", "FANART_API_KEY",
"COOKIECLOUD_KEY", "COOKIECLOUD_PASSWORD", "GITHUB_TOKEN", "REPO_GITHUB_TOKEN"}
)
# 追加用户唯一ID
# 追加用户唯一ID和订阅分享管理权限
info.update({
"USER_UNIQUE_ID": SystemUtils.generate_user_unique_id()
"USER_UNIQUE_ID": SubscribeHelper().get_user_uuid(),
"SUBSCRIBE_SHARE_MANAGE": SubscribeHelper().is_admin_user(),
})
return schemas.Response(success=True,
data=info)
@@ -281,6 +286,9 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None,
success, message = settings.update_setting(key=key, value=value)
return schemas.Response(success=success, message=message)
elif key in {item.value for item in SystemConfigKey}:
if isinstance(value, list):
value = list(filter(None, value))
value = value if value else None
SystemConfigOper().set(key, value)
return schemas.Response(success=True)
else:
@@ -288,7 +296,8 @@ def set_setting(key: str, value: Union[list, dict, bool, int, str] = None,
@router.get("/message", summary="实时消息")
async def get_message(request: Request, role: str = "system", _: schemas.TokenPayload = Depends(verify_resource_token)):
async def get_message(request: Request, role: Optional[str] = "system",
_: schemas.TokenPayload = Depends(verify_resource_token)):
"""
实时获取系统消息返回格式为SSE
"""
@@ -309,7 +318,7 @@ async def get_message(request: Request, role: str = "system", _: schemas.TokenPa
@router.get("/logging", summary="实时日志")
async def get_logging(request: Request, length: int = 50, logfile: str = "moviepilot.log",
async def get_logging(request: Request, length: Optional[int] = 50, logfile: Optional[str] = "moviepilot.log",
_: schemas.TokenPayload = Depends(verify_resource_token)):
"""
实时获取系统日志
@@ -381,7 +390,7 @@ def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
@router.get("/ruletest", summary="过滤规则测试", response_model=schemas.Response)
def ruletest(title: str,
rulegroup_name: str,
subtitle: str = None,
subtitle: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)):
"""
过滤规则测试,规则类型 1-订阅2-洗版3-搜索
@@ -500,7 +509,7 @@ def run_scheduler(jobid: str,
@router.get("/runscheduler2", summary="运行服务API_TOKEN", response_model=schemas.Response)
def run_scheduler2(jobid: str,
_: str = Depends(verify_apitoken)):
_: Annotated[str, Depends(verify_apitoken)]):
"""
执行命令API_TOKEN认证
"""

View File

@@ -1,4 +1,4 @@
from typing import List, Any
from typing import List, Any, Optional
from fastapi import APIRouter, Depends
@@ -61,8 +61,8 @@ def tmdb_recommend(tmdbid: int,
@router.get("/collection/{collection_id}", summary="系列合集详情", response_model=List[schemas.MediaInfo])
def tmdb_collection(collection_id: int,
page: int = 1,
count: int = 20,
page: Optional[int] = 1,
count: Optional[int] = 20,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据合集ID查询合集详情
@@ -76,7 +76,7 @@ def tmdb_collection(collection_id: int,
@router.get("/credits/{tmdbid}/{type_name}", summary="演员阵容", response_model=List[schemas.MediaPerson])
def tmdb_credits(tmdbid: int,
type_name: str,
page: int = 1,
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询演员阵容type_name: 电影/电视剧
@@ -102,7 +102,7 @@ def tmdb_person(person_id: int,
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
def tmdb_person_credits(person_id: int,
page: int = 1,
page: Optional[int] = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物参演作品
@@ -114,9 +114,9 @@ def tmdb_person_credits(person_id: int,
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
def tmdb_season_episodes(tmdbid: int, season: int,
def tmdb_season_episodes(tmdbid: int, season: int, episode_group: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询某季的所有信信息
"""
return TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season)
return TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season, episode_group=episode_group)

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, List
from typing import Any, List, Annotated, Optional
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
@@ -69,7 +69,7 @@ def remove_queue(fileitem: schemas.FileItem, _: schemas.TokenPayload = Depends(v
@router.post("/manual", summary="手动转移", response_model=schemas.Response)
def manual_transfer(transer_item: ManualTransferItem,
background: bool = False,
background: Optional[bool] = False,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(get_current_active_superuser)) -> Any:
"""
@@ -146,6 +146,7 @@ def manual_transfer(transer_item: ManualTransferItem,
doubanid=transer_item.doubanid,
mtype=mtype,
season=transer_item.season,
episode_group=transer_item.episode_group,
transfer_type=transer_item.transfer_type,
epformat=epformat,
min_filesize=transer_item.min_filesize,
@@ -165,7 +166,7 @@ def manual_transfer(transer_item: ManualTransferItem,
@router.get("/now", summary="立即执行下载器文件整理", response_model=schemas.Response)
def now(_: str = Depends(verify_apitoken)) -> Any:
def now(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
立即执行下载器文件整理 API_TOKEN认证?token=xxx
"""

View File

@@ -1,4 +1,4 @@
from typing import Any
from typing import Any, Annotated
from fastapi import APIRouter, BackgroundTasks, Request, Depends
@@ -19,7 +19,7 @@ def start_webhook_chain(body: Any, form: Any, args: Any):
@router.post("/", summary="Webhook消息响应", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks,
request: Request,
_: str = Depends(verify_apitoken)
_: Annotated[str, Depends(verify_apitoken)]
) -> Any:
"""
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=媒体服务器名
@@ -33,7 +33,7 @@ async def webhook_message(background_tasks: BackgroundTasks,
@router.get("/", summary="Webhook消息响应", response_model=schemas.Response)
def webhook_message(background_tasks: BackgroundTasks,
request: Request, _: str = Depends(verify_apitoken)) -> Any:
request: Request, _: Annotated[str, Depends(verify_apitoken)]) -> Any:
"""
Webhook响应配置请求中需要添加参数token=API_TOKEN&source=媒体服务器名
"""

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Any
from typing import List, Any, Optional
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
@@ -96,7 +96,7 @@ def delete_workflow(workflow_id: int,
@router.post("/{workflow_id}/run", summary="执行工作流", response_model=schemas.Response)
def run_workflow(workflow_id: int,
from_begin: bool = True,
from_begin: Optional[bool] = True,
_: schemas.TokenPayload = Depends(get_current_active_user)) -> Any:
"""
执行工作流
@@ -156,7 +156,7 @@ def reset_workflow(workflow_id: int,
# 停止工作流
global_vars.stop_workflow(workflow_id)
# 重置工作流
workflow.reset(db, workflow_id)
workflow.reset(db, workflow_id, reset_count=True)
# 删除缓存
SystemConfigOper().delete(f"WorkflowCache-{workflow_id}")
return schemas.Response(success=True)

View File

@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Annotated
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
@@ -18,7 +18,7 @@ arr_router = APIRouter(tags=['servarr'])
@arr_router.get("/system/status", summary="系统状态")
def arr_system_status(_: str = Depends(verify_apikey)) -> Any:
def arr_system_status(_: Annotated[str, Depends(verify_apikey)]) -> Any:
"""
模拟Radarr、Sonarr系统状态
"""
@@ -72,7 +72,7 @@ def arr_system_status(_: str = Depends(verify_apikey)) -> Any:
@arr_router.get("/qualityProfile", summary="质量配置")
def arr_qualityProfile(_: str = Depends(verify_apikey)) -> Any:
def arr_qualityProfile(_: Annotated[str, Depends(verify_apikey)]) -> Any:
"""
模拟Radarr、Sonarr质量配置
"""
@@ -113,7 +113,7 @@ def arr_qualityProfile(_: str = Depends(verify_apikey)) -> Any:
@arr_router.get("/rootfolder", summary="根目录")
def arr_rootfolder(_: str = Depends(verify_apikey)) -> Any:
def arr_rootfolder(_: Annotated[str, Depends(verify_apikey)]) -> Any:
"""
模拟Radarr、Sonarr根目录
"""
@@ -129,7 +129,7 @@ def arr_rootfolder(_: str = Depends(verify_apikey)) -> Any:
@arr_router.get("/tag", summary="标签")
def arr_tag(_: str = Depends(verify_apikey)) -> Any:
def arr_tag(_: Annotated[str, Depends(verify_apikey)]) -> Any:
"""
模拟Radarr、Sonarr标签
"""
@@ -142,7 +142,7 @@ def arr_tag(_: str = Depends(verify_apikey)) -> Any:
@arr_router.get("/languageprofile", summary="语言")
def arr_languageprofile(_: str = Depends(verify_apikey)) -> Any:
def arr_languageprofile(_: Annotated[str, Depends(verify_apikey)]) -> Any:
"""
模拟Radarr、Sonarr语言
"""
@@ -168,7 +168,7 @@ def arr_languageprofile(_: str = Depends(verify_apikey)) -> Any:
@arr_router.get("/movie", summary="所有订阅电影", response_model=List[schemas.RadarrMovie])
def arr_movies(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -> Any:
def arr_movies(_: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Rardar电影
"""
@@ -259,7 +259,7 @@ def arr_movies(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -
@arr_router.get("/movie/lookup", summary="查询电影", response_model=List[schemas.RadarrMovie])
def arr_movie_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_movie_lookup(term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Rardar电影 term: `tmdb:${id}`
存在和不存在均不能返回错误
@@ -305,7 +305,7 @@ def arr_movie_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(
@arr_router.get("/movie/{mid}", summary="电影订阅详情", response_model=schemas.RadarrMovie)
def arr_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_movie(mid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Rardar电影订阅
"""
@@ -331,9 +331,9 @@ def arr_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_a
@arr_router.post("/movie", summary="新增电影订阅")
def arr_add_movie(movie: RadarrMovie,
db: Session = Depends(get_db),
_: str = Depends(verify_apikey)
def arr_add_movie(_: Annotated[str, Depends(verify_apikey)],
movie: RadarrMovie,
db: Session = Depends(get_db)
) -> Any:
"""
新增Rardar电影订阅
@@ -362,7 +362,7 @@ def arr_add_movie(movie: RadarrMovie,
@arr_router.delete("/movie/{mid}", summary="删除电影订阅", response_model=schemas.Response)
def arr_remove_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_remove_movie(mid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
删除Rardar电影订阅
"""
@@ -378,7 +378,7 @@ def arr_remove_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(v
@arr_router.get("/series", summary="所有剧集", response_model=List[schemas.SonarrSeries])
def arr_series(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -> Any:
def arr_series(_: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Sonarr剧集
"""
@@ -514,36 +514,37 @@ def arr_series(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -
@arr_router.get("/series/lookup", summary="查询剧集")
def arr_series_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_series_lookup(term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Sonarr剧集 term: `tvdb:${id}` title
"""
# 季信息
seas: List[int] = []
# 获取TVDBID
if not term.startswith("tvdb:"):
mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
mtype=MediaType.TV)
if not mediainfo:
return [SonarrSeries()]
tvdbid = mediainfo.tvdb_id
if not tvdbid:
return [SonarrSeries()]
# 季信息
if mediainfo.seasons:
seas = list(mediainfo.seasons)
else:
mediainfo = None
tvdbid = int(term.replace("tvdb:", ""))
# 查询TVDB信息
tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
if not tvdbinfo:
return [SonarrSeries()]
# 查询TVDB信息
tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
if not tvdbinfo:
return [SonarrSeries()]
# 季信息
seas: List[int] = []
sea_num = tvdbinfo.get('season')
if sea_num:
seas = list(range(1, int(sea_num) + 1))
# 季信息
sea_num = tvdbinfo.get('season')
if sea_num:
seas = list(range(1, int(sea_num) + 1))
# 根据TVDB查询媒体信息
if not mediainfo:
# 根据TVDB查询媒体信息
mediainfo = MediaChain().recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
mtype=MediaType.TV)
@@ -603,7 +604,7 @@ def arr_series_lookup(term: str, db: Session = Depends(get_db), _: str = Depends
@arr_router.get("/series/{tid}", summary="剧集详情")
def arr_serie(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_serie(tid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
查询Sonarr剧集
"""
@@ -638,8 +639,8 @@ def arr_serie(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_a
@arr_router.post("/series", summary="新增剧集订阅")
def arr_add_series(tv: schemas.SonarrSeries,
db: Session = Depends(get_db),
_: str = Depends(verify_apikey)) -> Any:
_: Annotated[str, Depends(verify_apikey)],
db: Session = Depends(get_db)) -> Any:
"""
新增Sonarr剧集订阅
"""
@@ -689,7 +690,7 @@ def arr_update_series(tv: schemas.SonarrSeries) -> Any:
@arr_router.delete("/series/{tid}", summary="删除剧集订阅")
def arr_remove_series(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any:
def arr_remove_series(tid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any:
"""
删除Sonarr剧集订阅
"""

View File

@@ -3,6 +3,7 @@ import gc
import pickle
import traceback
from abc import ABCMeta
from collections.abc import Callable
from pathlib import Path
from typing import Optional, Any, Tuple, List, Set, Union, Dict
@@ -14,9 +15,10 @@ from app.core.context import Context, MediaInfo, TorrentInfo
from app.core.event import EventManager
from app.core.meta import MetaBase
from app.core.module import ModuleManager
from app.core.plugin import PluginManager
from app.db.message_oper import MessageOper
from app.db.user_oper import UserOper
from app.helper.message import MessageHelper, MessageQueueManager
from app.helper.message import MessageHelper, MessageQueueManager, MessageTemplateHelper
from app.helper.service import ServiceConfigHelper
from app.log import logger
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent, CommingMessage, Notification, \
@@ -42,6 +44,7 @@ class ChainBase(metaclass=ABCMeta):
send_callback=self.run_module
)
self.useroper = UserOper()
self.pluginmanager = PluginManager()
@staticmethod
def load_cache(filename: str) -> Any:
@@ -94,10 +97,53 @@ class ChainBase(metaclass=ABCMeta):
if isinstance(ret, tuple):
return all(value is None for value in ret)
else:
return result is None
return ret is None
logger.debug(f"请求模块执行:{method} ...")
result = None
plugin_modules = self.pluginmanager.get_plugin_modules()
# 插件模块
for plugin, module_dict in plugin_modules.items():
plugin_id, plugin_name = plugin
if method in module_dict:
func = module_dict[method]
if func:
try:
logger.info(f"请求插件 {plugin_name} 执行:{method} ...")
if is_result_empty(result):
# 返回None第一次执行或者需继续执行下一模块
result = func(*args, **kwargs)
elif isinstance(result, list):
# 返回为列表,有多个模块运行结果时进行合并
temp = func(*args, **kwargs)
if isinstance(temp, list):
result.extend(temp)
else:
break
except Exception as err:
if kwargs.get("raise_exception"):
raise
logger.error(
f"运行插件 {plugin_id} 模块 {method} 出错:{str(err)}\n{traceback.format_exc()}")
self.messagehelper.put(title=f"{plugin_name} 发生了错误",
message=str(err),
role="plugin")
self.eventmanager.send_event(
EventType.SystemError,
{
"type": "plugin",
"plugin_id": plugin_id,
"plugin_name": plugin_name,
"plugin_method": method,
"error": str(err),
"traceback": traceback.format_exc()
}
)
if not is_result_empty(result) and not isinstance(result, list):
# 插件模块返回结果不为空且不是列表,直接返回
return result
# 系统模块
logger.debug(f"请求系统模块执行:{method} ...")
modules = self.modulemanager.get_running_modules(method)
# 按优先级排序
modules = sorted(modules, key=lambda x: x.get_priority())
@@ -114,10 +160,10 @@ class ChainBase(metaclass=ABCMeta):
# 返回None第一次执行或者需继续执行下一模块
result = func(*args, **kwargs)
elif ObjectUtils.check_signature(func, result):
# 返回结果与方法签名一致,将结果传入(不能多个模块同时运行的需要通过开关控制)
# 返回结果与方法签名一致,将结果传入
result = func(result)
elif isinstance(result, list):
# 返回为列表,有多个模块运行结果时进行合并(不能多个模块同时运行的需要通过开关控制)
# 返回为列表,有多个模块运行结果时进行合并
temp = func(*args, **kwargs)
if isinstance(temp, list):
result.extend(temp)
@@ -146,10 +192,11 @@ class ChainBase(metaclass=ABCMeta):
return result
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
tmdbid: int = None,
doubanid: str = None,
bangumiid: int = None,
mtype: Optional[MediaType] = None,
tmdbid: Optional[int] = None,
doubanid: Optional[str] = None,
bangumiid: Optional[int] = None,
episode_group: Optional[str] = None,
cache: bool = True) -> Optional[MediaInfo]:
"""
识别媒体信息不含Fanart图片
@@ -158,6 +205,7 @@ class ChainBase(metaclass=ABCMeta):
:param tmdbid: tmdbid
:param doubanid: 豆瓣ID
:param bangumiid: BangumiID
:param episode_group: 剧集组
:param cache: 是否使用缓存
:return: 识别的媒体信息,包括剧集信息
"""
@@ -173,10 +221,11 @@ class ChainBase(metaclass=ABCMeta):
doubanid = None
bangumiid = None
return self.run_module("recognize_media", meta=meta, mtype=mtype,
tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, cache=cache)
tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid,
episode_group=episode_group, cache=cache)
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: MediaType = None, year: str = None, season: int = None,
def match_doubaninfo(self, name: str, imdbid: Optional[str] = None,
mtype: Optional[MediaType] = None, year: Optional[str] = None, season: Optional[int] = None,
raise_exception: bool = False) -> Optional[dict]:
"""
搜索和匹配豆瓣信息
@@ -190,8 +239,8 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("match_doubaninfo", name=name, imdbid=imdbid,
mtype=mtype, year=year, season=season, raise_exception=raise_exception)
def match_tmdbinfo(self, name: str, mtype: MediaType = None,
year: str = None, season: int = None) -> Optional[dict]:
def match_tmdbinfo(self, name: str, mtype: Optional[MediaType] = None,
year: Optional[str] = None, season: Optional[int] = None) -> Optional[dict]:
"""
搜索和匹配TMDB信息
:param name: 标题
@@ -211,8 +260,8 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("obtain_images", mediainfo=mediainfo)
def obtain_specific_image(self, mediaid: Union[str, int], mtype: MediaType,
image_type: MediaImageType, image_prefix: str = None,
season: int = None, episode: int = None) -> Optional[str]:
image_type: MediaImageType, image_prefix: Optional[str] = None,
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[str]:
"""
获取指定媒体信息图片,返回图片地址
:param mediaid: 媒体ID
@@ -226,7 +275,7 @@ class ChainBase(metaclass=ABCMeta):
image_prefix=image_prefix, image_type=image_type,
season=season, episode=episode)
def douban_info(self, doubanid: str, mtype: MediaType = None,
def douban_info(self, doubanid: str, mtype: Optional[MediaType] = None,
raise_exception: bool = False) -> Optional[dict]:
"""
获取豆瓣信息
@@ -245,7 +294,7 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("tvdb_info", tvdbid=tvdbid)
def tmdb_info(self, tmdbid: int, mtype: MediaType, season: int = None) -> Optional[dict]:
def tmdb_info(self, tmdbid: int, mtype: MediaType, season: Optional[int] = None) -> Optional[dict]:
"""
获取TMDB信息
:param tmdbid: int
@@ -312,8 +361,8 @@ class ChainBase(metaclass=ABCMeta):
def search_torrents(self, site: dict,
keywords: List[str],
mtype: MediaType = None,
page: int = 0) -> List[TorrentInfo]:
mtype: Optional[MediaType] = None,
page: Optional[int] = 0) -> List[TorrentInfo]:
"""
搜索一个站点的种子资源
:param site: 站点
@@ -325,7 +374,8 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("search_torrents", site=site, keywords=keywords,
mtype=mtype, page=page)
def refresh_torrents(self, site: dict, keyword: str = None, cat: str = None, page: int = 0) -> List[TorrentInfo]:
def refresh_torrents(self, site: dict, keyword: Optional[str] = None,
cat: Optional[str] = None, page: Optional[int] = 0) -> List[TorrentInfo]:
"""
获取站点最新一页的种子,多个站点需要多线程处理
:param site: 站点
@@ -350,8 +400,8 @@ class ChainBase(metaclass=ABCMeta):
torrent_list=torrent_list, mediainfo=mediainfo)
def download(self, content: Union[Path, str], download_dir: Path, cookie: str,
episodes: Set[int] = None, category: str = None, label: str = None,
downloader: str = None
episodes: Set[int] = None, category: Optional[str] = None, label: Optional[str] = None,
downloader: Optional[str] = None
) -> Optional[Tuple[Optional[str], Optional[str], Optional[str], str]]:
"""
根据种子文件,选择并添加下载任务
@@ -381,7 +431,7 @@ class ChainBase(metaclass=ABCMeta):
def list_torrents(self, status: TorrentStatus = None,
hashs: Union[list, str] = None,
downloader: str = None
downloader: Optional[str] = None
) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
"""
获取下载器种子列表
@@ -394,10 +444,11 @@ class ChainBase(metaclass=ABCMeta):
def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: MediaInfo,
target_directory: TransferDirectoryConf = None,
target_storage: str = None, target_path: Path = None,
transfer_type: str = None, scrape: bool = None,
target_storage: Optional[str] = None, target_path: Path = None,
transfer_type: Optional[str] = None, scrape: bool = None,
library_type_folder: bool = None, library_category_folder: bool = None,
episodes_info: List[TmdbEpisode] = None) -> Optional[TransferInfo]:
episodes_info: List[TmdbEpisode] = None,
source_oper: Callable = None, target_oper: Callable = None) -> Optional[TransferInfo]:
"""
文件转移
:param fileitem: 文件信息
@@ -411,6 +462,8 @@ class ChainBase(metaclass=ABCMeta):
:param library_type_folder: 是否按类型创建目录
:param library_category_folder: 是否按类别创建目录
:param episodes_info: 当前季的全部集信息
:param source_oper: 源存储操作类
:param target_oper: 目标存储操作类
:return: {path, target_path, message}
"""
return self.run_module("transfer",
@@ -420,9 +473,10 @@ class ChainBase(metaclass=ABCMeta):
transfer_type=transfer_type, scrape=scrape,
library_type_folder=library_type_folder,
library_category_folder=library_category_folder,
episodes_info=episodes_info)
episodes_info=episodes_info,
source_oper=source_oper, target_oper=target_oper)
def transfer_completed(self, hashs: str, downloader: str = None) -> None:
def transfer_completed(self, hashs: str, downloader: Optional[str] = None) -> None:
"""
下载器转移完成后的处理
:param hashs: 种子Hash
@@ -431,7 +485,7 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("transfer_completed", hashs=hashs, downloader=downloader)
def remove_torrents(self, hashs: Union[str, list], delete_file: bool = True,
downloader: str = None) -> bool:
downloader: Optional[str] = None) -> bool:
"""
删除下载器种子
:param hashs: 种子Hash
@@ -441,7 +495,7 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("remove_torrents", hashs=hashs, delete_file=delete_file, downloader=downloader)
def start_torrents(self, hashs: Union[list, str], downloader: str = None) -> bool:
def start_torrents(self, hashs: Union[list, str], downloader: Optional[str] = None) -> bool:
"""
开始下载
:param hashs: 种子Hash
@@ -450,7 +504,7 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("start_torrents", hashs=hashs, downloader=downloader)
def stop_torrents(self, hashs: Union[list, str], downloader: str = None) -> bool:
def stop_torrents(self, hashs: Union[list, str], downloader: Optional[str] = None) -> bool:
"""
停止下载
:param hashs: 种子Hash
@@ -460,7 +514,7 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("stop_torrents", hashs=hashs, downloader=downloader)
def torrent_files(self, tid: str,
downloader: str = None) -> Optional[Union[TorrentFilesList, List[File]]]:
downloader: Optional[str] = None) -> Optional[Union[TorrentFilesList, List[File]]]:
"""
获取种子文件
:param tid: 种子Hash
@@ -469,8 +523,8 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("torrent_files", tid=tid, downloader=downloader)
def media_exists(self, mediainfo: MediaInfo, itemid: str = None,
server: str = None) -> Optional[ExistMediaInfo]:
def media_exists(self, mediainfo: MediaInfo, itemid: Optional[str] = None,
server: Optional[str] = None) -> Optional[ExistMediaInfo]:
"""
判断媒体文件是否存在
:param mediainfo: 识别的媒体信息
@@ -488,13 +542,27 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("media_files", mediainfo=mediainfo)
def post_message(self, message: Notification) -> None:
def post_message(self,
message: Optional[Notification] = None,
meta: Optional[MetaBase] = None,
mediainfo: Optional[MediaInfo] = None,
torrentinfo: Optional[TorrentInfo] = None,
transferinfo: Optional[TransferInfo] = None,
**kwargs) -> None:
"""
发送消息
:param message: 消息体
:param message: Notification实例
:param meta: 元数据
:param mediainfo: 媒体信息
:param torrentinfo: 种子信息
:param transferinfo: 文件整理信息
:param kwargs: 其他参数(覆盖业务对象属性值)
:return: 成功或失败
"""
# 保存原消息
# 渲染消息
message = MessageTemplateHelper.render(message=message, meta=meta, mediainfo=mediainfo,
torrentinfo=torrentinfo, transferinfo=transferinfo, **kwargs)
# 保存消息
self.messagehelper.put(message, role="user", title=message.title)
self.messageoper.add(**message.dict())
# 发送消息按设置隔离
@@ -575,7 +643,8 @@ class ChainBase(metaclass=ABCMeta):
self.messageoper.add(**message.dict(), note=note_list)
return self.messagequeue.send_message("post_torrents_message", message=message, torrents=torrents)
def metadata_img(self, mediainfo: MediaInfo, season: int = None, episode: int = None) -> Optional[dict]:
def metadata_img(self, mediainfo: MediaInfo,
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[dict]:
"""
获取图片名称和url
:param mediainfo: 媒体信息

View File

@@ -9,13 +9,13 @@ class DashboardChain(ChainBase, metaclass=Singleton):
"""
各类仪表板统计处理链
"""
def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
def media_statistic(self, server: Optional[str] = None) -> Optional[List[schemas.Statistic]]:
"""
媒体数量统计
"""
return self.run_module("media_statistic", server=server)
def downloader_info(self, downloader: str = None) -> Optional[List[schemas.DownloaderInfo]]:
def downloader_info(self, downloader: Optional[str] = None) -> Optional[List[schemas.DownloaderInfo]]:
"""
下载器信息
"""

View File

@@ -19,7 +19,7 @@ class DoubanChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("douban_person_detail", person_id=person_id)
def person_credits(self, person_id: int, page: int = 1) -> List[MediaInfo]:
def person_credits(self, person_id: int, page: Optional[int] = 1) -> List[MediaInfo]:
"""
根据人物ID查询人物参演作品
:param person_id: 人物ID
@@ -27,7 +27,7 @@ class DoubanChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("douban_person_credits", person_id=person_id, page=page)
def movie_top250(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取豆瓣电影TOP250
:param page: 页码
@@ -35,26 +35,26 @@ class DoubanChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("movie_top250", page=page, count=count)
def movie_showing(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取正在上映的电影
"""
return self.run_module("movie_showing", page=page, count=count)
def tv_weekly_chinese(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取本周中国剧集榜
"""
return self.run_module("tv_weekly_chinese", page=page, count=count)
def tv_weekly_global(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取本周全球剧集榜
"""
return self.run_module("tv_weekly_global", page=page, count=count)
def douban_discover(self, mtype: MediaType, sort: str, tags: str,
page: int = 0, count: int = 30) -> Optional[List[MediaInfo]]:
page: Optional[int] = 0, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
发现豆瓣电影、剧集
:param mtype: 媒体类型
@@ -67,19 +67,19 @@ class DoubanChain(ChainBase, metaclass=Singleton):
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
page=page, count=count)
def tv_animation(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取动画剧集
"""
return self.run_module("tv_animation", page=page, count=count)
def movie_hot(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取热门电影
"""
return self.run_module("movie_hot", page=page, count=count)
def tv_hot(self, page: int = 1, count: int = 30) -> Optional[List[MediaInfo]]:
def tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[List[MediaInfo]]:
"""
获取热门剧集
"""

View File

@@ -20,7 +20,7 @@ from app.helper.message import MessageHelper
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification, ResourceSelectionEventData, ResourceDownloadEventData
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ChainEventType
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ContentType, ChainEventType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
@@ -38,66 +38,9 @@ class DownloadChain(ChainBase):
self.directoryhelper = DirectoryHelper()
self.messagehelper = MessageHelper()
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo,
channel: MessageChannel = None, username: str = None,
download_episodes: str = None):
"""
发送添加下载的消息,根据消息场景开关决定发给谁
:param meta: 元数据
:param mediainfo: 媒体信息
:param torrent: 种子信息
:param channel: 通知渠道
:param username: 通知显示的下载用户信息
:param download_episodes: 下载的集数
"""
# 拼装消息内容
msg_text = ""
if username:
msg_text = f"用户:{username}"
if torrent.site_name:
msg_text = f"{msg_text}\n站点:{torrent.site_name}"
if meta.resource_term:
msg_text = f"{msg_text}\n质量:{meta.resource_term}"
if torrent.size:
if str(torrent.size).replace(".", "").isdigit():
size = StringUtils.str_filesize(torrent.size)
else:
size = torrent.size
msg_text = f"{msg_text}\n大小:{size}"
if torrent.title:
msg_text = f"{msg_text}\n种子:{torrent.title}"
if torrent.pubdate:
msg_text = f"{msg_text}\n发布时间:{torrent.pubdate}"
if torrent.freedate:
msg_text = f"{msg_text}\n免费时间:{StringUtils.diff_time_str(torrent.freedate)}"
if torrent.seeders:
msg_text = f"{msg_text}\n做种数:{torrent.seeders}"
if torrent.uploadvolumefactor and torrent.downloadvolumefactor:
msg_text = f"{msg_text}\n促销:{torrent.volume_factor}"
if torrent.hit_and_run:
msg_text = f"{msg_text}\nHit&Run"
if torrent.labels:
msg_text = f"{msg_text}\n标签:{' '.join(torrent.labels)}"
if torrent.description:
html_re = re.compile(r'<[^>]+>', re.S)
description = html_re.sub('', torrent.description)
torrent.description = re.sub(r'<[^>]+>', '', description)
msg_text = f"{msg_text}\n描述:{torrent.description}"
# 下载成功按规则发送消息
self.post_message(Notification(
channel=channel,
mtype=NotificationType.Download,
title=f"{mediainfo.title_year} "
f"{'%s %s' % (meta.season, download_episodes) if download_episodes else meta.season_episode} 开始下载",
text=msg_text,
image=mediainfo.get_message_image(),
link=settings.MP_DOMAIN('/#/downloading'),
username=username))
def download_torrent(self, torrent: TorrentInfo,
channel: MessageChannel = None,
source: str = None,
source: Optional[str] = None,
userid: Union[str, int] = None
) -> Tuple[Optional[Union[Path, str]], str, list]:
"""
@@ -105,7 +48,7 @@ class DownloadChain(ChainBase):
:return: 种子路径,种子目录名,种子文件清单
"""
def __get_redict_url(url: str, ua: str = None, cookie: str = None) -> Optional[str]:
def __get_redict_url(url: str, ua: Optional[str] = None, cookie: Optional[str] = None) -> Optional[str]:
"""
获取下载链接, url格式[base64]url
"""
@@ -204,13 +147,12 @@ class DownloadChain(ChainBase):
def download_single(self, context: Context, torrent_file: Path = None,
episodes: Set[int] = None,
channel: MessageChannel = None,
source: str = None,
downloader: str = None,
save_path: str = None,
source: Optional[str] = None,
downloader: Optional[str] = None,
save_path: Optional[str] = None,
userid: Union[str, int] = None,
username: str = None,
media_category: str = None,
label: str = None) -> Optional[str]:
username: Optional[str] = None,
label: Optional[str] = None) -> Optional[str]:
"""
下载及发送通知
:param context: 资源上下文
@@ -222,9 +164,13 @@ class DownloadChain(ChainBase):
:param save_path: 保存路径
:param userid: 用户ID
:param username: 调用下载的用户名/插件名
:param media_category: 自定义媒体类别
:param label: 自定义标签
"""
_torrent = context.torrent_info
_media = context.media_info
_meta = context.meta_info
_site_downloader = _torrent.site_downloader
# 发送资源下载事件,允许外部拦截下载
event_data = ResourceDownloadEventData(
context=context,
@@ -236,7 +182,7 @@ class DownloadChain(ChainBase):
"save_path": save_path,
"userid": userid,
"username": username,
"media_category": media_category
"media_category": _media.category
}
)
# 触发资源下载事件
@@ -250,15 +196,11 @@ class DownloadChain(ChainBase):
f"Reason: {event_data.reason}")
return None
_torrent = context.torrent_info
_media = context.media_info
_meta = context.meta_info
_site_downloader = _torrent.site_downloader
# 补充完整的media数据
if not _media.genre_ids:
new_media = self.recognize_media(mtype=_media.type, tmdbid=_media.tmdb_id,
doubanid=_media.douban_id, bangumiid=_media.bangumi_id)
doubanid=_media.douban_id, bangumiid=_media.bangumi_id,
episode_group=_media.episode_group)
if new_media:
_media = new_media
@@ -355,7 +297,8 @@ class DownloadChain(ChainBase):
username=username,
channel=channel.value if channel else None,
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
media_category=media_category,
media_category=_media.category,
episode_group=_media.episode_group,
note={"source": source}
)
@@ -384,8 +327,20 @@ class DownloadChain(ChainBase):
self.downloadhis.add_files(files_to_add)
# 下载成功发送消息
self.post_download_message(meta=_meta, mediainfo=_media, torrent=_torrent,
username=username, download_episodes=download_episodes)
self.post_message(
Notification(
channel=channel,
mtype=NotificationType.Download,
ctype=ContentType.DownloadAdded,
image=_media.get_message_image(),
link=settings.MP_DOMAIN('/#/downloading'),
username=username
),
meta=_meta,
mediainfo=_media,
torrentinfo=_torrent,
download_episodes=download_episodes
)
# 下载成功后处理
self.download_added(context=context, download_dir=download_dir, torrent_path=torrent_file)
# 广播事件
@@ -418,13 +373,12 @@ class DownloadChain(ChainBase):
def batch_download(self,
contexts: List[Context],
no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]] = None,
save_path: str = None,
save_path: Optional[str] = None,
channel: MessageChannel = None,
source: str = None,
userid: str = None,
username: str = None,
media_category: str = None,
downloader: str = None
source: Optional[str] = None,
userid: Optional[str] = None,
username: Optional[str] = None,
downloader: Optional[str] = None
) -> Tuple[List[Context], Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
"""
根据缺失数据,自动种子列表中组合择优下载
@@ -435,7 +389,6 @@ class DownloadChain(ChainBase):
:param source: 来源(消息通知、订阅、手工下载等)
:param userid: 用户ID
:param username: 调用下载的用户名/插件名
:param media_category: 自定义媒体类别
:param downloader: 下载器
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id/douban_id] = {season: NotExistMediaInfo}
"""
@@ -524,7 +477,7 @@ class DownloadChain(ChainBase):
logger.info(f"开始下载电影 {context.torrent_info.title} ...")
if self.download_single(context, save_path=save_path, channel=channel,
source=source, userid=userid, username=username,
media_category=media_category, downloader=downloader):
downloader=downloader):
# 下载成功
logger.info(f"{context.torrent_info.title} 添加下载成功")
downloaded_list.append(context)
@@ -609,8 +562,7 @@ class DownloadChain(ChainBase):
source=source,
userid=userid,
username=username,
media_category=media_category,
downloader=downloader,
downloader=downloader
)
else:
# 下载
@@ -618,7 +570,6 @@ class DownloadChain(ChainBase):
download_id = self.download_single(context, save_path=save_path,
channel=channel, source=source,
userid=userid, username=username,
media_category=media_category,
downloader=downloader)
if download_id:
@@ -690,7 +641,6 @@ class DownloadChain(ChainBase):
download_id = self.download_single(context, save_path=save_path,
channel=channel, source=source,
userid=userid, username=username,
media_category=media_category,
downloader=downloader)
if download_id:
# 下载成功
@@ -780,7 +730,6 @@ class DownloadChain(ChainBase):
source=source,
userid=userid,
username=username,
media_category=media_category,
downloader=downloader
)
if not download_id:
@@ -866,7 +815,8 @@ class DownloadChain(ChainBase):
# 补充媒体信息
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id)
doubanid=mediainfo.douban_id,
episode_group=mediainfo.episode_group)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
return False, {}
@@ -933,7 +883,7 @@ class DownloadChain(ChainBase):
# 全部存在
return True, no_exists
def remote_downloading(self, channel: MessageChannel, userid: Union[str, int] = None, source: str = None):
def remote_downloading(self, channel: MessageChannel, userid: Union[str, int] = None, source: Optional[str] = None):
"""
查询正在下载的任务,并发送消息
"""
@@ -967,7 +917,7 @@ class DownloadChain(ChainBase):
link=settings.MP_DOMAIN('#/downloading')
))
def downloading(self, name: str = None) -> List[DownloadingTorrent]:
def downloading(self, name: Optional[str] = None) -> List[DownloadingTorrent]:
"""
查询正在下载的任务
"""

View File

@@ -32,7 +32,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
self.storagechain = StorageChain()
def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
season: int = None, episode: int = None) -> Optional[str]:
season: Optional[int] = None, episode: Optional[int] = None) -> Optional[str]:
"""
获取NFO文件内容文本
:param meta: 元数据
@@ -42,13 +42,13 @@ class MediaChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("metadata_nfo", meta=meta, mediainfo=mediainfo, season=season, episode=episode)
def recognize_by_meta(self, metainfo: MetaBase) -> Optional[MediaInfo]:
def recognize_by_meta(self, metainfo: MetaBase, episode_group: Optional[str] = None) -> Optional[MediaInfo]:
"""
根据主副标题识别媒体信息
"""
title = metainfo.title
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
mediainfo: MediaInfo = self.recognize_media(meta=metainfo, episode_group=episode_group)
if not mediainfo:
# 尝试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(ChainEventType.NameRecognize):
@@ -112,7 +112,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
# 重新识别
return self.recognize_media(meta=org_meta)
def recognize_by_path(self, path: str) -> Optional[Context]:
def recognize_by_path(self, path: str, episode_group: Optional[str] = None) -> Optional[Context]:
"""
根据文件路径识别媒体信息
"""
@@ -121,7 +121,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
# 元数据
file_meta = MetaInfoPath(file_path)
# 识别媒体信息
mediainfo = self.recognize_media(meta=file_meta)
mediainfo = self.recognize_media(meta=file_meta, episode_group=episode_group)
if not mediainfo:
# 尝试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(ChainEventType.NameRecognize):
@@ -238,7 +238,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
return None
def get_doubaninfo_by_tmdbid(self, tmdbid: int,
mtype: MediaType = None, season: int = None) -> Optional[dict]:
mtype: MediaType = None, season: Optional[int] = None) -> Optional[dict]:
"""
根据TMDBID获取豆瓣信息
"""
@@ -375,7 +375,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
if item:
logger.info(f"已保存文件:{item.path}")
else:
logger.warn(f"文件保存失败:{item.path}")
logger.warn(f"文件保存失败:{_path}")
finally:
if tmp_file.exists():
tmp_file.unlink()
@@ -474,7 +474,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
if not file_meta.begin_episode:
logger.warn(f"{filepath.name} 无法识别文件集数!")
return
file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id)
file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id,
episode_group=mediainfo.episode_group)
if not file_mediainfo:
logger.warn(f"{filepath.name} 无法识别文件媒体信息!")
return
@@ -483,7 +484,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
if overwrite or not self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
# 获取集的nfo文件
episode_nfo = self.metadata_nfo(meta=file_meta, mediainfo=file_mediainfo,
season=file_meta.begin_season, episode=file_meta.begin_episode)
season=file_meta.begin_season,
episode=file_meta.begin_episode)
if episode_nfo:
# 保存或上传nfo文件到上级目录
if not parent:

View File

@@ -21,14 +21,15 @@ class MediaServerChain(ChainBase):
super().__init__()
self.dboper = MediaServerOper()
def librarys(self, server: str, username: str = None, hidden: bool = False) -> List[MediaServerLibrary]:
def librarys(self, server: str, username: Optional[str] = None,
hidden: bool = False) -> List[MediaServerLibrary]:
"""
获取媒体服务器所有媒体库
"""
return self.run_module("mediaserver_librarys", server=server, username=username, hidden=hidden)
def items(self, server: str, library_id: Union[str, int],
start_index: int = 0, limit: Optional[int] = -1) -> Generator[Any, None, None]:
start_index: Optional[int] = 0, limit: Optional[int] = -1) -> Generator[Any, None, None]:
"""
获取媒体服务器项目列表,支持分页和不分页逻辑,默认不分页获取所有数据
@@ -81,28 +82,31 @@ class MediaServerChain(ChainBase):
"""
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
def playing(self, server: str, count: int = 20, username: str = None) -> List[MediaServerPlayItem]:
def playing(self, server: str, count: Optional[int] = 20,
username: Optional[str] = None) -> List[MediaServerPlayItem]:
"""
获取媒体服务器正在播放信息
"""
return self.run_module("mediaserver_playing", count=count, server=server, username=username)
def latest(self, server: str, count: int = 20, username: str = None) -> List[MediaServerPlayItem]:
def latest(self, server: str, count: Optional[int] = 20,
username: Optional[str] = None) -> List[MediaServerPlayItem]:
"""
获取媒体服务器最新入库条目
"""
return self.run_module("mediaserver_latest", count=count, server=server, username=username)
@cached(maxsize=1, ttl=3600)
def get_latest_wallpapers(self, server: str = None, count: int = 10,
remote: bool = True, username: str = None) -> List[str]:
def get_latest_wallpapers(self, server: Optional[str] = None, count: Optional[int] = 10,
remote: bool = True, username: Optional[str] = None) -> List[str]:
"""
获取最新最新入库条目海报作为壁纸缓存1小时
"""
return self.run_module("mediaserver_latest_images", server=server, count=count,
remote=remote, username=username)
def get_latest_wallpaper(self, server: str = None, remote: bool = True, username: str = None) -> Optional[str]:
def get_latest_wallpaper(self, server: Optional[str] = None,
remote: bool = True, username: Optional[str] = None) -> Optional[str]:
"""
获取最新最新入库条目海报作为壁纸缓存1小时
"""

View File

@@ -1,7 +1,7 @@
import io
import tempfile
from pathlib import Path
from typing import List
from typing import List, Optional
import pillow_avif # noqa 用于自动注册AVIF支持
from PIL import Image
@@ -162,15 +162,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def tmdb_movies(self, sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1) -> List[dict]:
def tmdb_movies(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1) -> List[dict]:
"""
TMDB热门电影
"""
@@ -188,15 +188,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def tmdb_tvs(self, sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "zh|en|ja|ko",
with_keywords: str = "",
with_watch_providers: str = "",
vote_average: float = 0,
vote_count: int = 0,
release_date: str = "",
page: int = 1) -> List[dict]:
def tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc",
with_genres: Optional[str] = "",
with_original_language: Optional[str] = "zh|en|ja|ko",
with_keywords: Optional[str] = "",
with_watch_providers: Optional[str] = "",
vote_average: Optional[float] = 0.0,
vote_count: Optional[int] = 0,
release_date: Optional[str] = "",
page: Optional[int] = 1) -> List[dict]:
"""
TMDB热门电视剧
"""
@@ -214,7 +214,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def tmdb_trending(self, page: int = 1) -> List[dict]:
def tmdb_trending(self, page: Optional[int] = 1) -> List[dict]:
"""
TMDB流行趋势
"""
@@ -223,7 +223,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def bangumi_calendar(self, page: int = 1, count: int = 30) -> List[dict]:
def bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
Bangumi每日放送
"""
@@ -232,7 +232,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_movie_showing(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣正在热映
"""
@@ -241,7 +241,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_movies(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> List[dict]:
def douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣最新电影
"""
@@ -251,7 +252,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_tvs(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> List[dict]:
def douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "",
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣最新电视剧
"""
@@ -261,7 +263,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_movie_top250(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣电影TOP250
"""
@@ -270,7 +272,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_tv_weekly_chinese(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣国产剧集榜
"""
@@ -279,7 +281,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_tv_weekly_global(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣全球剧集榜
"""
@@ -288,7 +290,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣热门动漫
"""
@@ -297,7 +299,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_movie_hot(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣热门电影
"""
@@ -306,7 +308,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
@log_execution_time(logger=logger)
@cached(ttl=recommend_ttl, region=recommend_cache_region)
def douban_tv_hot(self, page: int = 1, count: int = 30) -> List[dict]:
def douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
豆瓣热门电视剧
"""

View File

@@ -34,9 +34,9 @@ class SearchChain(ChainBase):
self.systemconfig = SystemConfigOper()
self.torrenthelper = TorrentHelper()
def search_by_id(self, tmdbid: int = None, doubanid: str = None,
mtype: MediaType = None, area: str = "title", season: int = None,
sites: List[int] = None) -> List[Context]:
def search_by_id(self, tmdbid: Optional[int] = None, doubanid: Optional[str] = None,
mtype: MediaType = None, area: Optional[str] = "title", season: Optional[int] = None,
sites: List[int] = None, cache_local: bool = False) -> List[Context]:
"""
根据TMDBID/豆瓣ID搜索资源精确匹配不过滤本地存在的资源
:param tmdbid: TMDB ID
@@ -45,6 +45,7 @@ class SearchChain(ChainBase):
:param area: 搜索范围title or imdbid
:param season: 季数
:param sites: 站点ID列表
:param cache_local: 是否缓存到本地
"""
mediainfo = self.recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype)
if not mediainfo:
@@ -59,12 +60,12 @@ class SearchChain(ChainBase):
}
results = self.process(mediainfo=mediainfo, sites=sites, area=area, no_exists=no_exists)
# 保存到本地文件
bytes_results = pickle.dumps(results)
self.save_cache(bytes_results, self.__result_temp_file)
if cache_local:
self.save_cache(pickle.dumps(results), self.__result_temp_file)
return results
def search_by_title(self, title: str, page: int = 0,
sites: List[int] = None, cache_local: bool = True) -> List[Context]:
def search_by_title(self, title: str, page: Optional[int] = 0,
sites: List[int] = None, cache_local: Optional[bool] = False) -> List[Context]:
"""
根据标题搜索资源,不识别不过滤,直接返回站点内容
:param title: 标题,为空时返回所有站点首页内容
@@ -86,8 +87,7 @@ class SearchChain(ChainBase):
torrent_info=torrent) for torrent in torrents]
# 保存到本地文件
if cache_local:
bytes_results = pickle.dumps(contexts)
self.save_cache(bytes_results, self.__result_temp_file)
self.save_cache(pickle.dumps(contexts), self.__result_temp_file)
return contexts
def last_search_results(self) -> List[Context]:
@@ -105,11 +105,11 @@ class SearchChain(ChainBase):
return []
def process(self, mediainfo: MediaInfo,
keyword: str = None,
keyword: Optional[str] = None,
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
sites: List[int] = None,
rule_groups: List[str] = None,
area: str = "title",
area: Optional[str] = "title",
custom_words: List[str] = None,
filter_params: Dict[str, str] = None) -> List[Context]:
"""
@@ -291,8 +291,8 @@ class SearchChain(ChainBase):
def __search_all_sites(self, keywords: List[str],
mediainfo: Optional[MediaInfo] = None,
sites: List[int] = None,
page: int = 0,
area: str = "title") -> Optional[List[TorrentInfo]]:
page: Optional[int] = 0,
area: Optional[str] = "title") -> Optional[List[TorrentInfo]]:
"""
多线程搜索多个站点
:param mediainfo: 识别的媒体信息

View File

@@ -1,7 +1,6 @@
import base64
import re
from datetime import datetime
from time import time
from typing import Optional, Tuple, Union, Dict
from urllib.parse import urljoin
@@ -178,12 +177,9 @@ class SiteChain(ChainBase):
domain = StringUtils.get_url_domain(site.url)
url = f"https://api.{domain}/api/member/profile"
headers = {
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "application/json, text/plain, */*",
"Authorization": site.token,
"x-api-key": site.apikey,
"ts": str(int(time()))
}
res = RequestUtils(
headers=headers,
@@ -193,27 +189,10 @@ class SiteChain(ChainBase):
if res is None:
return False, "无法打开网站!"
if res.status_code == 200:
state = False
message = "鉴权已过期或无效"
user_info = res.json() or {}
if user_info.get("data"):
# 更新最后访问时间
del headers["x-api-key"]
res = RequestUtils(headers=headers,
timeout=site.timeout or 15,
proxies=settings.PROXY if site.proxy else None,
referer=f"{site.url}index"
).post_res(url=f"https://api.{domain}/api/member/updateLastBrowse")
state = True
message = "连接成功,但更新状态失败"
if res and res.status_code == 200:
update_info = res.json() or {}
if "code" in update_info and int(update_info["code"]) == 0:
message = "连接成功"
elif user_info.get("message"):
# 使用馒头的错误提示
message = user_info.get("message")
return state, message
return True, "连接成功"
return False, user_info.get("message", "鉴权已过期或无效")
else:
return False, f"错误:{res.status_code} {res.reason}"
@@ -318,7 +297,7 @@ class SiteChain(ChainBase):
"""
if StringUtils.get_url_domain(inx.get("domain")) == sub_domain:
return inx.get("domain")
for ext_d in inx.get("ext_domains"):
for ext_d in inx.get("ext_domains", []):
if StringUtils.get_url_domain(ext_d) == sub_domain:
return ext_d
return sub_domain
@@ -610,7 +589,7 @@ class SiteChain(ChainBase):
return True, "连接成功"
def remote_list(self, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
查询所有站点,发送消息
"""
@@ -644,7 +623,7 @@ class SiteChain(ChainBase):
)
def remote_disable(self, arg_str: str, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
禁用站点
"""
@@ -669,7 +648,7 @@ class SiteChain(ChainBase):
self.remote_list(channel=channel, userid=userid, source=source)
def remote_enable(self, arg_str: str, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
启用站点
"""
@@ -695,7 +674,7 @@ class SiteChain(ChainBase):
self.remote_list(channel=channel, userid=userid, source=source)
def update_cookie(self, site_info: Site,
username: str, password: str, two_step_code: str = None) -> Tuple[bool, str]:
username: str, password: str, two_step_code: Optional[str] = None) -> Tuple[bool, str]:
"""
根据用户名密码更新站点Cookie
:param site_info: 站点信息
@@ -724,7 +703,7 @@ class SiteChain(ChainBase):
return False, "未知错误"
def remote_cookie(self, arg_str: str, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
使用用户名密码更新站点Cookie
"""
@@ -794,7 +773,7 @@ class SiteChain(ChainBase):
userid=userid))
def remote_refresh_userdatas(self, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
刷新所有站点用户数据
"""

View File

@@ -63,7 +63,7 @@ class StorageChain(ChainBase):
return self.run_module("download_file", fileitem=fileitem, path=path)
def upload_file(self, fileitem: schemas.FileItem, path: Path,
new_name: str = None) -> Optional[schemas.FileItem]:
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
"""
上传文件
:param fileitem: 保存目录项

View File

@@ -29,7 +29,7 @@ from app.helper.subscribe import SubscribeHelper
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas import MediaRecognizeConvertEventData
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType, ChainEventType
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType, ChainEventType, ContentType
from app.utils.singleton import Singleton
@@ -56,17 +56,18 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
def add(self, title: str, year: str,
mtype: MediaType = None,
tmdbid: int = None,
doubanid: str = None,
bangumiid: int = None,
mediaid: str = None,
season: int = None,
tmdbid: Optional[int] = None,
doubanid: Optional[str] = None,
bangumiid: Optional[int] = None,
mediaid: Optional[str] = None,
episode_group: Optional[str] = None,
season: Optional[int] = None,
channel: MessageChannel = None,
source: str = None,
userid: str = None,
username: str = None,
message: bool = True,
exist_ok: bool = False,
source: Optional[str] = None,
userid: Optional[str] = None,
username: Optional[str] = None,
message: Optional[bool] = True,
exist_ok: Optional[bool] = False,
**kwargs) -> Tuple[Optional[int], str]:
"""
识别媒体信息并添加订阅
@@ -117,7 +118,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo = __get_event_meida(mediaid, metainfo)
else:
# 使用TMDBID识别
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, cache=False)
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid,
episode_group=episode_group, cache=False)
else:
if doubanid:
# 豆瓣识别模式,不使用缓存
@@ -134,7 +136,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
# 使用名称识别兜底
if not mediainfo:
mediainfo = self.recognize_media(meta=metainfo)
mediainfo = self.recognize_media(meta=metainfo, episode_group=episode_group)
# 识别失败
if not mediainfo:
@@ -147,12 +149,13 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
season = 1
# 总集数
if not kwargs.get('total_episode'):
if not mediainfo.seasons:
if not mediainfo.seasons or episode_group:
# 补充媒体信息
mediainfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id,
bangumiid=mediainfo.bangumi_id,
episode_group=episode_group,
cache=False)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
@@ -207,8 +210,9 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
'save_path': self.__get_default_subscribe_config(mediainfo.type, "save_path") if not kwargs.get(
"save_path") else kwargs.get("save_path"),
'filter_groups': self.__get_default_subscribe_config(mediainfo.type, "filter_groups") if not kwargs.get(
"filter_groups") else kwargs.get("filter_groups"),
"filter_groups") else kwargs.get("filter_groups")
})
# 操作数据库
sid, err_msg = self.subscribeoper.add(mediainfo=mediainfo, season=season, username=username, **kwargs)
if not sid:
logger.error(f'{mediainfo.title_year} {err_msg}')
@@ -224,22 +228,22 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
userid=userid))
return None, err_msg
elif message:
logger.info(f'{mediainfo.title_year} {metainfo.season} 添加订阅成功')
if username:
text = f"评分:{mediainfo.vote_average},来自用户:{username}"
else:
text = f"评分:{mediainfo.vote_average}"
if mediainfo.type == MediaType.TV:
link = settings.MP_DOMAIN('#/subscribe/tv?tab=mysub')
else:
link = settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
# 订阅成功按规则发送消息
self.post_message(schemas.Notification(mtype=NotificationType.Subscribe,
title=f"{mediainfo.title_year} {metainfo.season} 已添加订阅",
text=text,
image=mediainfo.get_message_image(),
link=link,
username=username))
self.post_message(
schemas.Notification(
mtype=NotificationType.Subscribe,
ctype=ContentType.SubscribeAdded,
image=mediainfo.get_message_image(),
link=link,
username=username
),
mediainfo=mediainfo,
username=username
)
# 发送事件
EventManager().send_event(EventType.SubscribeAdded, {
"subscribe_id": sid,
@@ -275,7 +279,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
return True
return False
def search(self, sid: int = None, state: str = 'N', manual: bool = False):
def search(self, sid: Optional[int] = None, state: Optional[str] = 'N', manual: Optional[bool] = False):
"""
订阅搜索
:param sid: 订阅ID有值时只处理该订阅
@@ -323,6 +327,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -330,7 +335,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
continue
# 如果媒体已存在或已下载完毕,跳过当前订阅处理
exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe, meta=meta,
exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe,
meta=meta,
mediainfo=mediainfo,
mediakey=mediakey)
if exist_flag:
@@ -382,6 +388,11 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
logger.info(
f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
continue
# 更新订阅自定义属性
if subscribe.media_category:
torrent_mediainfo.category = subscribe.media_category
if subscribe.episode_group:
torrent_mediainfo.episode_group = subscribe.episode_group
matched_contexts.append(context)
if not matched_contexts:
@@ -397,7 +408,6 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
userid=subscribe.username,
username=subscribe.username,
save_path=subscribe.save_path,
media_category=subscribe.media_category,
downloader=subscribe.downloader,
source=self.get_subscribe_source_keyword(subscribe)
)
@@ -426,7 +436,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
logger.debug(f"search Lock released at {datetime.now()}")
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaBase,
mediainfo: MediaInfo, downloads: List[Context]):
mediainfo: MediaInfo, downloads: Optional[List[Context]]):
"""
更新订阅已下载资源的优先级
"""
@@ -451,7 +461,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaBase, mediainfo: MediaInfo,
downloads: List[Context] = None,
lefts: Dict[Union[int | str], Dict[int, schemas.NotExistMediaInfo]] = None,
force: bool = False):
force: Optional[bool] = False):
"""
判断是否应完成订阅
"""
@@ -573,6 +583,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -602,9 +613,10 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
logger.debug(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...')
for context in contexts:
# 提取信息
torrent_meta = copy.deepcopy(context.meta_info)
torrent_mediainfo = copy.deepcopy(context.media_info)
torrent_info = context.torrent_info
_context = copy.deepcopy(context)
torrent_meta = _context.meta_info
torrent_mediainfo = _context.media_info
torrent_info = _context.torrent_info
# 不在订阅站点范围的不处理
sub_sites = self.get_sub_sites(subscribe)
@@ -632,7 +644,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
if not torrent_mediainfo \
or (not torrent_mediainfo.tmdb_id and not torrent_mediainfo.douban_id):
# 重新识别媒体信息
torrent_mediainfo = self.recognize_media(meta=torrent_meta)
torrent_mediainfo = self.recognize_media(meta=torrent_meta,
episode_group=subscribe.episode_group)
if torrent_mediainfo:
# 更新种子缓存
context.media_info = torrent_mediainfo
@@ -735,7 +748,12 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
# 匹配成功
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
_match_context.append(context)
# 自定义属性
if subscribe.media_category:
torrent_mediainfo.category = subscribe.media_category
if subscribe.episode_group:
torrent_mediainfo.episode_group = subscribe.episode_group
_match_context.append(_context)
if not _match_context:
# 未匹配到资源
@@ -751,7 +769,6 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
userid=subscribe.username,
username=subscribe.username,
save_path=subscribe.save_path,
media_category=subscribe.media_category,
downloader=subscribe.downloader,
source=self.get_subscribe_source_keyword(subscribe)
)
@@ -792,6 +809,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -884,7 +902,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
logger.error(f'follow用户分享订阅 {title} 添加失败:{message}')
logger.info(f'follow用户分享订阅刷新完成共添加 {success_count} 个订阅')
def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]):
def __update_subscribe_note(self, subscribe: Subscribe, downloads: Optional[List[Context]]):
"""
更新已下载信息到note字段
"""
@@ -943,7 +961,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]],
subscribe: Subscribe,
mediainfo: MediaInfo,
update_date: bool = False):
update_date: Optional[bool] = False):
"""
更新订阅剩余集数及时间
"""
@@ -995,11 +1013,18 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
else:
link = settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
# 完成订阅按规则发送消息
self.post_message(schemas.Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成{msgstr}',
image=mediainfo.get_message_image(),
link=link,
username=subscribe.username))
self.post_message(
schemas.Notification(
mtype=NotificationType.Subscribe,
ctype=ContentType.SubscribeComplete,
image=mediainfo.get_message_image(),
link=link,
username=subscribe.username
),
meta=meta,
mediainfo=mediainfo,
msgstr=msgstr
)
# 发送事件
EventManager().send_event(EventType.SubscribeComplete, {
"subscribe_id": subscribe.id,
@@ -1013,7 +1038,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
})
def remote_list(self, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
查询订阅并发送消息
"""
@@ -1041,7 +1066,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
title=title, text='\n'.join(messages), userid=userid))
def remote_delete(self, arg_str: str, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
删除订阅
"""
@@ -1076,8 +1101,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
no_exists: Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]],
mediakey: Union[str, int],
begin_season: int,
total_episode: int,
start_episode: int,
total_episode: Optional[int],
start_episode: Optional[int],
downloaded_episodes: List[int] = None
) -> Tuple[bool, Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]]]:
"""
@@ -1273,7 +1298,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
# 查询TMDB中的集信息
tmdb_episodes = self.tmdbchain.tmdb_episodes(
tmdbid=subscribe.tmdbid,
season=subscribe.season
season=subscribe.season,
episode_group=subscribe.episode_group
)
if tmdb_episodes:
for episode in tmdb_episodes:
@@ -1335,6 +1361,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
episode_group=subscribe.episode_group,
cache=False)
if not mediainfo:
logger.warn(
@@ -1368,7 +1395,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
return subscribe_info
def check_and_handle_existing_media(self, subscribe: Subscribe, meta: MetaBase,
mediainfo: MediaInfo, mediakey: str):
mediainfo: MediaInfo, mediakey: Union[str, int]):
"""
检查媒体是否已经存在,并根据情况执行相应的操作
1. 查询缺失的媒体信息

View File

@@ -1,7 +1,7 @@
import json
import re
from pathlib import Path
from typing import Union
from typing import Union, Optional
from app.chain import ChainBase
from app.core.config import settings
@@ -25,7 +25,7 @@ class SystemChain(ChainBase, metaclass=Singleton):
# 重启完成检测
self.restart_finish()
def remote_clear_cache(self, channel: MessageChannel, userid: Union[int, str], source: str = None):
def remote_clear_cache(self, channel: MessageChannel, userid: Union[int, str], source: Optional[str] = None):
"""
清理系统缓存
"""
@@ -33,7 +33,7 @@ class SystemChain(ChainBase, metaclass=Singleton):
self.post_message(Notification(channel=channel, source=source,
title=f"缓存清理完成!", userid=userid))
def restart(self, channel: MessageChannel, userid: Union[int, str], source: str = None):
def restart(self, channel: MessageChannel, userid: Union[int, str], source: Optional[str] = None):
"""
重启系统
"""
@@ -65,7 +65,7 @@ class SystemChain(ChainBase, metaclass=Singleton):
title += f"当前前端版本:{front_local_version},远程版本:{front_release_version}"
return title
def version(self, channel: MessageChannel, userid: Union[int, str], source: str = None):
def version(self, channel: MessageChannel, userid: Union[int, str], source: Optional[str] = None):
"""
查看当前版本、远程版本
"""

View File

@@ -23,7 +23,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
vote_average: float,
vote_count: int,
release_date: str,
page: int = 1) -> Optional[List[MediaInfo]]:
page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
"""
:param mtype: 媒体类型
:param sort_by: 排序方式
@@ -48,7 +48,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
release_date=release_date,
page=page)
def tmdb_trending(self, page: int = 1) -> Optional[List[MediaInfo]]:
def tmdb_trending(self, page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
"""
TMDB流行趋势
:param page: 第几页
@@ -70,13 +70,21 @@ class TmdbChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("tmdb_seasons", tmdbid=tmdbid)
def tmdb_episodes(self, tmdbid: int, season: int) -> List[schemas.TmdbEpisode]:
def tmdb_group_seasons(self, group_id: str) -> List[schemas.TmdbSeason]:
"""
根据剧集组ID查询themoviedb所有季集信息
:param group_id: 剧集组ID
"""
return self.run_module("tmdb_group_seasons", group_id=group_id)
def tmdb_episodes(self, tmdbid: int, season: int, episode_group: Optional[str] = None) -> List[schemas.TmdbEpisode]:
"""
根据TMDBID查询某季的所有信信息
:param tmdbid: TMDBID
:param season: 季
:param episode_group: 剧集组
"""
return self.run_module("tmdb_episodes", tmdbid=tmdbid, season=season)
return self.run_module("tmdb_episodes", tmdbid=tmdbid, season=season, episode_group=episode_group)
def movie_similar(self, tmdbid: int) -> Optional[List[MediaInfo]]:
"""
@@ -106,7 +114,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("tmdb_tv_recommend", tmdbid=tmdbid)
def movie_credits(self, tmdbid: int, page: int = 1) -> Optional[List[schemas.MediaPerson]]:
def movie_credits(self, tmdbid: int, page: Optional[int] = 1) -> Optional[List[schemas.MediaPerson]]:
"""
根据TMDBID查询电影演职人员
:param tmdbid: TMDBID
@@ -114,7 +122,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("tmdb_movie_credits", tmdbid=tmdbid, page=page)
def tv_credits(self, tmdbid: int, page: int = 1) -> Optional[List[schemas.MediaPerson]]:
def tv_credits(self, tmdbid: int, page: Optional[int] = 1) -> Optional[List[schemas.MediaPerson]]:
"""
根据TMDBID查询电视剧演职人员
:param tmdbid: TMDBID
@@ -129,7 +137,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("tmdb_person_detail", person_id=person_id)
def person_credits(self, person_id: int, page: int = 1) -> Optional[List[MediaInfo]]:
def person_credits(self, person_id: int, page: Optional[int] = 1) -> Optional[List[MediaInfo]]:
"""
根据人物ID查询人物参演作品
:param person_id: 人物ID
@@ -152,7 +160,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
return None
@cached(maxsize=1, ttl=3600)
def get_trending_wallpapers(self, num: int = 10) -> List[str]:
def get_trending_wallpapers(self, num: Optional[int] = 10) -> List[str]:
"""
获取所有流行壁纸
"""

View File

@@ -1,6 +1,6 @@
import re
import traceback
from typing import Dict, List, Union
from typing import Dict, List, Union, Optional
from cachetools import cached, TTLCache
@@ -48,7 +48,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
self.post_message(Notification(channel=channel,
title=f"种子刷新完成!", userid=userid))
def get_torrents(self, stype: str = None) -> Dict[str, List[Context]]:
def get_torrents(self, stype: Optional[str] = None) -> Dict[str, List[Context]]:
"""
获取当前缓存的种子
:param stype: 强制指定缓存类型spider:爬虫缓存rss:rss缓存
@@ -73,7 +73,8 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
logger.info(f'种子缓存数据清理完成')
@cached(cache=TTLCache(maxsize=128, ttl=595))
def browse(self, domain: str, keyword: str = None, cat: str = None, page: int = 0) -> List[TorrentInfo]:
def browse(self, domain: str, keyword: Optional[str] = None, cat: Optional[str] = None,
page: Optional[int] = 0) -> List[TorrentInfo]:
"""
浏览站点首页内容返回种子清单TTL缓存10分钟
:param domain: 站点域名
@@ -134,7 +135,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
return ret_torrents
def refresh(self, stype: str = None, sites: List[int] = None) -> Dict[str, List[Context]]:
def refresh(self, stype: Optional[str] = None, sites: List[int] = None) -> Dict[str, List[Context]]:
"""
刷新站点最新资源,识别并缓存起来
:param stype: 强制指定缓存类型spider:爬虫缓存rss:rss缓存

View File

@@ -17,6 +17,7 @@ from app.core.config import settings, global_vars
from app.core.context import MediaInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfoPath
from app.core.event import eventmanager
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory
from app.db.models.transferhistory import TransferHistory
@@ -29,7 +30,8 @@ from app.log import logger
from app.schemas import TransferInfo, TransferTorrent, Notification, EpisodeFormat, FileItem, TransferDirectoryConf, \
TransferTask, TransferQueue, TransferJob, TransferJobTask
from app.schemas.types import TorrentStatus, EventType, MediaType, ProgressKey, NotificationType, MessageChannel, \
SystemConfigKey
SystemConfigKey, ChainEventType, ContentType
from app.schemas import StorageOperSelectionEventData
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
@@ -53,14 +55,14 @@ class JobManager:
self._season_episodes = {}
@staticmethod
def __get_meta_id(meta: MetaBase = None, season: int = None) -> Tuple:
def __get_meta_id(meta: MetaBase = None, season: Optional[int] = None) -> Tuple:
"""
获取元数据ID
"""
return meta.name, season
@staticmethod
def __get_media_id(media: MediaInfo = None, season: int = None) -> Tuple:
def __get_media_id(media: MediaInfo = None, season: Optional[int] = None) -> Tuple:
"""
获取媒体ID
"""
@@ -104,7 +106,7 @@ class JobManager:
"""
return schemas.MetaInfo(**task.meta.to_dict())
def add_task(self, task: TransferTask, state: str = "waiting"):
def add_task(self, task: TransferTask, state: Optional[str] = "waiting"):
"""
添加整理任务
"""
@@ -296,7 +298,7 @@ class JobManager:
media_success = True
return meta_success and media_success
def success_tasks(self, media: MediaInfo, season: int = None) -> List[TransferJobTask]:
def success_tasks(self, media: MediaInfo, season: Optional[int] = None) -> List[TransferJobTask]:
"""
获取某项任务成功的任务
"""
@@ -306,7 +308,7 @@ class JobManager:
return []
return [task for task in self._job_view[__mediaid__].tasks if task.state == "completed"]
def count(self, media: MediaInfo, season: int = None) -> int:
def count(self, media: MediaInfo, season: Optional[int] = None) -> int:
"""
获取某项任务总数
"""
@@ -317,7 +319,7 @@ class JobManager:
return 0
return len([task for task in self._job_view[__mediaid__].tasks if task.state == "completed"])
def size(self, media: MediaInfo, season: int = None) -> int:
def size(self, media: MediaInfo, season: Optional[int] = None) -> int:
"""
获取某项任务总大小
"""
@@ -341,7 +343,7 @@ class JobManager:
"""
return list(self._job_view.values())
def season_episodes(self, media: MediaInfo, season: int = None) -> List[int]:
def season_episodes(self, media: MediaInfo, season: Optional[int] = None) -> List[int]:
"""
获取季集清单
"""
@@ -623,7 +625,8 @@ class TransferChain(ChainBase, metaclass=Singleton):
# 下载记录中已存在识别信息
mediainfo: Optional[MediaInfo] = self.recognize_media(mtype=MediaType(download_history.type),
tmdbid=download_history.tmdbid,
doubanid=download_history.doubanid)
doubanid=download_history.doubanid,
episode_group=download_history.episode_group)
if mediainfo:
# 更新自定义媒体类别
if download_history.media_category:
@@ -681,7 +684,8 @@ class TransferChain(ChainBase, metaclass=Singleton):
season_num = 1
task.episodes_info = self.tmdbchain.tmdb_episodes(
tmdbid=task.mediainfo.tmdb_id,
season=season_num
season=season_num,
episode_group=task.mediainfo.episode_group
)
# 查询整理目标目录
@@ -697,10 +701,36 @@ class TransferChain(ChainBase, metaclass=Singleton):
storage=task.fileitem.storage,
src_path=Path(task.fileitem.path),
target_storage=task.target_storage)
if not task.target_storage and task.target_directory:
task.target_storage = task.target_directory.library_storage
# 正在处理
self.jobview.running_task(task)
# 广播事件,请示额外的源存储支持
source_oper = None
source_event_data = StorageOperSelectionEventData(
storage=task.fileitem.storage,
)
source_event = eventmanager.send_event(ChainEventType.StorageOperSelection, source_event_data)
# 使用事件返回的上下文数据
if source_event and source_event.event_data:
source_event_data: StorageOperSelectionEventData = source_event.event_data
if source_event_data.storage_oper:
source_oper = source_event_data.storage_oper
# 广播事件,请示额外的目标存储支持
target_oper = None
target_event_data = StorageOperSelectionEventData(
storage=task.target_storage,
)
target_event = eventmanager.send_event(ChainEventType.StorageOperSelection, target_event_data)
# 使用事件返回的上下文数据
if target_event and target_event.event_data:
target_event_data: StorageOperSelectionEventData = target_event.event_data
if target_event_data.storage_oper:
target_oper = target_event_data.storage_oper
# 执行整理
transferinfo: TransferInfo = self.transfer(fileitem=task.fileitem,
meta=task.meta,
@@ -712,7 +742,9 @@ class TransferChain(ChainBase, metaclass=Singleton):
episodes_info=task.episodes_info,
scrape=task.scrape,
library_type_folder=task.library_type_folder,
library_category_folder=task.library_category_folder)
library_category_folder=task.library_category_folder,
source_oper=source_oper,
target_oper=target_oper)
if not transferinfo:
logger.error("文件整理模块运行失败")
return False, "文件整理模块运行失败"
@@ -798,7 +830,8 @@ class TransferChain(ChainBase, metaclass=Singleton):
# 按TMDBID识别
mediainfo = self.recognize_media(mtype=mtype,
tmdbid=downloadhis.tmdbid,
doubanid=downloadhis.doubanid)
doubanid=downloadhis.doubanid,
episode_group=downloadhis.episode_group)
if mediainfo:
# 补充图片
self.obtain_images(mediainfo)
@@ -907,13 +940,13 @@ class TransferChain(ChainBase, metaclass=Singleton):
def do_transfer(self, fileitem: FileItem,
meta: MetaBase = None, mediainfo: MediaInfo = None,
target_directory: TransferDirectoryConf = None,
target_storage: str = None, target_path: Path = None,
transfer_type: str = None, scrape: bool = None,
library_type_folder: bool = None, library_category_folder: bool = None,
season: int = None, epformat: EpisodeFormat = None, min_filesize: int = 0,
downloader: str = None, download_hash: str = None,
force: bool = False, background: bool = True,
manual: bool = False, continue_callback: Callable = None) -> Tuple[bool, str]:
target_storage: Optional[str] = None, target_path: Path = None,
transfer_type: Optional[str] = None, scrape: Optional[bool] = None,
library_type_folder: Optional[bool] = None, library_category_folder: Optional[bool] = None,
season: Optional[int] = None, epformat: EpisodeFormat = None, min_filesize: Optional[int] = 0,
downloader: Optional[str] = None, download_hash: Optional[str] = None,
force: Optional[bool] = False, background: Optional[bool] = True,
manual: Optional[bool] = False, continue_callback: Callable = None) -> Tuple[bool, str]:
"""
执行一个复杂目录的整理操作
:param fileitem: 文件项
@@ -1153,7 +1186,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
return all_success, "".join(err_msgs)
def remote_transfer(self, arg_str: str, channel: MessageChannel,
userid: Union[str, int] = None, source: str = None):
userid: Union[str, int] = None, source: Optional[str] = None):
"""
远程重新整理,参数 历史记录ID TMDBID|类型
"""
@@ -1195,7 +1228,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
return
def __re_transfer(self, logid: int, mtype: MediaType = None,
mediaid: str = None) -> Tuple[bool, str]:
mediaid: Optional[str] = None) -> Tuple[bool, str]:
"""
根据历史记录,重新识别整理,只支持简单条件
:param logid: 历史记录ID
@@ -1214,12 +1247,12 @@ class TransferChain(ChainBase, metaclass=Singleton):
# 查询媒体信息
if mtype and mediaid:
mediainfo = self.recognize_media(mtype=mtype, tmdbid=int(mediaid) if str(mediaid).isdigit() else None,
doubanid=mediaid)
doubanid=mediaid, episode_group=history.episode_group)
if mediainfo:
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
else:
mediainfo = self.mediachain.recognize_by_path(str(src_path))
mediainfo = self.mediachain.recognize_by_path(str(src_path), episode_group=history.episode_group)
if not mediainfo:
return False, f"未识别到媒体信息,类型:{mtype.value}id{mediaid}"
# 重新执行整理
@@ -1246,20 +1279,21 @@ class TransferChain(ChainBase, metaclass=Singleton):
def manual_transfer(self,
fileitem: FileItem,
target_storage: str = None,
target_storage: Optional[str] = None,
target_path: Path = None,
tmdbid: int = None,
doubanid: str = None,
tmdbid: Optional[int] = None,
doubanid: Optional[str] = None,
mtype: MediaType = None,
season: int = None,
transfer_type: str = None,
season: Optional[int] = None,
episode_group: Optional[str] = None,
transfer_type: Optional[str] = None,
epformat: EpisodeFormat = None,
min_filesize: int = 0,
scrape: bool = None,
library_type_folder: bool = None,
library_category_folder: bool = None,
force: bool = False,
background: bool = False) -> Tuple[bool, Union[str, list]]:
min_filesize: Optional[int] = 0,
scrape: Optional[bool] = None,
library_type_folder: Optional[bool] = None,
library_category_folder: Optional[bool] = None,
force: Optional[bool] = False,
background: Optional[bool] = False) -> Tuple[bool, Union[str, list]]:
"""
手动整理,支持复杂条件,带进度显示
:param fileitem: 文件项
@@ -1269,6 +1303,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
:param doubanid: 豆瓣ID
:param mtype: 媒体类型
:param season: 季度
:param episode_group: 剧集组
:param transfer_type: 整理类型
:param epformat: 剧集格式
:param min_filesize: 最小文件大小(MB)
@@ -1282,7 +1317,8 @@ class TransferChain(ChainBase, metaclass=Singleton):
if tmdbid or doubanid:
# 有输入TMDBID时单个识别
# 识别媒体信息
mediainfo: MediaInfo = self.mediachain.recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype)
mediainfo: MediaInfo = self.mediachain.recognize_media(tmdbid=tmdbid, doubanid=doubanid,
mtype=mtype, episode_group=episode_group)
if not mediainfo:
return False, f"媒体信息识别失败tmdbid{tmdbid}doubanid{doubanid}type: {mtype.value}"
else:
@@ -1334,26 +1370,20 @@ class TransferChain(ChainBase, metaclass=Singleton):
return state, errmsg
def send_transfer_message(self, meta: MetaBase, mediainfo: MediaInfo,
transferinfo: TransferInfo, season_episode: str = None, username: str = None):
transferinfo: TransferInfo, season_episode: Optional[str] = None, username: Optional[str] = None):
"""
发送入库成功的消息
"""
msg_title = f"{mediainfo.title_year} {meta.season_episode if not season_episode else season_episode} 已入库"
if mediainfo.vote_average:
msg_str = f"评分:{mediainfo.vote_average},类型:{mediainfo.type.value}"
else:
msg_str = f"类型:{mediainfo.type.value}"
if mediainfo.category:
msg_str = f"{msg_str},类别:{mediainfo.category}"
if meta.resource_term:
msg_str = f"{msg_str},质量:{meta.resource_term}"
msg_str = f"{msg_str},共{transferinfo.file_count}个文件," \
f"大小:{StringUtils.str_filesize(transferinfo.total_size)}"
if transferinfo.message:
msg_str = f"{msg_str},以下文件处理失败:\n{transferinfo.message}"
# 发送
self.post_message(Notification(
mtype=NotificationType.Organize,
title=msg_title, text=msg_str, image=mediainfo.get_message_image(),
username=username,
link=settings.MP_DOMAIN('#/history')))
self.post_message(
Notification(
mtype=NotificationType.Organize,
ctype=ContentType.OrganizeSuccess,
image=mediainfo.get_message_image(),
username=username,
link=settings.MP_DOMAIN('#/history')
),
meta=meta,
mediainfo=mediainfo,
transferinfo=transferinfo,
season_episode=season_episode
)

View File

@@ -30,7 +30,7 @@ class UserChain(ChainBase, metaclass=Singleton):
password: Optional[str] = None,
mfa_code: Optional[str] = None,
code: Optional[str] = None,
grant_type: str = "password"
grant_type: Optional[str] = "password"
) -> Union[Tuple[bool, Optional[str]], Tuple[bool, Optional[User]]]:
"""
认证用户,根据不同的 grant_type 处理不同的认证流程

View File

@@ -4,7 +4,7 @@ import threading
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
from time import sleep
from typing import List, Tuple
from typing import List, Tuple, Optional
from pydantic.fields import Callable
@@ -192,7 +192,7 @@ class WorkflowChain(ChainBase):
super().__init__()
self.workflowoper = WorkflowOper()
def process(self, workflow_id: int, from_begin: bool = True) -> Tuple[bool, str]:
def process(self, workflow_id: int, from_begin: Optional[bool] = True) -> Tuple[bool, str]:
"""
处理工作流
:param workflow_id: 工作流ID

View File

@@ -273,8 +273,8 @@ class Command(metaclass=Singleton):
}
return plugin_commands
def __run_command(self, command: Dict[str, any], data_str: str = "",
channel: MessageChannel = None, source: str = None, userid: Union[str, int] = None):
def __run_command(self, command: Dict[str, any], data_str: Optional[str] = "",
channel: MessageChannel = None, source: Optional[str] = None, userid: Union[str, int] = None):
"""
运行定时服务
"""
@@ -339,8 +339,8 @@ class Command(metaclass=Singleton):
"""
return self._commands.get(cmd, {})
def register(self, cmd: str, func: Any, data: dict = None,
desc: str = None, category: str = None) -> None:
def register(self, cmd: str, func: Any, data: Optional[dict] = None,
desc: Optional[str] = None, category: Optional[str] = None) -> None:
"""
注册单个命令
"""
@@ -352,8 +352,8 @@ class Command(metaclass=Singleton):
"data": data or {}
}
def execute(self, cmd: str, data_str: str = "",
channel: MessageChannel = None, source: str = None,
def execute(self, cmd: str, data_str: Optional[str] = "",
channel: MessageChannel = None, source: Optional[str] = None,
userid: Union[str, int] = None) -> None:
"""
执行命令

View File

@@ -1,6 +1,7 @@
import inspect
import json
import pickle
import threading
from abc import ABC, abstractmethod
from functools import wraps
from typing import Any, Dict, Optional
@@ -16,6 +17,8 @@ from app.log import logger
# 默认缓存区
DEFAULT_CACHE_REGION = "DEFAULT"
lock = threading.Lock()
class CacheBackend(ABC):
"""
@@ -23,7 +26,7 @@ class CacheBackend(ABC):
"""
@abstractmethod
def set(self, key: str, value: Any, ttl: int, region: str = DEFAULT_CACHE_REGION, **kwargs) -> None:
def set(self, key: str, value: Any, ttl: int, region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
"""
设置缓存
@@ -36,7 +39,7 @@ class CacheBackend(ABC):
pass
@abstractmethod
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> bool:
"""
判断缓存键是否存在
@@ -47,7 +50,7 @@ class CacheBackend(ABC):
pass
@abstractmethod
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Any:
def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> Any:
"""
获取缓存
@@ -58,7 +61,7 @@ class CacheBackend(ABC):
pass
@abstractmethod
def delete(self, key: str, region: str = DEFAULT_CACHE_REGION) -> None:
def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
"""
删除缓存
@@ -84,7 +87,7 @@ class CacheBackend(ABC):
pass
@staticmethod
def get_region(region: str = DEFAULT_CACHE_REGION):
def get_region(region: Optional[str] = DEFAULT_CACHE_REGION):
"""
获取缓存的区
"""
@@ -128,7 +131,7 @@ class CacheToolsBackend(CacheBackend):
- 不支持按 `key` 独立隔离 TTL 和 Maxsize仅支持作用于 region 级别
"""
def __init__(self, maxsize: int = 1000, ttl: int = 1800):
def __init__(self, maxsize: Optional[int] = 1000, ttl: Optional[int] = 1800):
"""
初始化缓存实例
@@ -147,7 +150,8 @@ class CacheToolsBackend(CacheBackend):
region = self.get_region(region)
return self._region_caches.get(region)
def set(self, key: str, value: Any, ttl: int = None, region: str = DEFAULT_CACHE_REGION, **kwargs) -> None:
def set(self, key: str, value: Any, ttl: Optional[int] = None,
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
"""
设置缓存值支持每个 key 独立配置 TTL 和 Maxsize
@@ -163,9 +167,10 @@ class CacheToolsBackend(CacheBackend):
# 如果该 key 尚未有缓存实例,则创建一个新的 TTLCache 实例
region_cache = self._region_caches.setdefault(region, TTLCache(maxsize=maxsize, ttl=ttl))
# 设置缓存值
region_cache[key] = value
with lock:
region_cache[key] = value
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> bool:
"""
判断缓存键是否存在
@@ -178,7 +183,7 @@ class CacheToolsBackend(CacheBackend):
return False
return key in region_cache
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Any:
def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> Any:
"""
获取缓存的值
@@ -191,7 +196,7 @@ class CacheToolsBackend(CacheBackend):
return None
return region_cache.get(key)
def delete(self, key: str, region: str = DEFAULT_CACHE_REGION) -> None:
def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
"""
删除缓存
@@ -201,7 +206,8 @@ class CacheToolsBackend(CacheBackend):
region_cache = self.__get_region_cache(region)
if region_cache is None:
return None
del region_cache[key]
with lock:
del region_cache[key]
def clear(self, region: Optional[str] = None) -> None:
"""
@@ -213,12 +219,14 @@ class CacheToolsBackend(CacheBackend):
# 清理指定缓存区
region_cache = self.__get_region_cache(region)
if region_cache:
region_cache.clear()
with lock:
region_cache.clear()
logger.info(f"Cleared cache for region: {region}")
else:
# 清除所有区域的缓存
for region_cache in self._region_caches.values():
region_cache.clear()
with lock:
region_cache.clear()
logger.info("Cleared all cache")
def close(self) -> None:
@@ -246,7 +254,7 @@ class RedisBackend(CacheBackend):
_complex_serializable_types = set()
_simple_serializable_types = set()
def __init__(self, redis_url: str = "redis://localhost", ttl: int = 1800):
def __init__(self, redis_url: Optional[str] = "redis://localhost", ttl: Optional[int] = 1800):
"""
初始化 Redis 缓存实例
@@ -271,7 +279,7 @@ class RedisBackend(CacheBackend):
logger.error(f"Failed to connect to Redis: {e}")
raise RuntimeError("Redis connection failed") from e
def set_memory_limit(self, policy: str = "allkeys-lru"):
def set_memory_limit(self, policy: Optional[str] = "allkeys-lru"):
"""
动态设置 Redis 最大内存和内存淘汰策略
:param policy: 淘汰策略(如 'allkeys-lru'
@@ -349,7 +357,8 @@ class RedisBackend(CacheBackend):
region = self.get_region(quote(region))
return f"{region}:key:{quote(key)}"
def set(self, key: str, value: Any, ttl: int = None, region: str = DEFAULT_CACHE_REGION, **kwargs) -> None:
def set(self, key: str, value: Any, ttl: Optional[int] = None,
region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None:
"""
设置缓存
@@ -369,7 +378,7 @@ class RedisBackend(CacheBackend):
except Exception as e:
logger.error(f"Failed to set key: {key} in region: {region}, error: {e}")
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
def exists(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> bool:
"""
判断缓存键是否存在
@@ -384,7 +393,7 @@ class RedisBackend(CacheBackend):
logger.error(f"Failed to exists key: {key} region: {region}, error: {e}")
return False
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Optional[Any]:
def get(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> Optional[Any]:
"""
获取缓存的值
@@ -402,7 +411,7 @@ class RedisBackend(CacheBackend):
logger.error(f"Failed to get key: {key} in region: {region}, error: {e}")
return None
def delete(self, key: str, region: str = DEFAULT_CACHE_REGION) -> None:
def delete(self, key: str, region: Optional[str] = DEFAULT_CACHE_REGION) -> None:
"""
删除缓存
@@ -445,7 +454,7 @@ class RedisBackend(CacheBackend):
self.client.close()
def get_cache_backend(maxsize: int = 1000, ttl: int = 1800) -> CacheBackend:
def get_cache_backend(maxsize: Optional[int] = 1000, ttl: Optional[int] = 1800) -> CacheBackend:
"""
根据配置获取缓存后端实例
@@ -473,8 +482,8 @@ def get_cache_backend(maxsize: int = 1000, ttl: int = 1800) -> CacheBackend:
return CacheToolsBackend(maxsize=maxsize, ttl=ttl)
def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
skip_none: bool = True, skip_empty: bool = False):
def cached(region: Optional[str] = None, maxsize: Optional[int] = 1000, ttl: Optional[int] = 1800,
skip_none: Optional[bool] = True, skip_empty: Optional[bool] = False):
"""
自定义缓存装饰器,支持为每个 key 动态传递 maxsize 和 ttl

View File

@@ -101,6 +101,8 @@ class ConfigModel(BaseModel):
TMDB_IMAGE_DOMAIN: str = "image.tmdb.org"
# TMDB API地址
TMDB_API_DOMAIN: str = "api.themoviedb.org"
# TMDB元数据语言
TMDB_LOCALE: str = "zh"
# TMDB API Key
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
# TVDB API Key
@@ -109,6 +111,10 @@ class ConfigModel(BaseModel):
FANART_ENABLE: bool = True
# Fanart API Key
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
# 115 AppId
U115_APP_ID: str = "100196807"
# Alipan AppId
ALIPAN_APP_ID: str = "ac1bf04dc9fd4d9aaabb65b4a668d403"
# 元数据识别缓存过期时间(小时)
META_CACHE_EXPIRE: int = 0
# 电视剧动漫的分类genre_ids
@@ -208,7 +214,8 @@ class ConfigModel(BaseModel):
PLUGIN_MARKET: str = ("https://github.com/jxxghp/MoviePilot-Plugins,"
"https://github.com/thsrite/MoviePilot-Plugins,"
"https://github.com/honue/MoviePilot-Plugins,"
"https://github.com/InfinityPacer/MoviePilot-Plugins")
"https://github.com/InfinityPacer/MoviePilot-Plugins,"
"https://github.com/DDS-Derek/MoviePilot-Plugins")
# 插件安装数据共享
PLUGIN_STATISTIC_SHARE: bool = True
# 是否开启插件热加载
@@ -233,6 +240,7 @@ class ConfigModel(BaseModel):
SECURITY_IMAGE_DOMAINS: List[str] = Field(
default_factory=lambda: ["image.tmdb.org",
"static-mdb.v.geilijiasu.com",
"bing.com",
"doubanio.com",
"lain.bgm.tv",
"raw.githubusercontent.com",
@@ -608,7 +616,7 @@ class GlobalVar(object):
# webpush订阅
SUBSCRIPTIONS: List[dict] = []
# 需应急停止的工作流
EMERGENCY_STOP_WORKFLOWS: List[str] = []
EMERGENCY_STOP_WORKFLOWS: List[int] = []
def stop_system(self):
"""
@@ -635,21 +643,21 @@ class GlobalVar(object):
"""
self.SUBSCRIPTIONS.append(subscription)
def stop_workflow(self, workflow_id: str):
def stop_workflow(self, workflow_id: int):
"""
停止工作流
"""
if workflow_id not in self.EMERGENCY_STOP_WORKFLOWS:
self.EMERGENCY_STOP_WORKFLOWS.append(workflow_id)
def workflow_resume(self, workflow_id: str):
def workflow_resume(self, workflow_id: int):
"""
恢复工作流
"""
if workflow_id in self.EMERGENCY_STOP_WORKFLOWS:
self.EMERGENCY_STOP_WORKFLOWS.remove(workflow_id)
def is_workflow_stopped(self, workflow_id: str):
def is_workflow_stopped(self, workflow_id: int):
"""
是否停止工作流
"""

View File

@@ -1,7 +1,7 @@
import re
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Any, Tuple
from typing import List, Dict, Any, Tuple, Optional
from app.core.config import settings
from app.core.meta import MetaBase
@@ -37,7 +37,7 @@ class TorrentInfo:
# 详情页面
page_url: str = None
# 种子大小
size: float = 0
size: float = 0.0
# 做种者
seeders: int = 0
# 下载者
@@ -193,7 +193,7 @@ class MediaInfo:
# LOGO
logo_path: str = None
# 评分
vote_average: float = 0
vote_average: float = 0.0
# 描述
overview: str = None
# 风格ID
@@ -264,6 +264,10 @@ class MediaInfo:
next_episode_to_air: dict = field(default_factory=dict)
# 内容分级
content_rating: str = None
# 全部剧集组
episode_groups: List[dict] = field(default_factory=list)
# 剧集组
episode_group: str = None
def __post_init__(self):
# 设置媒体信息
@@ -454,6 +458,10 @@ class MediaInfo:
air_date = seainfo.get("air_date")
if air_date:
self.season_years[season] = air_date[:4]
# 剧集组
if info.get("episode_groups"):
self.episode_groups = info.pop("episode_groups").get("results") or []
# 海报
if info.get('poster_path'):
self.poster_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('poster_path')}"
@@ -714,7 +722,7 @@ class MediaInfo:
return self.backdrop_path.replace("original", "w500")
return default or ""
def get_message_image(self, default: bool = None):
def get_message_image(self, default: Optional[bool] = None):
"""
返回消息图片地址
"""
@@ -722,7 +730,7 @@ class MediaInfo:
return self.backdrop_path.replace("original", "w500")
return self.get_poster_image(default=default)
def get_poster_image(self, default: bool = None):
def get_poster_image(self, default: Optional[bool] = None):
"""
返回海报图片地址
"""
@@ -730,7 +738,7 @@ class MediaInfo:
return self.poster_path.replace("original", "w500")
return default or ""
def get_overview_string(self, max_len: int = 140):
def get_overview_string(self, max_len: Optional[int] = 140):
"""
返回带限定长度的简介信息
:param max_len: 内容长度
@@ -773,6 +781,7 @@ class MediaInfo:
self.spoken_languages = []
self.networks = []
self.next_episode_to_air = {}
self.episode_groups = []
@dataclass

View File

@@ -31,7 +31,7 @@ class Event:
def __init__(self, event_type: Union[EventType, ChainEventType],
event_data: Optional[Union[Dict, ChainEventData]] = None,
priority: int = DEFAULT_EVENT_PRIORITY):
priority: Optional[int] = DEFAULT_EVENT_PRIORITY):
"""
:param event_type: 事件的类型,支持 EventType 或 ChainEventType
:param event_data: 可选,事件携带的数据,默认为空字典
@@ -130,7 +130,7 @@ class EventManager(metaclass=Singleton):
)
def send_event(self, etype: Union[EventType, ChainEventType], data: Optional[Union[Dict, ChainEventData]] = None,
priority: int = DEFAULT_EVENT_PRIORITY) -> Optional[Event]:
priority: Optional[int] = DEFAULT_EVENT_PRIORITY) -> Optional[Event]:
"""
发送事件,根据事件类型决定是广播事件还是链式事件
:param etype: 事件类型 (EventType 或 ChainEventType)
@@ -147,7 +147,7 @@ class EventManager(metaclass=Singleton):
logger.error(f"Unknown event type: {etype}")
def add_event_listener(self, event_type: Union[EventType, ChainEventType], handler: Callable,
priority: int = DEFAULT_EVENT_PRIORITY):
priority: Optional[int] = DEFAULT_EVENT_PRIORITY):
"""
注册事件处理器,将处理器添加到对应的事件订阅列表中
:param event_type: 事件类型 (EventType 或 ChainEventType)
@@ -506,7 +506,7 @@ class EventManager(metaclass=Singleton):
)
def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type],
priority: int = DEFAULT_EVENT_PRIORITY):
priority: Optional[int] = DEFAULT_EVENT_PRIORITY):
"""
事件注册装饰器,用于将函数注册为事件的处理器
:param etype:

View File

@@ -172,7 +172,7 @@ class MetaVideo(MetaBase):
return None
@staticmethod
def __is_pinyin(name_str: str) -> bool:
def __is_pinyin(name_str: Optional[str]) -> bool:
"""
判断是否拼音
"""
@@ -183,7 +183,7 @@ class MetaVideo(MetaBase):
return False
return True
def __fix_name(self, name: str):
def __fix_name(self, name: Optional[str]):
"""
去掉名字中不需要的干扰字符
"""
@@ -207,7 +207,7 @@ class MetaVideo(MetaBase):
name = None
return name
def __init_name(self, token: str):
def __init_name(self, token: Optional[str]):
"""
识别名称
"""

View File

@@ -15,32 +15,32 @@ class ReleaseGroupsMatcher(metaclass=Singleton):
"0ff": ['FF(?:(?:A|WE)B|CD|E(?:DU|B)|TV)'],
"1pt": [],
"52pt": [],
"audiences": ['Audies', 'AD(?:Audio|E(?:|book)|Music|Web)'],
"audiences": ['Audies', 'AD(?:Audio|E(?:book|)|Music|Web)'],
"azusa": [],
"beitai": ['BeiTai'],
"btschool": ['Bts(?:CHOOL|HD|PAD|TV)', 'Zone'],
"carpt": ['CarPT'],
"chdbits": ['CHD(?:|Bits|PAD|(?:|HK)TV|WEB)', 'StBOX', 'OneHD', 'Lee', 'xiaopie'],
"chdbits": ['CHD(?:Bits|PAD|(?:|HK)TV|WEB|)', 'StBOX', 'OneHD', 'Lee', 'xiaopie'],
"discfan": [],
"dragonhd": [],
"eastgame": ['(?:(?:iNT|(?:HALFC|Mini(?:S|H|FH)D))-|)TLF'],
"filelist": [],
"gainbound": ['(?:DG|GBWE)B'],
"hares": ['Hares(?:|(?:M|T)V|Web)'],
"hares": ['Hares(?:(?:M|T)V|Web|)'],
"hd4fans": [],
"hdarea": ['HDA(?:pad|rea|TV)', 'EPiC'],
"hdatmos": [],
"hdbd": [],
"hdchina": ['HDC(?:|hina|TV)', 'k9611', 'tudou', 'iHD'],
"hdchina": ['HDC(?:hina|TV|)', 'k9611', 'tudou', 'iHD'],
"hddolby": ['D(?:ream|BTV)', '(?:HD|QHstudI)o'],
"hdfans": ['beAst(?:|TV)'],
"hdhome": ['HDH(?:|ome|Pad|TV|WEB)'],
"hdpt": ['HDPT(?:|Web)'],
"hdsky": ['HDS(?:|ky|TV|Pad|WEB)', 'AQLJ'],
"hdfans": ['beAst(?:TV|)'],
"hdhome": ['HDH(?:ome|Pad|TV|WEB|)'],
"hdpt": ['HDPT(?:Web|)'],
"hdsky": ['HDS(?:ky|TV|Pad|WEB|)', 'AQLJ'],
"hdtime": [],
"HDU": [],
"hdvideo": [],
"hdzone": ['HDZ(?:|one)'],
"hdzone": ['HDZ(?:one|)'],
"hhanclub": ['HHWEB'],
"hitpt": [],
"htpt": ['HTPT'],
@@ -48,34 +48,36 @@ class ReleaseGroupsMatcher(metaclass=Singleton):
"joyhd": [],
"keepfrds": ['FRDS', 'Yumi', 'cXcY'],
"lemonhd": ['L(?:eague(?:(?:C|H)D|(?:M|T)V|NF|WEB)|HD)', 'i18n', 'CiNT'],
"mteam": ['MTeam(?:|TV)', 'MPAD'],
"mteam": ['MTeam(?:TV|)', 'MPAD'],
"nanyangpt": [],
"nicept": [],
"oshen": [],
"ourbits": ['Our(?:Bits|TV)', 'FLTTH', 'Ao', 'PbK', 'MGs', 'iLove(?:HD|TV)'],
"piggo": ['PiGo(?:NF|(?:H|WE)B)'],
"ptchina": [],
"pterclub": ['PTer(?:|DIY|Game|(?:M|T)V|WEB)'],
"pthome": ['PTH(?:|Audio|eBook|music|ome|tv|WEB)'],
"pterclub": ['PTer(?:DIY|Game|(?:M|T)V|WEB|)'],
"pthome": ['PTH(?:Audio|eBook|music|ome|tv|WEB|)'],
"ptmsg": [],
"ptsbao": ['PTsbao', 'OPS', 'F(?:Fans(?:AIeNcE|BD|D(?:VD|IY)|TV|WEB)|HDMv)', 'SGXT'],
"pttime": [],
"putao": ['PuTao'],
"soulvoice": [],
"springsunday": ['CMCT(?:|V)'],
"sharkpt": ['Shark(?:|WEB|DIY|TV|MV)'],
"springsunday": ['CMCT(?:V|)'],
"sharkpt": ['Shark(?:WEB|DIY|TV|MV|)'],
"tccf": [],
"tjupt": ['TJUPT'],
"totheglory": ['TTG', 'WiKi', 'NGB', 'DoA', '(?:ARi|ExRE)N'],
"U2": [],
"ultrahd": [],
"others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:|yG)',
'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )', 'UBWEB'],
"others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:yG|)',
'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )',],
"anime": ['ANi', 'HYSUB', 'KTXP', 'LoliHouse', 'MCE', 'Nekomoe kissaten', 'SweetSub', 'MingY',
'(?:Lilith|NC)-Raws', '织梦字幕组', '枫叶字幕组', '猎户手抄部', '喵萌奶茶屋', '漫猫字幕社',
'霜庭云花Sub', '北宇治字幕组', '氢气烤肉架', '云歌字幕组', '萌樱字幕组', '极影字幕社',
'悠哈璃羽字幕社',
'❀拨雪寻春❀', '沸羊羊(?:制作|字幕组)', '(?:桜|樱)都字幕组']
'❀拨雪寻春❀', '沸羊羊(?:制作|字幕组)', '(?:桜|樱)都字幕组'],
"forge": ['FROG(?:E|Web|)'],
"ubits": ['UB(?:its|WEB|TV)'],
}
def __init__(self):
@@ -97,13 +99,15 @@ class ReleaseGroupsMatcher(metaclass=Singleton):
if not groups:
# 自定义组
custom_release_groups = self.systemconfig.get(SystemConfigKey.CustomReleaseGroups)
if isinstance(custom_release_groups, list):
custom_release_groups = list(filter(None, custom_release_groups))
if custom_release_groups:
custom_release_groups_str = '|'.join(custom_release_groups)
groups = f"{self.__release_groups}|{custom_release_groups_str}"
else:
groups = self.__release_groups
title = f"{title} "
groups_re = re.compile(r"(?<=[-@\[£【&])(?:%s)(?=[@.\s\]\[】&])" % groups, re.I)
groups_re = re.compile(r"(?<=[-@\[£【&])(?:%s)(?=[@.\s\S\]\[】&])" % groups, re.I)
# 处理一个制作组识别多次的情况,保留顺序
unique_groups = []
for item in re.findall(groups_re, title):

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Tuple, List
from typing import Tuple, List, Optional
import regex as re
@@ -10,7 +10,7 @@ from app.log import logger
from app.schemas.types import MediaType
def MetaInfo(title: str, subtitle: str = None, custom_words: List[str] = None) -> MetaBase:
def MetaInfo(title: str, subtitle: Optional[str] = None, custom_words: List[str] = None) -> MetaBase:
"""
根据标题和副标题识别元数据
:param title: 标题、种子名、文件名
@@ -92,7 +92,8 @@ def is_anime(name: str) -> bool:
return True
if re.search(r'\s+-\s+[\dv]{1,4}\s+', name, re.IGNORECASE):
return True
if re.search(r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}", name,
if re.search(r"S\d{2}\s*-\s*S\d{2}|S\d{2}|\s+S\d{1,2}|EP?\d{2,4}\s*-\s*EP?\d{2,4}|EP?\d{2,4}|\s+EP?\d{1,4}",
name,
re.IGNORECASE):
return False
if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE):
@@ -133,13 +134,10 @@ def find_metainfo(title: str) -> Tuple[str, dict]:
# 查找媒体类型
mtype = re.findall(r'(?<=type=)\w+', result)
if mtype:
match mtype[0]:
case "movie":
metainfo['type'] = MediaType.MOVIE
case "tv":
metainfo['type'] = MediaType.TV
case _:
pass
if mtype[0] == "movies":
metainfo['type'] = MediaType.MOVIE
elif mtype[0] == "tv":
metainfo['type'] = MediaType.TV
# 查找季信息
begin_season = re.findall(r'(?<=s=)\d+', result)
if begin_season and begin_season[0].isdigit():

View File

@@ -7,8 +7,10 @@ import time
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from typing import Any, Dict, List, Optional, Type, Union, Callable, Tuple
from fastapi import HTTPException
from starlette import status
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
@@ -111,7 +113,7 @@ class PluginManager(metaclass=Singleton):
# 启动插件
self.start()
def start(self, pid: str = None):
def start(self, pid: Optional[str] = None):
"""
启动加载插件
:param pid: 插件ID为空加载所有插件
@@ -194,7 +196,7 @@ class PluginManager(metaclass=Singleton):
# 禁用插件类的事件处理器
eventmanager.disable_event_handler(type(plugin))
def stop(self, pid: str = None):
def stop(self, pid: Optional[str] = None):
"""
停止插件服务
:param pid: 插件ID为空停止所有插件
@@ -220,6 +222,14 @@ class PluginManager(metaclass=Singleton):
self._running_plugins = {}
logger.info("插件停止完成")
@property
def running_plugins(self):
"""
获取运行态插件列表
:return: 运行态插件列表
"""
return self._running_plugins
def reload_monitor(self):
"""
重新加载插件文件修改监测
@@ -407,68 +417,6 @@ class PluginManager(metaclass=Singleton):
self.plugindata.del_data(pid)
return True
def get_plugin_form(self, pid: str) -> Tuple[List[dict], Dict[str, Any]]:
"""
获取插件表单
:param pid: 插件ID
"""
plugin = self._running_plugins.get(pid)
if not plugin:
return [], {}
if hasattr(plugin, "get_form"):
return plugin.get_form() or ([], {})
return [], {}
def get_plugin_page(self, pid: str) -> List[dict]:
"""
获取插件页面
:param pid: 插件ID
"""
plugin = self._running_plugins.get(pid)
if not plugin:
return []
if hasattr(plugin, "get_page"):
return plugin.get_page() or []
return []
def get_plugin_dashboard(self, pid: str, key: str = None, **kwargs) -> Optional[schemas.PluginDashboard]:
"""
获取插件仪表盘
:param pid: 插件ID
:param key: 仪表盘key
"""
def __get_params_count(func: Callable):
"""
获取函数的参数信息
"""
signature = inspect.signature(func)
return len(signature.parameters)
plugin = self._running_plugins.get(pid)
if not plugin:
return None
if hasattr(plugin, "get_dashboard"):
# 检查方法的参数个数
params_count = __get_params_count(plugin.get_dashboard)
if params_count > 1:
dashboard: Tuple = plugin.get_dashboard(key=key, **kwargs)
elif params_count > 0:
dashboard: Tuple = plugin.get_dashboard(**kwargs)
else:
dashboard: Tuple = plugin.get_dashboard()
if dashboard:
cols, attrs, elements = dashboard
return schemas.PluginDashboard(
id=pid,
name=plugin.plugin_name,
key=key or "",
cols=cols or {},
elements=elements,
attrs=attrs or {}
)
return None
def get_plugin_state(self, pid: str) -> bool:
"""
获取插件状态
@@ -558,7 +506,63 @@ class PluginManager(metaclass=Singleton):
logger.error(f"获取插件 {plugin_id} 服务出错:{str(e)}")
return ret_services
def get_plugin_dashboard_meta(self):
def get_plugin_modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str, Any]]:
"""
获取插件模块
{
plugin_id: {
method: function
}
}
"""
ret_modules = {}
for plugin_id, plugin in self._running_plugins.items():
if pid and pid != plugin_id:
continue
if hasattr(plugin, "get_module") and ObjectUtils.check_method(plugin.get_module):
try:
if not plugin.get_state():
continue
plugin_module = plugin.get_module() or []
ret_modules[(plugin_id, plugin.get_name())] = plugin_module
except Exception as e:
logger.error(f"获取插件 {plugin_id} 模块出错:{str(e)}")
return ret_modules
@staticmethod
def get_plugin_remote_entry(plugin_id: str, dist_path: str) -> str:
"""
获取插件的远程入口地址
:param plugin_id: 插件 ID
:param dist_path: 插件的分发路径
:return: 远程入口地址
"""
if dist_path.startswith("/"):
dist_path = dist_path[1:]
if dist_path.endswith("/"):
dist_path = dist_path[:-1]
return f"/plugin/file/{plugin_id.lower()}/{dist_path}/remoteEntry.js"
def get_plugin_remotes(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件联邦组件列表
"""
remotes = []
for plugin_id, plugin in self._running_plugins.items():
if pid and pid != plugin_id:
continue
if hasattr(plugin, "get_render_mode"):
render_mode, dist_path = plugin.get_render_mode()
if render_mode != "vue":
continue
remotes.append({
"id": plugin_id,
"url": self.get_plugin_remote_entry(plugin_id, dist_path),
"name": plugin.plugin_name,
})
return remotes
def get_plugin_dashboard_meta(self) -> List[Dict[str, str]]:
"""
获取所有插件仪表盘元信息
"""
@@ -588,6 +592,49 @@ class PluginManager(metaclass=Singleton):
logger.error(f"获取插件[{plugin_id}]仪表盘元数据出错:{str(e)}")
return dashboard_meta
def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = None) -> schemas.PluginDashboard:
"""
获取插件仪表盘
"""
def __get_params_count(func: Callable):
"""
获取函数的参数信息
"""
signature = inspect.signature(func)
return len(signature.parameters)
# 获取插件实例
plugin_instance = self.running_plugins.get(pid)
if not plugin_instance:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"插件 {pid} 不存在或未加载")
# 渲染模式
render_mode, _ = plugin_instance.get_render_mode()
# 获取插件仪表板
try:
# 检查方法的参数个数
params_count = __get_params_count(plugin_instance.get_dashboard)
if params_count > 1:
dashboard: Tuple = plugin_instance.get_dashboard(key=key, user_agent=user_agent)
elif params_count > 0:
dashboard: Tuple = plugin_instance.get_dashboard(user_agent=user_agent)
else:
dashboard: Tuple = plugin_instance.get_dashboard()
except Exception as e:
logger.error(f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"插件 {pid} 调用方法 get_dashboard 出错: {str(e)}")
cols, attrs, elements = dashboard
return schemas.PluginDashboard(
id=pid,
name=plugin_instance.plugin_name,
key=key,
render_mode=render_mode,
cols=cols or {},
attrs=attrs or {},
)
def get_plugin_attr(self, pid: str, attr: str) -> Any:
"""
获取插件属性
@@ -781,7 +828,7 @@ class PluginManager(metaclass=Singleton):
logger.debug(f"获取插件是否在本地包中存在失败,{e}")
return False
def get_plugins_from_market(self, market: str, package_version: str = None) -> Optional[List[schemas.Plugin]]:
def get_plugins_from_market(self, market: str, package_version: Optional[str] = None) -> Optional[List[schemas.Plugin]]:
"""
从指定的市场获取插件信息
:param market: 市场的 URL 或标识

View File

@@ -1,10 +1,10 @@
import base64
import datetime
import hashlib
import hmac
import json
import os
import traceback
import datetime
from datetime import timedelta
from typing import Any, Union, Annotated, Optional
@@ -44,9 +44,9 @@ api_key_query = APIKeyQuery(name="apikey", auto_error=False, scheme_name="api_ke
def create_access_token(
userid: Union[str, Any],
username: str,
super_user: bool = False,
super_user: Optional[bool] = False,
expires_delta: Optional[timedelta] = None,
level: int = 1,
level: Optional[int] = 1,
purpose: Optional[str] = "authentication"
) -> str:
"""
@@ -136,7 +136,7 @@ def __set_or_refresh_resource_token_cookie(request: Request, response: Response,
)
def __verify_token(token: str, purpose: str = "authentication") -> schemas.TokenPayload:
def __verify_token(token: str, purpose: Optional[str] = "authentication") -> schemas.TokenPayload:
"""
使用 JWT Token 进行身份认证并解析 Token 的内容
:param token: JWT 令牌
@@ -176,7 +176,7 @@ def __verify_token(token: str, purpose: str = "authentication") -> schemas.Token
def verify_token(
request: Request,
response: Response,
token: str = Security(oauth2_scheme)
token: Annotated[str, Security(oauth2_scheme)]
) -> schemas.TokenPayload:
"""
验证 JWT 令牌并自动处理 resource_token 写入
@@ -196,7 +196,7 @@ def verify_token(
def verify_resource_token(
resource_token: str = Security(resource_token_cookie)
resource_token: Annotated[str, Security(resource_token_cookie)]
) -> schemas.TokenPayload:
"""
验证资源访问令牌(从 Cookie 中获取)
@@ -249,7 +249,7 @@ def __verify_key(key: str, expected_key: str, key_type: str) -> str:
return key
def verify_apitoken(token: str = Security(__get_api_token)) -> str:
def verify_apitoken(token: Annotated[str, Security(__get_api_token)]) -> str:
"""
使用 API Token 进行身份认证
:param token: API Token从 URL 查询参数中获取
@@ -258,7 +258,7 @@ def verify_apitoken(token: str = Security(__get_api_token)) -> str:
return __verify_key(token, settings.API_TOKEN, "API_TOKEN")
def verify_apikey(apikey: str = Security(__get_api_key)) -> str:
def verify_apikey(apikey: Annotated[str, Security(__get_api_key)]) -> str:
"""
使用 API Key 进行身份认证
:param apikey: API Key从 URL 查询参数或请求头中获取

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional
from app.db import DbOper
from app.db.models.downloadhistory import DownloadHistory, DownloadFiles
@@ -51,7 +51,7 @@ class DownloadHistoryOper(DbOper):
"""
DownloadFiles.truncate(self._db)
def get_files_by_hash(self, download_hash: str, state: int = None) -> List[DownloadFiles]:
def get_files_by_hash(self, download_hash: str, state: Optional[int] = None) -> List[DownloadFiles]:
"""
按Hash查询下载文件记录
:param download_hash: 数据key
@@ -97,7 +97,7 @@ class DownloadHistoryOper(DbOper):
return fileinfo.download_hash
return ""
def list_by_page(self, page: int = 1, count: int = 30) -> List[DownloadHistory]:
def list_by_page(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[DownloadHistory]:
"""
分页查询下载历史
"""
@@ -109,10 +109,11 @@ class DownloadHistoryOper(DbOper):
"""
DownloadHistory.truncate(self._db)
def get_last_by(self, mtype=None, title: str = None, year: str = None,
season: str = None, episode: str = None, tmdbid=None) -> List[DownloadHistory]:
def get_last_by(self, mtype=None, title: Optional[str] = None, year: Optional[str] = None,
season: Optional[str] = None, episode: Optional[str] = None, tmdbid=None) -> List[DownloadHistory]:
"""
按类型、标题、年份、季集查询下载记录
tmdbid + mtype 或 title + year
"""
return DownloadHistory.get_last_by(db=self._db,
mtype=mtype,
@@ -122,7 +123,7 @@ class DownloadHistoryOper(DbOper):
episode=episode,
tmdbid=tmdbid)
def list_by_user_date(self, date: str, username: str = None) -> List[DownloadHistory]:
def list_by_user_date(self, date: str, username: Optional[str] = None) -> List[DownloadHistory]:
"""
查询某用户某时间之前的下载历史
"""
@@ -130,7 +131,7 @@ class DownloadHistoryOper(DbOper):
date=date,
username=username)
def list_by_date(self, date: str, type: str, tmdbid: str, seasons: str = None) -> List[DownloadHistory]:
def list_by_date(self, date: str, type: str, tmdbid: str, seasons: Optional[str] = None) -> List[DownloadHistory]:
"""
查询某时间之后的下载历史
"""
@@ -140,7 +141,7 @@ class DownloadHistoryOper(DbOper):
tmdbid=tmdbid,
seasons=seasons)
def list_by_type(self, mtype: str, days: int = 7) -> List[DownloadHistory]:
def list_by_type(self, mtype: str, days: Optional[int] = 7) -> List[DownloadHistory]:
"""
获取指定类型的下载历史
"""

View File

@@ -18,14 +18,14 @@ class MessageOper(DbOper):
def add(self,
channel: MessageChannel = None,
source: str = None,
source: Optional[str] = None,
mtype: NotificationType = None,
title: str = None,
text: str = None,
image: str = None,
link: str = None,
userid: str = None,
action: int = 1,
title: Optional[str] = None,
text: Optional[str] = None,
image: Optional[str] = None,
link: Optional[str] = None,
userid: Optional[str] = None,
action: Optional[int] = 1,
note: Union[list, dict] = None,
**kwargs):
"""
@@ -62,7 +62,7 @@ class MessageOper(DbOper):
Message(**kwargs).create(self._db)
def list_by_page(self, page: int = 1, count: int = 30) -> Optional[str]:
def list_by_page(self, page: Optional[int] = 1, count: Optional[int] = 30) -> Optional[str]:
"""
获取媒体服务器数据ID
"""

View File

@@ -1,4 +1,5 @@
import time
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, JSON
from sqlalchemy.orm import Session
@@ -51,6 +52,8 @@ class DownloadHistory(Base):
note = Column(JSON)
# 自定义媒体类别
media_category = Column(String)
# 剧集组
episode_group = Column(String)
@staticmethod
@db_query
@@ -67,7 +70,7 @@ class DownloadHistory(Base):
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30):
def list_by_page(db: Session, page: Optional[int] = 1, count: Optional[int] = 30):
result = db.query(DownloadHistory).offset((page - 1) * count).limit(count).all()
return list(result)
@@ -78,52 +81,62 @@ class DownloadHistory(Base):
@staticmethod
@db_query
def get_last_by(db: Session, mtype: str = None, title: str = None, year: int = None, season: str = None,
episode: str = None, tmdbid: int = None):
def get_last_by(db: Session, mtype: Optional[str] = None, title: Optional[str] = None,
year: Optional[str] = None, season: Optional[str] = None,
episode: Optional[str] = None, tmdbid: Optional[int] = None):
"""
据tmdbid、season、season_episode查询转移记录
据tmdbid、season、season_episode查询下载记录
tmdbid + mtype 或 title + year
"""
result = None
if tmdbid and not season and not episode:
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).order_by(
# TMDBID + 类型
if tmdbid and mtype:
# 电视剧某季某集
if season and episode:
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.type == mtype,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
DownloadHistory.id.desc()).all()
if tmdbid and season and not episode:
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season).order_by(
# 电视剧某季
elif season:
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.type == mtype,
DownloadHistory.seasons == season).order_by(
DownloadHistory.id.desc()).all()
if tmdbid and season and episode:
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
else:
# 电视剧所有季集/电影
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.type == mtype).order_by(
DownloadHistory.id.desc()).all()
# 电视剧所有季集|电影
if not season and not episode:
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year).order_by(
# 标题 + 年份
elif title and year:
# 电视剧某季某集
if season and episode:
result = db.query(DownloadHistory).filter(DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
DownloadHistory.id.desc()).all()
# 电视剧某季
if season and not episode:
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season).order_by(
# 电视剧某季
elif season:
result = db.query(DownloadHistory).filter(DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season).order_by(
DownloadHistory.id.desc()).all()
# 电视剧某季某集
if season and episode:
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
else:
# 电视剧所有季集/电影
result = db.query(DownloadHistory).filter(DownloadHistory.title == title,
DownloadHistory.year == year).order_by(
DownloadHistory.id.desc()).all()
if result:
return list(result)
return []
@staticmethod
@db_query
def list_by_user_date(db: Session, date: str, username: str = None):
def list_by_user_date(db: Session, date: str, username: Optional[str] = None):
"""
查询某用户某时间之后的下载历史
"""
@@ -138,7 +151,7 @@ class DownloadHistory(Base):
@staticmethod
@db_query
def list_by_date(db: Session, date: str, type: str, tmdbid: str, seasons: str = None):
def list_by_date(db: Session, date: str, type: str, tmdbid: str, seasons: Optional[str] = None):
"""
查询某时间之后的下载历史
"""
@@ -187,7 +200,7 @@ class DownloadFiles(Base):
@staticmethod
@db_query
def get_by_hash(db: Session, download_hash: str, state: int = None):
def get_by_hash(db: Session, download_hash: str, state: Optional[int] = None):
if state:
result = db.query(DownloadFiles).filter(DownloadFiles.download_hash == download_hash,
DownloadFiles.state == state).all()

View File

@@ -1,3 +1,5 @@
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, JSON
from sqlalchemy.orm import Session
@@ -34,7 +36,7 @@ class Message(Base):
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30):
def list_by_page(db: Session, page: Optional[int] = 1, count: Optional[int] = 30):
result = db.query(Message).order_by(Message.reg_time.desc()).offset((page - 1) * count).limit(
count).all()
result.sort(key=lambda x: x.reg_time, reverse=False)

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, Float, JSON, func, or_
from sqlalchemy.orm import Session
@@ -54,7 +55,7 @@ class SiteUserData(Base):
@staticmethod
@db_query
def get_by_domain(db: Session, domain: str, workdate: str = None, worktime: str = None):
def get_by_domain(db: Session, domain: str, workdate: Optional[str] = None, worktime: Optional[str] = None):
if workdate and worktime:
return db.query(SiteUserData).filter(SiteUserData.domain == domain,
SiteUserData.updated_day == workdate,

View File

@@ -1,4 +1,5 @@
import time
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, Float, JSON
from sqlalchemy.orm import Session
@@ -83,10 +84,12 @@ class Subscribe(Base):
media_category = Column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
# 选择的剧集组
episode_group = Column(String)
@staticmethod
@db_query
def exists(db: Session, tmdbid: int = None, doubanid: str = None, season: int = None):
def exists(db: Session, tmdbid: Optional[int] = None, doubanid: Optional[str] = None, season: Optional[int] = None):
if tmdbid:
if season:
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
@@ -110,7 +113,7 @@ class Subscribe(Base):
@staticmethod
@db_query
def get_by_title(db: Session, title: str, season: int = None):
def get_by_title(db: Session, title: str, season: Optional[int] = None):
if season:
return db.query(Subscribe).filter(Subscribe.name == title,
Subscribe.season == season).first()
@@ -118,7 +121,7 @@ class Subscribe(Base):
@staticmethod
@db_query
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
def get_by_tmdbid(db: Session, tmdbid: int, season: Optional[int] = None):
if season:
result = db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
Subscribe.season == season).all()
@@ -164,7 +167,7 @@ class Subscribe(Base):
@staticmethod
@db_query
def list_by_username(db: Session, username: str, state: str = None, mtype: str = None):
def list_by_username(db: Session, username: str, state: Optional[str] = None, mtype: Optional[str] = None):
if mtype:
if state:
result = db.query(Subscribe).filter(Subscribe.state == state,

View File

@@ -1,3 +1,5 @@
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, Float, JSON
from sqlalchemy.orm import Session
@@ -67,10 +69,12 @@ class SubscribeHistory(Base):
media_category = Column(String)
# 过滤规则组
filter_groups = Column(JSON, default=list)
# 剧集组
episode_group = Column(String)
@staticmethod
@db_query
def list_by_type(db: Session, mtype: str, page: int = 1, count: int = 30):
def list_by_type(db: Session, mtype: str, page: Optional[int] = 1, count: Optional[int] = 30):
result = db.query(SubscribeHistory).filter(
SubscribeHistory.type == mtype
).order_by(
@@ -80,7 +84,7 @@ class SubscribeHistory(Base):
@staticmethod
@db_query
def exists(db: Session, tmdbid: int = None, doubanid: str = None, season: int = None):
def exists(db: Session, tmdbid: Optional[int] = None, doubanid: Optional[str] = None, season: Optional[int] = None):
if tmdbid:
if season:
return db.query(SubscribeHistory).filter(SubscribeHistory.tmdbid == tmdbid,

View File

@@ -1,4 +1,5 @@
import time
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence, Boolean, func, or_, JSON
from sqlalchemy.orm import Session
@@ -55,10 +56,12 @@ class TransferHistory(Base):
date = Column(String, index=True)
# 文件清单以JSON存储
files = Column(JSON, default=list)
# 剧集组
episode_group = Column(String)
@staticmethod
@db_query
def list_by_title(db: Session, title: str, page: int = 1, count: int = 30, status: bool = None):
def list_by_title(db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30, status: bool = None):
if status is not None:
result = db.query(TransferHistory).filter(
TransferHistory.status == status
@@ -77,7 +80,7 @@ class TransferHistory(Base):
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30, status: bool = None):
def list_by_page(db: Session, page: Optional[int] = 1, count: Optional[int] = 30, status: bool = None):
if status is not None:
result = db.query(TransferHistory).filter(
TransferHistory.status == status
@@ -97,7 +100,7 @@ class TransferHistory(Base):
@staticmethod
@db_query
def get_by_src(db: Session, src: str, storage: str = None):
def get_by_src(db: Session, src: str, storage: Optional[str] = None):
if storage:
return db.query(TransferHistory).filter(TransferHistory.src == src,
TransferHistory.src_storage == storage).first()
@@ -117,7 +120,7 @@ class TransferHistory(Base):
@staticmethod
@db_query
def statistic(db: Session, days: int = 7):
def statistic(db: Session, days: Optional[int] = 7):
"""
统计最近days天的下载历史数量按日期分组返回每日数量
"""
@@ -150,8 +153,8 @@ class TransferHistory(Base):
@staticmethod
@db_query
def list_by(db: Session, mtype: str = None, title: str = None, year: str = None, season: str = None,
episode: str = None, tmdbid: int = None, dest: str = None):
def list_by(db: Session, mtype: Optional[str] = None, title: Optional[str] = None, year: Optional[str] = None, season: Optional[str] = None,
episode: Optional[str] = None, tmdbid: Optional[int] = None, dest: Optional[str] = None):
"""
据tmdbid、season、season_episode查询转移记录
tmdbid + mtype 或 title + year 必输
@@ -218,7 +221,7 @@ class TransferHistory(Base):
@staticmethod
@db_query
def get_by_type_tmdbid(db: Session, mtype: str = None, tmdbid: int = None):
def get_by_type_tmdbid(db: Session, mtype: Optional[str] = None, tmdbid: Optional[int] = None):
"""
据tmdbid、type查询转移记录
"""
@@ -227,7 +230,7 @@ class TransferHistory(Base):
@staticmethod
@db_update
def update_download_hash(db: Session, historyid: int = None, download_hash: str = None):
def update_download_hash(db: Session, historyid: Optional[int] = None, download_hash: Optional[str] = None):
db.query(TransferHistory).filter(TransferHistory.id == historyid).update(
{
"download_hash": download_hash

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, Integer, JSON, Sequence, String, and_
@@ -72,7 +73,7 @@ class Workflow(Base):
@staticmethod
@db_update
def success(db, wid: int, result: str = None):
def success(db, wid: int, result: Optional[str] = None):
db.query(Workflow).filter(and_(Workflow.id == wid, Workflow.state != "P")).update({
"state": 'S',
"result": result,
@@ -83,12 +84,12 @@ class Workflow(Base):
@staticmethod
@db_update
def reset(db, wid: int):
def reset(db, wid: int, reset_count: Optional[bool] = False):
db.query(Workflow).filter(Workflow.id == wid).update({
"state": 'W',
"result": None,
"current_action": None,
"run_count": 0,
"run_count": 0 if reset_count else Workflow.run_count,
})
return True

View File

@@ -1,4 +1,4 @@
from typing import Any
from typing import Any, Optional
from app.db import DbOper
from app.db.models.plugindata import PluginData
@@ -24,7 +24,7 @@ class PluginDataOper(DbOper):
else:
PluginData(plugin_id=plugin_id, key=key, value=value).create(self._db)
def get_data(self, plugin_id: str, key: str = None) -> Any:
def get_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
"""
获取插件数据
:param plugin_id: 插件id
@@ -38,7 +38,7 @@ class PluginDataOper(DbOper):
else:
return PluginData.get_plugin_data(self._db, plugin_id)
def del_data(self, plugin_id: str, key: str = None) -> Any:
def del_data(self, plugin_id: str, key: Optional[str] = None) -> Any:
"""
删除插件数据
:param plugin_id: 插件id

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Tuple
from typing import List, Tuple, Optional
from app.db import DbOper
from app.db.models import SiteIcon
@@ -121,7 +121,8 @@ class SiteOper(DbOper):
siteuserdatas = SiteUserData.get_by_domain(self._db, domain=domain, workdate=current_day)
if siteuserdatas:
# 存在则更新
siteuserdatas[0].update(self._db, payload)
if not payload.get("err_msg"):
siteuserdatas[0].update(self._db, payload)
else:
# 不存在则插入
SiteUserData(**payload).create(self._db)
@@ -133,7 +134,7 @@ class SiteOper(DbOper):
"""
return SiteUserData.list(self._db)
def get_userdata_by_domain(self, domain: str, workdate: str = None) -> List[SiteUserData]:
def get_userdata_by_domain(self, domain: str, workdate: Optional[str] = None) -> List[SiteUserData]:
"""
获取站点用户数据
"""
@@ -172,7 +173,7 @@ class SiteOper(DbOper):
})
return True
def success(self, domain: str, seconds: int = None):
def success(self, domain: str, seconds: Optional[int] = None):
"""
站点访问成功
"""

View File

@@ -1,5 +1,5 @@
import time
from typing import Tuple, List
from typing import Tuple, List, Optional
from app.core.context import MediaInfo
from app.db import DbOper
@@ -20,21 +20,24 @@ class SubscribeOper(DbOper):
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id,
season=kwargs.get('season'))
kwargs.update({
"name": mediainfo.title,
"year": mediainfo.year,
"type": mediainfo.type.value,
"tmdbid": mediainfo.tmdb_id,
"imdbid": mediainfo.imdb_id,
"tvdbid": mediainfo.tvdb_id,
"doubanid": mediainfo.douban_id,
"bangumiid": mediainfo.bangumi_id,
"episode_group": mediainfo.episode_group,
"poster": mediainfo.get_poster_image(),
"backdrop": mediainfo.get_backdrop_image(),
"vote": mediainfo.vote_average,
"description": mediainfo.overview,
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
})
if not subscribe:
subscribe = Subscribe(name=mediainfo.title,
year=mediainfo.year,
type=mediainfo.type.value,
tmdbid=mediainfo.tmdb_id,
imdbid=mediainfo.imdb_id,
tvdbid=mediainfo.tvdb_id,
doubanid=mediainfo.douban_id,
bangumiid=mediainfo.bangumi_id,
poster=mediainfo.get_poster_image(),
backdrop=mediainfo.get_backdrop_image(),
vote=mediainfo.vote_average,
description=mediainfo.overview,
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
**kwargs)
subscribe = Subscribe(**kwargs)
subscribe.create(self._db)
# 查询订阅
subscribe = Subscribe.exists(self._db,
@@ -45,7 +48,7 @@ class SubscribeOper(DbOper):
else:
return subscribe.id, "订阅已存在"
def exists(self, tmdbid: int = None, doubanid: str = None, season: int = None) -> bool:
def exists(self, tmdbid: Optional[int] = None, doubanid: Optional[str] = None, season: Optional[int] = None) -> bool:
"""
判断是否存在
"""
@@ -64,7 +67,7 @@ class SubscribeOper(DbOper):
"""
return Subscribe.get(self._db, rid=sid)
def list(self, state: str = None) -> List[Subscribe]:
def list(self, state: Optional[str] = None) -> List[Subscribe]:
"""
获取订阅列表
"""
@@ -87,19 +90,19 @@ class SubscribeOper(DbOper):
subscribe.update(self._db, payload)
return subscribe
def list_by_tmdbid(self, tmdbid: int, season: int = None) -> List[Subscribe]:
def list_by_tmdbid(self, tmdbid: int, season: Optional[int] = None) -> List[Subscribe]:
"""
获取指定tmdb_id的订阅
"""
return Subscribe.get_by_tmdbid(self._db, tmdbid=tmdbid, season=season)
def list_by_username(self, username: str, state: str = None, mtype: str = None) -> List[Subscribe]:
def list_by_username(self, username: str, state: Optional[str] = None, mtype: Optional[str] = None) -> List[Subscribe]:
"""
获取指定用户的订阅
"""
return Subscribe.list_by_username(self._db, username=username, state=state, mtype=mtype)
def list_by_type(self, mtype: str, days: int = 7) -> Subscribe:
def list_by_type(self, mtype: str, days: Optional[int] = 7) -> Subscribe:
"""
获取指定类型的订阅
"""
@@ -119,7 +122,7 @@ class SubscribeOper(DbOper):
subscribe = SubscribeHistory(**kwargs)
subscribe.create(self._db)
def exist_history(self, tmdbid: int = None, doubanid: str = None, season: int = None):
def exist_history(self, tmdbid: Optional[int] = None, doubanid: Optional[str] = None, season: Optional[int] = None):
"""
判断是否存在订阅历史
"""

View File

@@ -1,5 +1,5 @@
import time
from typing import Any, List
from typing import Any, List, Optional
from app.core.context import MediaInfo
from app.core.meta import MetaBase
@@ -27,7 +27,7 @@ class TransferHistoryOper(DbOper):
"""
return TransferHistory.list_by_title(self._db, title)
def get_by_src(self, src: str, storage: str = None) -> TransferHistory:
def get_by_src(self, src: str, storage: Optional[str] = None) -> TransferHistory:
"""
按源查询转移记录
:param src: 数据key
@@ -58,14 +58,15 @@ class TransferHistoryOper(DbOper):
})
TransferHistory(**kwargs).create(self._db)
def statistic(self, days: int = 7) -> List[Any]:
def statistic(self, days: Optional[int] = 7) -> List[Any]:
"""
统计最近days天的下载历史数量
"""
return TransferHistory.statistic(self._db, days)
def get_by(self, title: str = None, year: str = None, mtype: str = None,
season: str = None, episode: str = None, tmdbid: int = None, dest: str = None) -> List[TransferHistory]:
def get_by(self, title: Optional[str] = None, year: Optional[str] = None, mtype: Optional[str] = None,
season: Optional[str] = None, episode: Optional[str] = None, tmdbid: Optional[int] = None,
dest: Optional[str] = None) -> List[TransferHistory]:
"""
按类型、标题、年份、季集查询转移记录
"""
@@ -78,7 +79,7 @@ class TransferHistoryOper(DbOper):
episode=episode,
tmdbid=tmdbid)
def get_by_type_tmdbid(self, mtype: str = None, tmdbid: int = None) -> TransferHistory:
def get_by_type_tmdbid(self, mtype: Optional[str] = None, tmdbid: Optional[int] = None) -> TransferHistory:
"""
按类型、tmdb查询转移记录
"""
@@ -120,7 +121,7 @@ class TransferHistoryOper(DbOper):
def add_success(self, fileitem: FileItem, mode: str, meta: MetaBase,
mediainfo: MediaInfo, transferinfo: TransferInfo,
downloader: str = None, download_hash: str = None):
downloader: Optional[str] = None, download_hash: Optional[str] = None):
"""
新增转移成功历史记录
"""
@@ -150,7 +151,7 @@ class TransferHistoryOper(DbOper):
)
def add_fail(self, fileitem: FileItem, mode: str, meta: MetaBase, mediainfo: MediaInfo = None,
transferinfo: TransferInfo = None, downloader: str = None, download_hash: str = None):
transferinfo: TransferInfo = None, downloader: Optional[str] = None, download_hash: Optional[str] = None):
"""
新增转移失败历史记录
"""
@@ -176,6 +177,7 @@ class TransferHistoryOper(DbOper):
image=mediainfo.get_poster_image(),
downloader=downloader,
download_hash=download_hash,
episode_group=mediainfo.episode_group,
status=0,
errmsg=transferinfo.message or '未知错误',
files=transferinfo.file_list

View File

@@ -1,4 +1,4 @@
from typing import List, Tuple
from typing import List, Tuple, Optional
from app.db import DbOper
from app.db.models.workflow import Workflow
@@ -43,7 +43,7 @@ class WorkflowOper(DbOper):
"""
return Workflow.start(self._db, wid)
def success(self, wid: int, result: str = None) -> bool:
def success(self, wid: int, result: Optional[str] = None) -> bool:
"""
成功
"""
@@ -61,8 +61,8 @@ class WorkflowOper(DbOper):
"""
return Workflow.update_current_action(self._db, wid, action_id, context)
def reset(self, wid: int) -> bool:
def reset(self, wid: int, reset_count: bool = False) -> bool:
"""
重置
"""
return Workflow.reset(self._db, wid)
return Workflow.reset(self._db, wid, reset_count=reset_count)

View File

@@ -20,11 +20,11 @@ class PlaywrightHelper:
def action(self, url: str,
callback: Callable,
cookies: str = None,
ua: str = None,
proxies: dict = None,
headless: bool = False,
timeout: int = 30) -> Any:
cookies: Optional[str] = None,
ua: Optional[str] = None,
proxies: Optional[dict] = None,
headless: Optional[bool] = False,
timeout: Optional[int] = 30) -> Any:
"""
访问网页接收Page对象并执行操作
:param url: 网页地址
@@ -57,11 +57,11 @@ class PlaywrightHelper:
return None
def get_page_source(self, url: str,
cookies: str = None,
ua: str = None,
proxies: dict = None,
headless: bool = False,
timeout: int = 20) -> Optional[str]:
cookies: Optional[str] = None,
ua: Optional[str] = None,
proxies: Optional[dict] = None,
headless: Optional[bool] = False,
timeout: Optional[int] = 20) -> Optional[str]:
"""
获取网页源码
:param url: 网页地址

View File

@@ -73,8 +73,8 @@ class CookieHelper:
url: str,
username: str,
password: str,
two_step_code: str = None,
proxies: dict = None) -> Tuple[Optional[str], Optional[str], str]:
two_step_code: Optional[str] = None,
proxies: Optional[dict] = None) -> Tuple[Optional[str], Optional[str], str]:
"""
获取站点cookie和ua
:param url: 站点地址

View File

@@ -49,9 +49,9 @@ class DirectoryHelper:
"""
return [d for d in self.get_library_dirs() if d.library_storage == "local"]
def get_dir(self, media: MediaInfo, include_unsorted: bool = False,
storage: str = None, src_path: Path = None,
target_storage: str = None, dest_path: Path = None
def get_dir(self, media: MediaInfo, include_unsorted: Optional[bool] = False,
storage: Optional[str] = None, src_path: Path = None,
target_storage: Optional[str] = None, dest_path: Path = None
) -> Optional[schemas.TransferDirectoryConf]:
"""
根据媒体信息获取下载目录、媒体库目录配置

View File

@@ -24,4 +24,3 @@ class DisplayHelper(metaclass=Singleton):
logger.info("正在停止虚拟显示...")
self._display.stop()
logger.info("虚拟显示已停止")

View File

@@ -129,7 +129,7 @@ def doh_query_json(resolver: str, host: str) -> Optional[str]:
if response.status != 200:
return None
response_body = response.read().decode("utf-8")
logger.debug("<== body: %s", response_body)
logger.debug("<== body: %s", response_body)
answer = json.loads(response_body)["Answer"]
return answer[0]["data"]
except Exception as e:

View File

@@ -10,8 +10,8 @@ class FormatParser(object):
_key = ""
_split_chars = r"\.|\s+|\(|\)|\[|]|-|\+|【|】|/||;|&|\||#|_|「|」|~"
def __init__(self, eformat: str, details: str = None, part: str = None,
offset: str = None, key: str = "ep"):
def __init__(self, eformat: str, details: Optional[str] = None, part: Optional[str] = None,
offset: Optional[str] = None, key: Optional[str] = "ep"):
"""
:params eformat: 格式化字符串
:params details: 格式化详情

View File

@@ -1,18 +1,525 @@
from __future__ import annotations
import ast
import json
import queue
import re
import threading
import time
from datetime import datetime
from typing import Any, Union
from typing import List, Optional, Callable
from typing import Any, Literal, Optional, List, Dict, Union
from typing import Callable
from cachetools import TTLCache
from jinja2 import Template
from app.core.config import global_vars
from app.core.context import MediaInfo, TorrentInfo
from app.core.meta import MetaBase
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.schemas.message import Notification
from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import TransferInfo
from app.schemas.types import SystemConfigKey
from app.utils.singleton import Singleton, SingletonClass
from app.log import logger
from app.utils.string import StringUtils
class TemplateContextBuilder:
"""
模板上下文构建器
"""
def __init__(self):
self._context = {}
def build(
self,
meta: Optional[MetaBase] = None,
mediainfo: Optional[MediaInfo] = None,
torrentinfo: Optional[TorrentInfo] = None,
transferinfo: Optional[TransferInfo] = None,
file_extension: Optional[str] = None,
episodes_info: Optional[List[TmdbEpisode]] = None,
include_raw_objects: bool = False,
**kwargs
) -> Dict[str, Any]:
"""
:param meta: 媒体信息
:param mediainfo: 媒体信息
:param torrentinfo: 种子信息
:param transferinfo: 传输信息
:param file_extension: 文件扩展名
:param episodes_info: 剧集信息
:param include_raw_objects: 是否包含原始对象
:return: 渲染上下文字典
"""
self._context.clear()
self._add_episode_details(meta, episodes_info)
self._add_media_info(mediainfo)
self._add_transfer_info(transferinfo)
self._add_torrent_info(torrentinfo)
self._add_file_info(file_extension)
if kwargs: self._context.update(kwargs)
if include_raw_objects:
self._add_raw_objects(meta, mediainfo, torrentinfo, transferinfo, episodes_info)
return self._context
def _add_media_info(self, mediainfo: MediaInfo):
"""
增加媒体信息
"""
if not mediainfo: return
base_info = {
# 标题
"title": self.__convert_invalid_characters(mediainfo.title),
# 英文标题
"en_title": self.__convert_invalid_characters(mediainfo.en_title),
# 原语种标题
"original_title": self.__convert_invalid_characters(mediainfo.original_title),
# 年份
"year": mediainfo.year or self._context.get("year"),
"title_year": mediainfo.title_year or self._context.get("title_year"),
}
_meta_season = self._context.get("season")
media_info = {
# 类型
"type": mediainfo.type.value,
# 类别
"category": mediainfo.category,
# 评分
"vote_average": mediainfo.vote_average,
# 海报
"poster": mediainfo.get_poster_image(),
# 背景图
"backdrop": mediainfo.get_backdrop_image(),
# 季年份根据season值获取
"season_year": mediainfo.season_years.get(
int(_meta_season),
None) if (mediainfo.season_years and _meta_season) else None,
# 演员
"actors": ''.join([actor['name'] for actor in mediainfo.actors[:5]]),
# 简介
"overview": mediainfo.overview,
# TMDBID
"tmdbid": mediainfo.tmdb_id,
# IMDBID
"imdbid": mediainfo.imdb_id,
# 豆瓣ID
"doubanid": mediainfo.douban_id,
}
self._context.update({**base_info, **media_info})
def _add_episode_details(self, meta: Optional[MetaBase], episodes: Optional[List[TmdbEpisode]]):
"""
添加剧集详细信息
"""
if not meta:
return
episode_data = {"episode_title": None, "episode_date": None}
if meta.begin_episode and episodes:
for episode in episodes:
if episode.episode_number == meta.begin_episode:
episode_data.update({
"episode_title": self.__convert_invalid_characters(episode.name),
"episode_date": episode.air_date if episode.air_date else None
})
break
meta_info = {
# 原文件名
"original_name": meta.title,
# 识别名称(优先使用中文)
"name": meta.name,
# 识别的英文名称(可能为空)
"en_name": meta.en_name,
# 年份
"year": meta.year,
# 名字 + 年份
"title_year": self._context.get("title_year") or "%s (%s)" % (
meta.name, meta.year) if meta.year else meta.name,
# 季号
"season": meta.season_seq,
# 集号
"episode": meta.episode_seqs,
# 季集 SxxExx
"season_episode": "%s%s" % (meta.season, meta.episode),
# 段/节
"part": meta.part,
# 自定义占位符
"customization": meta.customization,
}
tech_metadata = {
# 资源类型
"resourceType": meta.resource_type,
# 特效
"effect": meta.resource_effect,
# 版本
"edition": meta.edition,
# 分辨率
"videoFormat": meta.resource_pix,
# 质量
"resource_term": meta.resource_term,
# 制作组/字幕组
"releaseGroup": meta.resource_team,
# 视频编码
"videoCodec": meta.video_encode,
# 音频编码
"audioCodec": meta.audio_encode,
}
self._context.update({**meta_info, **tech_metadata, **episode_data})
def _add_torrent_info(self, torrentinfo: Optional[TorrentInfo]):
"""
添加种子信息
"""
if not torrentinfo:
return
if torrentinfo.size:
if str(torrentinfo.size).replace(".", "").isdigit():
size = StringUtils.str_filesize(torrentinfo.size)
else:
size = torrentinfo.size
else:
size = 0
if torrentinfo.description:
html_re = re.compile(r'<[^>]+>', re.S)
description = html_re.sub('', torrentinfo.description)
torrentinfo.description = re.sub(r'<[^>]+>', '', description)
torrent_info = {
# 种子标题
"torrent_title": torrentinfo.title,
# 发布时间
"pubdate": torrentinfo.pubdate,
# 免费剩余时间
"freedate": torrentinfo.freedate_diff,
# 做种数
"seeders": torrentinfo.seeders,
# 促销信息
"volume_factor": torrentinfo.volume_factor,
# Hit&Run
"hit_and_run": "" if torrentinfo.hit_and_run else "",
# 种子标签
"labels": ' '.join(torrentinfo.labels),
# 描述
"description": torrentinfo.description,
# 站点名称
"site_name": torrentinfo.site_name,
# 种子大小
"size": size,
}
self._context.update(torrent_info)
def _add_transfer_info(self, transferinfo: Optional[TransferInfo]) -> Optional[Dict]:
"""
添加文件转移上下文
"""
if not transferinfo:
return None
ctx = {
"transfer_type": transferinfo.transfer_type,
"file_count": transferinfo.file_count,
"total_size": StringUtils.str_filesize(transferinfo.total_size),
"err_msg": transferinfo.message,
}
self._context.update(ctx)
def _add_file_info(self, file_extension: Optional[str]):
"""
添加文件信息
"""
if not file_extension: return
file_info = {
# 文件后缀
"fileExt": file_extension,
}
self._context.update(file_info)
def _add_raw_objects(
self,
meta: Optional[MetaBase],
mediainfo: Optional[MediaInfo],
torrentinfo: Optional[TorrentInfo],
transferinfo: Optional[TransferInfo],
episodes_info: Optional[List[TmdbEpisode]],
):
"""
添加原始对象引用
"""
raw_objects = {
# 文件元数据
"__meta__": meta,
# 识别的媒体信息
"__mediainfo__": mediainfo,
# 种子信息
"__torrentinfo__": torrentinfo,
# 文件转移信息
"__transferinfo__": transferinfo,
# 当前季的全部集信息
"__episodes_info__": episodes_info,
}
self._context.update({k: v for k, v in raw_objects.items() if v is not None})
@staticmethod
def __convert_invalid_characters(filename: str):
"""
将不支持的字符转换为全角字符
"""
if not filename:
return filename
invalid_characters = r'\/:*?"<>|'
# 创建半角到全角字符的转换表
halfwidth_chars = "".join([chr(i) for i in range(33, 127)])
fullwidth_chars = "".join([chr(i + 0xFEE0) for i in range(33, 127)])
translation_table = str.maketrans(halfwidth_chars, fullwidth_chars)
# 将不支持的字符替换为对应的全角字符
for char in invalid_characters:
filename = filename.replace(char, char.translate(translation_table))
return filename
class TemplateHelper(metaclass=SingletonClass):
"""
模板格式渲染帮助类
"""
def __init__(self):
self.builder = TemplateContextBuilder()
self.cache = TTLCache(maxsize=100, ttl=600)
@staticmethod
def _generate_cache_key(cuntent: Union[str, dict]) -> str:
"""
生成缓存键
"""
if isinstance(cuntent, dict):
base_str = cuntent.get("title", '') + cuntent.get("text", '')
return StringUtils.md5_hash(json.dumps(base_str, sort_keys=True, ensure_ascii=False))
return StringUtils.md5_hash(cuntent)
def get_cache_context(self, cuntent: Union[str, dict]) -> Optional[dict]:
"""
获取缓存上下文
"""
cache_key = self._generate_cache_key(cuntent)
return self.cache.get(cache_key)
def set_cache_context(self, cuntent: Union[str, dict], context: dict) -> None:
"""
设置缓存上下文
"""
cache_key = self._generate_cache_key(cuntent)
self.cache[cache_key] = context
def render(self,
template_content: str,
template_type: Literal['string', 'dict', 'literal'] = "literal",
**kwargs) -> Optional[Union[str, dict]]:
"""
根据模板格式渲染内容
:param template_content: 模板字符串
:param template_type: 模板字符串类型(消息通知`literal`, 路径`string`)
:param kwargs: 补传业务对象
:raises ValueError: 当模板处理过程中出现错误
:return: 渲染后的结果
"""
try:
# 解析模板字符
parsed = self.parse_template_content(template_content, template_type)
if not parsed:
raise ValueError("模板解析失败")
context = self.builder.build(**kwargs)
if not context:
raise ValueError("上下文构建失败")
rendered = self.render_with_context(parsed, context)
if not rendered:
raise ValueError("模板渲染失败")
if rendered := rendered if template_type == 'string' else self.__process_formatted_string(rendered):
# 缓存上下文
self.set_cache_context(rendered, context)
# 返回渲染结果
return rendered
except Exception as e:
logger.error(f"模板处理失败: {str(e)}")
raise ValueError(f"模板处理失败: {str(e)}") from e
@staticmethod
def render_with_context(template_content: str, context: dict) -> str:
"""
使用指定上下文渲染 Jinja2 模板字符串
template_content: Jinja2 模板字符串
context: 渲染用的上下文数据
"""
# 渲染模板
template = Template(template_content)
return template.render(context)
@staticmethod
def parse_template_content(template_content: Union[str, dict],
template_type: Literal['string', 'dict', 'literal'] = None) -> Optional[str]:
"""
解析模板字符
:param template_content 模板格式字符
:param template_type 模板字符类型
"""
def parse_literal(_template_content: str) -> str:
"""
解析Python字面量
"""
try:
template_dict = ast.literal_eval(_template_content) if isinstance(_template_content,
str) else _template_content
if not isinstance(template_dict, dict):
raise ValueError("解析结果必须是一个字典")
return json.dumps(template_dict, ensure_ascii=False)
except (ValueError, SyntaxError) as err:
raise ValueError(f"无效的Python字面量格式: {str(err)}")
try:
if template_type:
parse_map = {
'string': lambda x: str(x),
'dict': lambda x: json.dumps(x, ensure_ascii=False),
'literal': parse_literal
}
return parse_map[template_type](template_content)
# 自动判断模板类型
if isinstance(template_content, dict):
return json.dumps(template_content, ensure_ascii=False)
elif isinstance(template_content, str):
try:
json.loads(template_content)
return template_content
except json.JSONDecodeError:
try:
return parse_literal(template_content)
except (ValueError, SyntaxError):
return template_content
else:
raise ValueError(f"不支持的模板类型: {type(template_content)}")
except Exception as e:
logger.error(f"模板解析失败: {str(e)}")
return None
@staticmethod
def __process_formatted_string(rendered: str) -> Optional[Union[dict, str]]:
"""
处理格式化字符串
保留转义字符
"""
def restore_chars(obj: Any) -> Any:
"""恢复特殊字符"""
if isinstance(obj, str):
return obj.replace('\\n', '\n').replace('\\r', '\r').replace('\\t', '\t').replace('\\b', '\b').replace(
'\\f', '\f')
elif isinstance(obj, dict):
return {k: restore_chars(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [restore_chars(item) for item in obj]
return obj
# 定义特殊字符映射
special_chars = {
'\n': '\\n', # 换行符
'\r': '\\r', # 回车符
'\t': '\\t', # 制表符
'\b': '\\b', # 退格符
'\f': '\\f', # 换页符
}
# 处理特殊字符
processed = rendered
for char, escape in special_chars.items():
processed = processed.replace(char, escape)
# 尝试解析为JSON
try:
rendered_dict = json.loads(processed)
return restore_chars(rendered_dict)
except json.JSONDecodeError:
return rendered
class MessageTemplateHelper:
"""
消息模板渲染器
"""
@staticmethod
def render(message: Notification, *args, **kwargs) -> Optional[Notification]:
"""
渲染消息模板
"""
if not MessageTemplateHelper.is_instance_valid(message):
if MessageTemplateHelper.meets_update_conditions(message, *args, **kwargs):
logger.info("将使用模板渲染消息内容")
return MessageTemplateHelper._apply_template_data(message, *args, **kwargs)
return message
@staticmethod
def is_instance_valid(message: Notification) -> bool:
"""
检查消息是否有效
"""
if isinstance(message, Notification):
return bool(message.title or message.text)
return False
@staticmethod
def meets_update_conditions(message: Notification, *args, **kwargs) -> bool:
"""
判断是否满足消息实例更新条件
满足条件需同时具备:
1. 消息为有效Notification实例
2. 消息指定了模板类型(ctype)
3. 存在待渲染的模板变量数据
"""
if isinstance(message, Notification):
return True if message.ctype and (args or kwargs) else False
return False
@staticmethod
def _apply_template_data(message: Notification, *args, **kwargs) -> Optional[Notification]:
"""
更新消息实例
"""
try:
if template := MessageTemplateHelper._get_template(message):
rendered = TemplateHelper().render(template_content=template, *args, **kwargs)
for key, value in rendered.items():
if hasattr(message, key):
setattr(message, key, value)
return message
except Exception as e:
logger.error(f"更新Notification时出现错误{str(e)}")
return message
@staticmethod
def _get_template(message: Notification) -> Optional[str]:
"""
获取消息模板
"""
template_dict: dict[str, str] = SystemConfigOper().get(SystemConfigKey.NotificationTemplates)
return template_dict.get(f"{message.ctype.value}")
class MessageQueueManager(metaclass=SingletonClass):
@@ -25,7 +532,7 @@ class MessageQueueManager(metaclass=SingletonClass):
def __init__(
self,
send_callback: Optional[Callable] = None,
check_interval: int = 10
check_interval: Optional[int] = 10
) -> None:
"""
消息队列管理器初始化
@@ -64,6 +571,8 @@ class MessageQueueManager(metaclass=SingletonClass):
for period in periods:
if not period:
continue
if not period.get('start') or not period.get('end'):
continue
start_h, start_m = map(int, period['start'].split(':'))
end_h, end_m = map(int, period['end'].split(':'))
parsed.append((start_h, start_m, end_h, end_m))

View File

@@ -3,18 +3,26 @@ import importlib
import pkgutil
import traceback
from pathlib import Path
from typing import List, Any
from typing import List, Any, Callable
from app.log import logger
FilterFuncType = Callable[[str, Any], bool]
def _default_filter(name: str, obj: Any) -> bool:
"""
默认过滤器
"""
return True
class ModuleHelper:
"""
模块动态加载
"""
@classmethod
def load(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]:
def load(cls, package_path: str, filter_func: FilterFuncType = _default_filter) -> List[Any]:
"""
导入模块
:param package_path: 父包名
@@ -46,7 +54,7 @@ class ModuleHelper:
return submodules
@classmethod
def load_with_pre_filter(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]:
def load_with_pre_filter(cls, package_path: str, filter_func: FilterFuncType = _default_filter) -> List[Any]:
"""
导入子模块
:param package_path: 父包名

View File

@@ -1,4 +1,5 @@
import base64
from typing import Optional
from app.core.config import settings
from app.utils.http import RequestUtils
@@ -8,7 +9,8 @@ class OcrHelper:
_ocr_b64_url = f"{settings.OCR_HOST}/captcha/base64"
def get_captcha_text(self, image_url=None, image_b64=None, cookie=None, ua=None):
def get_captcha_text(self, image_url: Optional[str] = None, image_b64: Optional[str] = None,
cookie: Optional[str] = None, ua: Optional[str] = None):
"""
根据图片地址,获取验证码图片,并识别内容
:param image_url: 图片地址

View File

@@ -39,7 +39,7 @@ class PluginHelper(metaclass=Singleton):
self.systemconfig.set(SystemConfigKey.PluginInstallReport, "1")
@cached(maxsize=1000, ttl=1800)
def get_plugins(self, repo_url: str, package_version: str = None) -> Optional[Dict[str, dict]]:
def get_plugins(self, repo_url: str, package_version: Optional[str] = None) -> Optional[Dict[str, dict]]:
"""
获取Github所有最新插件列表
:param repo_url: Github仓库地址
@@ -66,7 +66,7 @@ class PluginHelper(metaclass=Singleton):
return None
return {}
def get_plugin_package_version(self, pid: str, repo_url: str, package_version: str = None) -> Optional[str]:
def get_plugin_package_version(self, pid: str, repo_url: str, package_version: Optional[str] = None) -> Optional[str]:
"""
检查并获取指定插件的可用版本,支持多版本优先级加载和版本兼容性检测
1. 如果未指定版本,则使用系统配置的默认版本(通过 settings.VERSION_FLAG 设置)
@@ -157,7 +157,7 @@ class PluginHelper(metaclass=Singleton):
json={"plugins": [{"plugin_id": plugin} for plugin in plugins]})
return True if res else False
def install(self, pid: str, repo_url: str, package_version: str = None, force_install: bool = False) \
def install(self, pid: str, repo_url: str, package_version: Optional[str] = None, force_install: bool = False) \
-> Tuple[bool, str]:
"""
安装插件,包括依赖安装和文件下载,相关资源支持自动降级策略
@@ -260,7 +260,7 @@ class PluginHelper(metaclass=Singleton):
self.install_reg(pid)
return True, ""
def __get_file_list(self, pid: str, user_repo: str, package_version: str = None) -> \
def __get_file_list(self, pid: str, user_repo: str, package_version: Optional[str] = None) -> \
Tuple[Optional[list], Optional[str]]:
"""
获取插件的文件列表
@@ -295,7 +295,7 @@ class PluginHelper(metaclass=Singleton):
return None, "插件数据解析失败"
def __download_files(self, pid: str, file_list: List[dict], user_repo: str,
package_version: str = None, skip_requirements: bool = False) -> Tuple[bool, str]:
package_version: Optional[str] = None, skip_requirements: bool = False) -> Tuple[bool, str]:
"""
下载插件文件
:param pid: 插件 ID
@@ -480,7 +480,7 @@ class PluginHelper(metaclass=Singleton):
@staticmethod
def __request_with_fallback(url: str,
headers: Optional[dict] = None,
timeout: int = 60,
timeout: Optional[int] = 60,
is_api: bool = False) -> Optional[Any]:
"""
使用自动降级策略,请求资源,优先级依次为镜像站、代理、直连

View File

@@ -1,5 +1,5 @@
from enum import Enum
from typing import Union, Dict
from typing import Union, Dict, Optional
from app.schemas.types import ProgressKey
from app.utils.singleton import Singleton
@@ -40,7 +40,7 @@ class ProgressHelper(metaclass=Singleton):
"text": "正在处理..."
}
def update(self, key: Union[ProgressKey, str], value: float = None, text: str = None):
def update(self, key: Union[ProgressKey, str], value: Union[float, int] = None, text: Optional[str] = None):
if isinstance(key, Enum):
key = key.value
if not self._process_detail.get(key, {}).get('enable'):

View File

@@ -1,7 +1,7 @@
import re
import traceback
import xml.dom.minidom
from typing import List, Tuple, Union
from typing import List, Tuple, Union, Optional
from urllib.parse import urljoin
import chardet
@@ -225,7 +225,7 @@ class RssHelper:
}
@staticmethod
def parse(url, proxy: bool = False, timeout: int = 15, headers: dict = None) -> Union[List[dict], None, bool]:
def parse(url, proxy: bool = False, timeout: Optional[int] = 15, headers: dict = None) -> Union[List[dict], None, bool]:
"""
解析RSS订阅URL获取RSS中的种子信息
:param url: RSS地址
@@ -301,6 +301,8 @@ class RssHelper:
if pubdate:
# 转换为时间
pubdate = StringUtils.get_time(pubdate)
# 获取豆瓣昵称
nickname = DomUtils.tag_value(item, "dc:createor", default="")
# 返回对象
tmp_dict = {'title': title,
'enclosure': enclosure,
@@ -308,6 +310,9 @@ class RssHelper:
'description': description,
'link': link,
'pubdate': pubdate}
# 如果豆瓣昵称不为空返回数据增加豆瓣昵称供doubansync插件获取
if nickname:
tmp_dict['nickname'] = nickname
ret_array.append(tmp_dict)
except Exception as e1:
logger.debug(f"解析RSS失败{str(e1)} - {traceback.format_exc()}")

View File

@@ -50,3 +50,24 @@ class StorageHelper:
s.config = conf
break
self.systemconfig.set(SystemConfigKey.Storages, [s.dict() for s in storagies])
def add_storage(self, storage: str, name: str, conf: dict):
"""
添加存储配置
"""
storagies = self.get_storagies()
if not storagies:
storagies = [
schemas.StorageConf(
type=storage,
name=name,
config=conf
)
]
else:
storagies.append(schemas.StorageConf(
type=storage,
name=name,
config=conf
))
self.systemconfig.set(SystemConfigKey.Storages, [s.dict() for s in storagies])

View File

@@ -1,10 +1,11 @@
from threading import Thread
from typing import List, Tuple
from typing import List, Tuple, Optional
from app.core.cache import cached, cache_backend
from app.core.config import settings
from app.db.subscribe_oper import SubscribeOper
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.schemas.types import SystemConfigKey
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
@@ -32,16 +33,33 @@ class SubscribeHelper(metaclass=Singleton):
_shares_cache_region = "subscribe_share"
_github_user = None
_share_user_id = None
_admin_users = [
"jxxghp",
"thsrite",
"InfinityPacer",
"DDSRem",
"Aqr-K",
"Putarku",
"4Nest",
"xyswordzoro",
"wikrin"
]
def __init__(self):
self.systemconfig = SystemConfigOper()
self.share_user_id = SystemUtils.generate_user_unique_id()
if settings.SUBSCRIBE_STATISTIC_SHARE:
if not self.systemconfig.get(SystemConfigKey.SubscribeReport):
if self.sub_report():
self.systemconfig.set(SystemConfigKey.SubscribeReport, "1")
self.get_user_uuid()
self.get_github_user()
@cached(maxsize=20, ttl=1800)
def get_statistic(self, stype: str, page: int = 1, count: int = 30) -> List[dict]:
def get_statistic(self, stype: str, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
获取订阅统计数据
"""
@@ -135,7 +153,7 @@ class SubscribeHelper(metaclass=Singleton):
"share_title": share_title,
"share_comment": share_comment,
"share_user": share_user,
"share_uid": self.share_user_id,
"share_uid": self._share_user_id,
**subscribe_dict
})
if res is None:
@@ -155,7 +173,7 @@ class SubscribeHelper(metaclass=Singleton):
return False, "当前没有开启订阅数据共享功能"
res = RequestUtils(proxies=settings.PROXY,
timeout=5).delete_res(f"{self._sub_share}/{share_id}",
params={"share_uid": self.share_user_id})
params={"share_uid": self._share_user_id})
if res is None:
return False, "连接MoviePilot服务器失败"
if res.ok:
@@ -182,7 +200,7 @@ class SubscribeHelper(metaclass=Singleton):
return False, res.json().get("message")
@cached(region=_shares_cache_region)
def get_shares(self, name: str = None, page: int = 1, count: int = 30) -> List[dict]:
def get_shares(self, name: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
"""
获取订阅分享数据
"""
@@ -196,3 +214,35 @@ class SubscribeHelper(metaclass=Singleton):
if res and res.status_code == 200:
return res.json()
return []
def get_user_uuid(self) -> str:
"""
获取用户uuid
"""
if not self._share_user_id:
self._share_user_id = SystemUtils.generate_user_unique_id()
logger.info(f"当前用户UUID: {self._share_user_id}")
return self._share_user_id
def get_github_user(self) -> str:
"""
获取github用户
"""
if self._github_user is None and settings.GITHUB_HEADERS:
res = RequestUtils(headers=settings.GITHUB_HEADERS,
proxies=settings.PROXY,
timeout=15).get_res(f"https://api.github.com/user")
if res:
self._github_user = res.json().get("login")
logger.info(f"当前Github用户: {self._github_user}")
return self._github_user
def is_admin_user(self) -> bool:
"""
判断是否是管理员
"""
if not self._github_user:
return False
if self._github_user in self._admin_users:
return True
return False

View File

@@ -1,4 +1,5 @@
from concurrent.futures import ThreadPoolExecutor
from typing import Optional
from app.utils.singleton import Singleton
@@ -7,7 +8,7 @@ class ThreadHelper(metaclass=Singleton):
"""
线程池管理
"""
def __init__(self, max_workers=50):
def __init__(self, max_workers: Optional[int] = 50):
self.pool = ThreadPoolExecutor(max_workers=max_workers)
def submit(self, func, *args, **kwargs):

View File

@@ -33,10 +33,10 @@ class TorrentHelper(metaclass=Singleton):
self.site_oper = SiteOper()
def download_torrent(self, url: str,
cookie: str = None,
ua: str = None,
referer: str = None,
proxy: bool = False) \
cookie: Optional[str] = None,
ua: Optional[str] = None,
referer: Optional[str] = None,
proxy: Optional[bool] = False) \
-> Tuple[Optional[Path], Optional[Union[str, bytes]], Optional[str], Optional[list], Optional[str]]:
"""
把种子下载到本地

View File

@@ -29,7 +29,6 @@ class _ModuleBase(metaclass=ABCMeta):
pass
@staticmethod
@abstractmethod
def get_name() -> str:
"""
获取模块名称
@@ -37,7 +36,6 @@ class _ModuleBase(metaclass=ABCMeta):
pass
@staticmethod
@abstractmethod
def get_type() -> ModuleType:
"""
获取模块类型
@@ -45,7 +43,6 @@ class _ModuleBase(metaclass=ABCMeta):
pass
@staticmethod
@abstractmethod
def get_subtype() -> Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]:
"""
获取模块子类型(下载器、媒体服务器、消息通道、存储类型、其他杂项模块类型)
@@ -53,7 +50,6 @@ class _ModuleBase(metaclass=ABCMeta):
pass
@staticmethod
@abstractmethod
def get_priority() -> int:
"""
获取模块优先级,数字越小优先级越高,只有同一接口下优先级才生效

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from typing import Optional
import requests
@@ -31,7 +32,7 @@ class BangumiApi(object):
@classmethod
@cached(maxsize=settings.CACHE_CONF["bangumi"], ttl=settings.CACHE_CONF["meta"])
def __invoke(cls, url, key: str = None, **kwargs):
def __invoke(cls, url, key: Optional[str] = None, **kwargs):
req_url = cls._base_url + url
params = {}
if kwargs:

View File

@@ -75,8 +75,8 @@ class DoubanModule(_ModuleBase):
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
doubanid: str = None,
cache: bool = True,
doubanid: Optional[str] = None,
cache: Optional[bool] = True,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息

View File

@@ -4,6 +4,7 @@ import hashlib
import hmac
from datetime import datetime
from random import choice
from typing import Optional
from urllib import parse
import requests
@@ -18,7 +19,6 @@ class DoubanApi(metaclass=Singleton):
_urls = {
# 搜索类
# sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映
# q=search_word&start: int = 0&count: int = 20&sort=U
# 聚合搜索
"search": "/search/weixin",
"search_agg": "/search",
@@ -27,21 +27,18 @@ class DoubanApi(metaclass=Singleton):
# 电影探索
# sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间
# tags='日本,动画,2022'&start: int = 0&count: int = 20&sort=U
"movie_recommend": "/movie/recommend",
# 电视剧探索
"tv_recommend": "/tv/recommend",
# 搜索
"movie_tag": "/movie/tag",
"tv_tag": "/tv/tag",
# q=search_word&start: int = 0&count: int = 20
"movie_search": "/search/movie",
"tv_search": "/search/movie",
"book_search": "/search/book",
"group_search": "/search/group",
# 各类主题合集
# start: int = 0&count: int = 20
# 正在上映
"movie_showing": "/subject_collection/movie_showing/items",
# 热门电影
@@ -252,7 +249,7 @@ class DoubanApi(metaclass=Singleton):
"""
return self.__post(self._urls["imdbid"] % imdbid, _ts=ts)
def search(self, keyword: str, start: int = 0, count: int = 20,
def search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')) -> dict:
"""
关键字搜索
@@ -260,7 +257,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["search"], q=keyword,
start=start, count=count, _ts=ts)
def movie_search(self, keyword: str, start: int = 0, count: int = 20,
def movie_search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影搜索
@@ -268,7 +265,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["movie_search"], q=keyword,
start=start, count=count, _ts=ts)
def tv_search(self, keyword: str, start: int = 0, count: int = 20,
def tv_search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视搜索
@@ -276,7 +273,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["tv_search"], q=keyword,
start=start, count=count, _ts=ts)
def book_search(self, keyword: str, start: int = 0, count: int = 20,
def book_search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
书籍搜索
@@ -284,7 +281,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["book_search"], q=keyword,
start=start, count=count, _ts=ts)
def group_search(self, keyword: str, start: int = 0, count: int = 20,
def group_search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
小组搜索
@@ -292,7 +289,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["group_search"], q=keyword,
start=start, count=count, _ts=ts)
def person_search(self, keyword: str, start: int = 0, count: int = 20,
def person_search(self, keyword: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
人物搜索
@@ -300,7 +297,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["search_subject"], type="person", q=keyword,
start=start, count=count, _ts=ts)
def movie_showing(self, start: int = 0, count: int = 20,
def movie_showing(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
正在热映
@@ -308,7 +305,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_showing"],
start=start, count=count, _ts=ts)
def movie_soon(self, start: int = 0, count: int = 20,
def movie_soon(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
即将上映
@@ -316,7 +313,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_soon"],
start=start, count=count, _ts=ts)
def movie_hot_gaia(self, start: int = 0, count: int = 20,
def movie_hot_gaia(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门电影
@@ -324,7 +321,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_hot_gaia"],
start=start, count=count, _ts=ts)
def tv_hot(self, start: int = 0, count: int = 20,
def tv_hot(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门剧集
@@ -332,7 +329,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_hot"],
start=start, count=count, _ts=ts)
def tv_animation(self, start: int = 0, count: int = 20,
def tv_animation(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
动画
@@ -340,7 +337,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_animation"],
start=start, count=count, _ts=ts)
def tv_variety_show(self, start: int = 0, count: int = 20,
def tv_variety_show(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺
@@ -348,7 +345,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_variety_show"],
start=start, count=count, _ts=ts)
def tv_rank_list(self, start: int = 0, count: int = 20,
def tv_rank_list(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧排行榜
@@ -356,7 +353,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_rank_list"],
start=start, count=count, _ts=ts)
def show_hot(self, start: int = 0, count: int = 20,
def show_hot(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺热门
@@ -394,7 +391,7 @@ class DoubanApi(metaclass=Singleton):
"""
return self.__invoke_search(self._urls["book_detail"] + subject_id)
def movie_top250(self, start: int = 0, count: int = 20,
def movie_top250(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影TOP250
@@ -402,7 +399,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_top250"],
start=start, count=count, _ts=ts)
def movie_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
def movie_recommend(self, tags='', sort='R', start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影探索
@@ -410,7 +407,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
def tv_recommend(self, tags='', sort='R', start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧探索
@@ -418,7 +415,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_chinese_best_weekly(self, start: int = 0, count: int = 20,
def tv_chinese_best_weekly(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
华语口碑周榜
@@ -426,7 +423,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_chinese_best_weekly"],
start=start, count=count, _ts=ts)
def tv_global_best_weekly(self, start: int = 0, count: int = 20,
def tv_global_best_weekly(self, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
全球口碑周榜
@@ -441,7 +438,7 @@ class DoubanApi(metaclass=Singleton):
"""
return self.__invoke_search(self._urls["doulist"] + subject_id)
def doulist_items(self, subject_id: str, start: int = 0, count: int = 20,
def doulist_items(self, subject_id: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
豆列列表
@@ -453,7 +450,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["doulist_items"] % subject_id,
start=start, count=count, _ts=ts)
def movie_recommendations(self, subject_id: str, start: int = 0, count: int = 20,
def movie_recommendations(self, subject_id: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影推荐
@@ -465,7 +462,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["movie_recommendations"] % subject_id,
start=start, count=count, _ts=ts)
def tv_recommendations(self, subject_id: str, start: int = 0, count: int = 20,
def tv_recommendations(self, subject_id: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧推荐
@@ -477,7 +474,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_recommend(self._urls["tv_recommendations"] % subject_id,
start=start, count=count, _ts=ts)
def movie_photos(self, subject_id: str, start: int = 0, count: int = 20,
def movie_photos(self, subject_id: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影剧照
@@ -489,7 +486,7 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke_search(self._urls["movie_photos"] % subject_id,
start=start, count=count, _ts=ts)
def tv_photos(self, subject_id: str, start: int = 0, count: int = 20,
def tv_photos(self, subject_id: str, start: Optional[int] = 0, count: Optional[int] = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧剧照
@@ -509,8 +506,9 @@ class DoubanApi(metaclass=Singleton):
"""
return self.__invoke_search(self._urls["person_detail"] + str(subject_id))
def person_work(self, subject_id: int, start: int = 0, count: int = 20, sort_by: str = "time",
collection_title: str = "影视",
def person_work(self, subject_id: int, start: Optional[int] = 0, count: Optional[int] = 20,
sort_by: Optional[str] = "time",
collection_title: Optional[str] = "影视",
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
用户作品集

View File

@@ -165,7 +165,7 @@ class DoubanCache(metaclass=Singleton):
# None时不缓存此时代表网络错误允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': "0"}
def save(self, force: bool = False) -> None:
def save(self, force: Optional[bool] = False) -> None:
"""
保存缓存数据到文件
"""

View File

@@ -11,7 +11,7 @@ class DoubanScraper:
_force_nfo = False
_force_img = False
def get_metadata_nfo(self, mediainfo: MediaInfo, season: int = None) -> Optional[str]:
def get_metadata_nfo(self, mediainfo: MediaInfo, season: Optional[int] = None) -> Optional[str]:
"""
获取NFO文件内容文本
:param mediainfo: 媒体信息
@@ -33,7 +33,7 @@ class DoubanScraper:
return None
@staticmethod
def get_metadata_img(mediainfo: MediaInfo, season: int = None, episode: int = None) -> Optional[dict]:
def get_metadata_img(mediainfo: MediaInfo, season: Optional[int] = None, episode: Optional[int] = None) -> Optional[dict]:
"""
获取图片内容
:param mediainfo: 媒体信息

View File

@@ -135,8 +135,8 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
return result
return None
def media_exists(self, mediainfo: MediaInfo, itemid: str = None,
server: str = None) -> Optional[schemas.ExistMediaInfo]:
def media_exists(self, mediainfo: MediaInfo, itemid: Optional[str] = None,
server: Optional[str] = None) -> Optional[schemas.ExistMediaInfo]:
"""
判断媒体文件是否存在
:param mediainfo: 识别的媒体信息
@@ -148,12 +148,12 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
servers = [(server, self.get_instance(server))]
else:
servers = self.get_instances().items()
for name, server in servers:
if not server:
for name, s in servers:
if not s:
continue
if mediainfo.type == MediaType.MOVIE:
if itemid:
movie = server.get_iteminfo(itemid)
movie = s.get_iteminfo(itemid)
if movie:
logger.info(f"媒体库 {name} 中找到了 {movie}")
return schemas.ExistMediaInfo(
@@ -162,9 +162,9 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
server=name,
itemid=movie.item_id
)
movies = server.get_movies(title=mediainfo.title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id)
movies = s.get_movies(title=mediainfo.title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id)
if not movies:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
@@ -177,10 +177,10 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
itemid=movies[0].item_id
)
else:
itemid, tvs = server.get_tv_episodes(title=mediainfo.title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id,
item_id=itemid)
itemid, tvs = s.get_tv_episodes(title=mediainfo.title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id,
item_id=itemid)
if not tvs:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
@@ -195,7 +195,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
)
return None
def media_statistic(self, server: str = None) -> Optional[List[schemas.Statistic]]:
def media_statistic(self, server: Optional[str] = None) -> Optional[List[schemas.Statistic]]:
"""
媒体数量统计
"""
@@ -207,17 +207,17 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
else:
servers = self.get_instances().values()
media_statistics = []
for server in servers:
media_statistic = server.get_medias_count()
for s in servers:
media_statistic = s.get_medias_count()
if not media_statistic:
continue
media_statistic.user_count = server.get_user_count()
media_statistic.user_count = s.get_user_count()
media_statistics.append(media_statistic)
return media_statistics
def mediaserver_librarys(self, server: str,
username: str = None,
hidden: bool = False) -> Optional[List[schemas.MediaServerLibrary]]:
username: Optional[str] = None,
hidden: Optional[bool] = False) -> Optional[List[schemas.MediaServerLibrary]]:
"""
媒体库列表
"""
@@ -226,7 +226,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
return server_obj.get_librarys(username=username, hidden=hidden)
return None
def mediaserver_items(self, server: str, library_id: Union[str, int], start_index: int = 0,
def mediaserver_items(self, server: str, library_id: Union[str, int], start_index: Optional[int] = 0,
limit: Optional[int] = -1) -> Optional[Generator]:
"""
获取媒体服务器项目列表,支持分页和不分页逻辑,默认不分页获取所有数据
@@ -269,7 +269,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
) for season, episodes in seasoninfo.items()]
def mediaserver_playing(self, server: str,
count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
count: Optional[int] = 20, username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
"""
获取媒体服务器正在播放信息
"""
@@ -287,8 +287,8 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
return None
return server_obj.get_play_url(item_id)
def mediaserver_latest(self, server: str = None,
count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
def mediaserver_latest(self, server: Optional[str] = None,
count: Optional[int] = 20, username: Optional[str] = None) -> List[schemas.MediaServerPlayItem]:
"""
获取媒体服务器最新入库条目
"""
@@ -298,10 +298,10 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
return server_obj.get_latest(num=count, username=username)
def mediaserver_latest_images(self,
server: str = None,
count: int = 10,
username: str = None,
remote: bool = False
server: Optional[str] = None,
count: Optional[int] = 10,
username: Optional[str] = None,
remote: Optional[bool] = False
) -> List[str]:
"""
获取媒体服务器最新入库条目的图片

View File

@@ -17,13 +17,13 @@ from schemas import MediaServerItem
class Emby:
_host: str = None
_playhost: str = None
_apikey: str = None
_host: Optional[str] = None
_playhost: Optional[str] = None
_apikey: Optional[str] = None
_sync_libraries: List[str] = []
user: Optional[Union[str, int]] = None
def __init__(self, host: str = None, apikey: str = None, play_host: str = None,
def __init__(self, host: Optional[str] = None, apikey: Optional[str] = None, play_host: Optional[str] = None,
sync_libraries: list = None, **kwargs):
if not host or not apikey:
logger.error("Emby服务器配置不完整")
@@ -116,7 +116,7 @@ class Emby:
logger.error(f"连接Library/VirtualFolders/Query 出错:" + str(e))
return []
def __get_emby_librarys(self, username: str = None) -> List[dict]:
def __get_emby_librarys(self, username: Optional[str] = None) -> List[dict]:
"""
获取Emby媒体库列表
"""
@@ -139,7 +139,7 @@ class Emby:
logger.error(f"连接User/Views 出错:" + str(e))
return []
def get_librarys(self, username: str = None, hidden: bool = False) -> List[schemas.MediaServerLibrary]:
def get_librarys(self, username: Optional[str] = None, hidden: Optional[bool] = False) -> List[schemas.MediaServerLibrary]:
"""
获取媒体服务器所有媒体库列表
"""
@@ -150,13 +150,12 @@ class Emby:
if hidden and self._sync_libraries and "all" not in self._sync_libraries \
and library.get("Id") not in self._sync_libraries:
continue
match library.get("CollectionType"):
case "movies":
library_type = MediaType.MOVIE.value
case "tvshows":
library_type = MediaType.TV.value
case _:
library_type = MediaType.UNKNOWN.value
if library.get("CollectionType") == "movies":
library_type = MediaType.MOVIE.value
elif library.get("CollectionType") == "tvshows":
library_type = MediaType.TV.value
else:
library_type = MediaType.UNKNOWN.value
image = self.__get_local_image_by_id(library.get("Id"))
libraries.append(
schemas.MediaServerLibrary(
@@ -172,7 +171,7 @@ class Emby:
)
return libraries
def get_user(self, user_name: str = None) -> Optional[Union[str, int]]:
def get_user(self, user_name: Optional[str] = None) -> Optional[Union[str, int]]:
"""
获得管理员用户
"""
@@ -343,8 +342,8 @@ class Emby:
def get_movies(self,
title: str,
year: str = None,
tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]:
year: Optional[str] = None,
tmdb_id: Optional[int] = None) -> Optional[List[schemas.MediaServerItem]]:
"""
根据标题和年份检查电影是否在Emby中存在存在则返回列表
:param title: 标题
@@ -387,11 +386,11 @@ class Emby:
return []
def get_tv_episodes(self,
item_id: str = None,
title: str = None,
year: str = None,
tmdb_id: int = None,
season: int = None
item_id: Optional[str] = None,
title: Optional[str] = None,
year: Optional[str] = None,
tmdb_id: Optional[int] = None,
season: Optional[int] = None
) -> Tuple[Optional[str], Optional[Dict[int, List[int]]]]:
"""
根据标题和年份和季返回Emby中的剧集列表
@@ -419,7 +418,7 @@ class Emby:
return None, {}
# 查集的信息
if not season:
season = ""
season = None
try:
url = f"{self._host}emby/Shows/{item_id}/Episodes"
params = {
@@ -669,7 +668,7 @@ class Emby:
logger.error(f"连接/Users/{self.user}/Items/{itemid}出错:" + str(e))
return None
def get_items(self, parent: Union[str, int], start_index: int = 0,
def get_items(self, parent: Union[str, int], start_index: Optional[int] = 0,
limit: Optional[int] = -1) -> Generator[MediaServerItem | None | Any, Any, None]:
"""
获取媒体服务器项目列表,支持分页和不分页逻辑,默认不分页获取所有数据
@@ -1032,6 +1031,8 @@ class Emby:
eventItem.image_url = self.get_remote_image_by_id(item_id=eventItem.item_id,
image_type="Backdrop")
eventItem.json_object = message
return eventItem
def get_data(self, url: str) -> Optional[Response]:
@@ -1050,7 +1051,7 @@ class Emby:
logger.error(f"连接Emby出错" + str(e))
return None
def post_data(self, url: str, data: str = None, headers: dict = None) -> Optional[Response]:
def post_data(self, url: str, data: Optional[str] = None, headers: dict = None) -> Optional[Response]:
"""
自定义URL从媒体服务器获取数据其中[HOST]、[APIKEY]、[USER]会被替换成实际的值
:param url: 请求地址
@@ -1078,7 +1079,7 @@ class Emby:
return f"{self._playhost or self._host}web/index.html#!" \
f"/item?id={item_id}&context=home&serverId={self.serverid}"
def get_backdrop_url(self, item_id: str, image_tag: str, remote: bool = False) -> str:
def get_backdrop_url(self, item_id: str, image_tag: str, remote: Optional[bool] = False) -> str:
"""
获取Emby的Backdrop图片地址
:param: item_id: 在Emby中的ID
@@ -1107,7 +1108,7 @@ class Emby:
return ""
return "%sItems/%s/Images/Primary" % (self._host, item_id)
def get_resume(self, num: int = 12, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
def get_resume(self, num: Optional[int] = 12, username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
"""
获得继续观看
"""
@@ -1175,7 +1176,7 @@ class Emby:
logger.error(f"连接Users/Items/Resume出错" + str(e))
return []
def get_latest(self, num: int = 20, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
def get_latest(self, num: Optional[int] = 20, username: Optional[str] = None) -> Optional[List[schemas.MediaServerPlayItem]]:
"""
获得最近更新
"""

View File

@@ -1,7 +1,7 @@
import re
from pathlib import Path
from threading import Lock
from typing import Optional, List, Tuple, Union, Dict
from typing import Optional, List, Tuple, Union, Dict, Callable
from jinja2 import Template
@@ -11,7 +11,7 @@ from app.core.event import eventmanager
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo, MetaInfoPath
from app.helper.directory import DirectoryHelper
from app.helper.message import MessageHelper
from app.helper.message import MessageHelper, TemplateHelper
from app.helper.module import ModuleHelper
from app.log import logger
from app.modules import _ModuleBase
@@ -30,6 +30,7 @@ class FileManagerModule(_ModuleBase):
"""
_storage_schemas = []
_support_storages = []
def __init__(self):
super().__init__()
@@ -40,6 +41,8 @@ class FileManagerModule(_ModuleBase):
# 加载模块
self._storage_schemas = ModuleHelper.load('app.modules.filemanager.storages',
filter_func=lambda _, obj: hasattr(obj, 'schema') and obj.schema)
# 获取存储类型
self._support_storages = [storage.schema.value for storage in self._storage_schemas]
@staticmethod
def get_name() -> str:
@@ -114,6 +117,8 @@ class FileManagerModule(_ModuleBase):
"""
支持的整理方式
"""
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的整理方式获取")
@@ -169,19 +174,21 @@ class FileManagerModule(_ModuleBase):
return None
return storage_oper.check_login(**kwargs)
def list_files(self, fileitem: FileItem, recursion: bool = False) -> Optional[List[FileItem]]:
def list_files(self, fileitem: FileItem, recursion: Optional[bool] = False) -> Optional[List[FileItem]]:
"""
浏览文件
:param fileitem: 源文件
:param recursion: 是否递归,此时只浏览文件
:return: 文件项列表
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件浏览")
return None
def __get_files(_item: FileItem, _r: bool = False):
def __get_files(_item: FileItem, _r: Optional[bool] = False):
"""
递归处理
"""
@@ -206,6 +213,8 @@ class FileManagerModule(_ModuleBase):
"""
查询当前目录下是否存在指定扩展名任意文件
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件浏览")
@@ -239,26 +248,32 @@ class FileManagerModule(_ModuleBase):
:param name: 目录名
:return: 创建的目录
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的目录创建")
return None
return storage_oper.create_folder(fileitem, name)
def delete_file(self, fileitem: FileItem) -> bool:
def delete_file(self, fileitem: FileItem) -> Optional[bool]:
"""
删除文件或目录
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的删除处理")
return False
return storage_oper.delete(fileitem)
def rename_file(self, fileitem: FileItem, name: str) -> bool:
def rename_file(self, fileitem: FileItem, name: str) -> Optional[bool]:
"""
重命名文件或目录
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的重命名处理")
@@ -269,16 +284,20 @@ class FileManagerModule(_ModuleBase):
"""
下载文件
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的下载处理")
return None
return storage_oper.download(fileitem, path=path)
def upload_file(self, fileitem: FileItem, path: Path, new_name: str = None) -> Optional[FileItem]:
def upload_file(self, fileitem: FileItem, path: Path, new_name: Optional[str] = None) -> Optional[FileItem]:
"""
上传文件
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的上传处理")
@@ -289,6 +308,8 @@ class FileManagerModule(_ModuleBase):
"""
根据路径获取文件项
"""
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的文件获取")
@@ -299,6 +320,8 @@ class FileManagerModule(_ModuleBase):
"""
获取上级目录项
"""
if fileitem.storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件获取")
@@ -309,6 +332,8 @@ class FileManagerModule(_ModuleBase):
"""
快照存储
"""
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的快照处理")
@@ -319,6 +344,8 @@ class FileManagerModule(_ModuleBase):
"""
存储使用情况
"""
if storage not in self._support_storages:
return None
storage_oper = self.__get_storage_oper(storage)
if not storage_oper:
logger.error(f"不支持 {storage} 的存储使用情况")
@@ -327,10 +354,11 @@ class FileManagerModule(_ModuleBase):
def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: MediaInfo,
target_directory: TransferDirectoryConf = None,
target_storage: str = None, target_path: Path = None,
transfer_type: str = None, scrape: bool = None,
library_type_folder: bool = None, library_category_folder: bool = None,
episodes_info: List[TmdbEpisode] = None) -> TransferInfo:
target_storage: Optional[str] = None, target_path: Path = None,
transfer_type: Optional[str] = None, scrape: Optional[bool] = None,
library_type_folder: Optional[bool] = None, library_category_folder: Optional[bool] = None,
episodes_info: List[TmdbEpisode] = None,
source_oper: Callable = None, target_oper: Callable = None) -> TransferInfo:
"""
文件整理
:param fileitem: 文件信息
@@ -344,6 +372,8 @@ class FileManagerModule(_ModuleBase):
:param library_type_folder: 是否按媒体类型创建目录
:param library_category_folder: 是否按媒体类别创建目录
:param episodes_info: 当前季的全部集信息
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:return: {path, target_path, message}
"""
# 检查目录路径
@@ -370,9 +400,6 @@ class FileManagerModule(_ModuleBase):
overwrite_mode = target_directory.overwrite_mode
# 是否需要刮削
need_scrape = target_directory.scraping if scrape is None else scrape
# 目标存储类型
if not target_storage:
target_storage = target_directory.library_storage
# 拼装媒体库一、二级子目录
target_path = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_directory,
need_type_folder=library_type_folder,
@@ -399,6 +426,29 @@ class FileManagerModule(_ModuleBase):
return TransferInfo(success=False,
fileitem=fileitem,
message=f"{target_directory.name} 未设置整理方式")
# 源操作对象
if not source_oper:
source_oper = self.__get_storage_oper(fileitem.storage)
if not source_oper:
return TransferInfo(success=False,
message=f"不支持的存储类型:{fileitem.storage}",
fileitem=fileitem,
fail_list=[fileitem.path],
transfer_type=transfer_type,
need_notify=need_notify
)
# 目的操作对象
if not target_oper:
target_oper = self.__get_storage_oper(target_storage)
if not target_oper:
return TransferInfo(success=False,
message=f"不支持的存储类型:{target_storage}",
fileitem=fileitem,
fail_list=[fileitem.path],
transfer_type=transfer_type,
need_notify=need_notify)
# 整理
logger.info(f"获取整理目标路径:【{target_storage}{target_path}")
return self.transfer_media(fileitem=fileitem,
@@ -411,9 +461,11 @@ class FileManagerModule(_ModuleBase):
need_rename=need_rename,
need_notify=need_notify,
overwrite_mode=overwrite_mode,
episodes_info=episodes_info)
episodes_info=episodes_info,
source_oper=source_oper,
target_oper=target_oper)
def __get_storage_oper(self, _storage: str, _func: str = None) -> Optional[StorageBase]:
def __get_storage_oper(self, _storage: str, _func: Optional[str] = None) -> Optional[StorageBase]:
"""
获取存储操作对象
"""
@@ -430,12 +482,17 @@ class FileManagerModule(_ModuleBase):
"""
pass
def __transfer_command(self, fileitem: FileItem, target_storage: str,
target_file: Path, transfer_type: str) -> Tuple[Optional[FileItem], str]:
@staticmethod
def __transfer_command(fileitem: FileItem, target_storage: str,
source_oper: StorageBase, target_oper: StorageBase,
target_file: Path, transfer_type: str,
) -> Tuple[Optional[FileItem], str]:
"""
处理单个文件
:param fileitem: 源文件
:param target_storage: 目标存储
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_file: 目标文件路径
:param transfer_type: 整理方式
"""
@@ -459,13 +516,6 @@ class FileManagerModule(_ModuleBase):
and fileitem.storage != "local" and target_storage != "local"):
return None, f"不支持 {fileitem.storage}{target_storage} 的文件整理"
# 源操作对象
source_oper: StorageBase = self.__get_storage_oper(fileitem.storage)
# 目的操作对象
target_oper: StorageBase = self.__get_storage_oper(target_storage)
if not source_oper or not target_oper:
return None, f"不支持的存储类型:{fileitem.storage}{target_storage}"
# 加锁
with lock:
if fileitem.storage == "local" and target_storage == "local":
@@ -568,18 +618,23 @@ class FileManagerModule(_ModuleBase):
return None, "未知错误"
def __transfer_other_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
transfer_type: str) -> Tuple[bool, str]:
def __transfer_other_files(self, fileitem: FileItem, target_storage: str,
source_oper: StorageBase, target_oper: StorageBase,
target_file: Path, transfer_type: str) -> Tuple[bool, str]:
"""
根据文件名整理其他相关文件
:param fileitem: 源文件
:param target_storage: 目标存储
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_file: 目标路径
:param transfer_type: 整理方式
"""
# 整理字幕
state, errmsg = self.__transfer_subtitles(fileitem=fileitem,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=target_file,
transfer_type=transfer_type)
if not state:
@@ -587,17 +642,22 @@ class FileManagerModule(_ModuleBase):
# 整理音轨文件
state, errmsg = self.__transfer_audio_track_files(fileitem=fileitem,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=target_file,
transfer_type=transfer_type)
return state, errmsg
def __transfer_subtitles(self, fileitem: FileItem, target_storage: str, target_file: Path,
transfer_type: str) -> Tuple[bool, str]:
def __transfer_subtitles(self, fileitem: FileItem, target_storage: str,
source_oper: StorageBase, target_oper: StorageBase,
target_file: Path, transfer_type: str) -> Tuple[bool, str]:
"""
根据文件名整理对应字幕文件
:param fileitem: 源文件
:param target_storage: 目标存储
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_file: 目标路径
:param transfer_type: 整理方式
"""
@@ -617,17 +677,12 @@ class FileManagerModule(_ModuleBase):
# 比对文件名并整理字幕
org_path = Path(fileitem.path)
# 列出所有字幕文件
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件整理")
return False, f"不支持的文件存储:{fileitem.storage}"
# 查找上级文件项
parent_item: FileItem = storage_oper.get_parent(fileitem)
parent_item: FileItem = source_oper.get_parent(fileitem)
if not parent_item:
return False, f"{org_path} 上级目录获取失败"
# 字幕文件列表
file_list: List[FileItem] = storage_oper.list(parent_item) or []
file_list: List[FileItem] = source_oper.list(parent_item) or []
file_list = [f for f in file_list if f.type == "file" and f.extension
and f".{f.extension.lower()}" in settings.RMT_SUBEXT]
if len(file_list) == 0:
@@ -677,9 +732,9 @@ class FileManagerModule(_ModuleBase):
}
new_sub_tag_list = [
(".default" + new_file_type if (
(settings.DEFAULT_SUB == "zh-cn" and new_file_type == ".chi.zh-cn") or
(settings.DEFAULT_SUB == "zh-tw" and new_file_type == ".zh-tw") or
(settings.DEFAULT_SUB == "eng" and new_file_type == ".eng")
(settings.DEFAULT_SUB == "zh-cn" and new_file_type == ".chi.zh-cn") or
(settings.DEFAULT_SUB == "zh-tw" and new_file_type == ".zh-tw") or
(settings.DEFAULT_SUB == "eng" and new_file_type == ".eng")
) else new_file_type) if t == 0 else "%s%s(%s)" % (new_file_type,
new_sub_tag_dict.get(
new_file_type, ""
@@ -693,6 +748,8 @@ class FileManagerModule(_ModuleBase):
logger.debug(f"正在处理字幕:{sub_item.name}")
new_item, errmsg = self.__transfer_command(fileitem=sub_item,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=new_file,
transfer_type=transfer_type)
if new_item:
@@ -705,26 +762,24 @@ class FileManagerModule(_ModuleBase):
logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}")
return True, ""
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
transfer_type: str) -> Tuple[bool, str]:
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str,
source_oper: StorageBase, target_oper: StorageBase,
target_file: Path, transfer_type: str) -> Tuple[bool, str]:
"""
根据文件名整理对应音轨文件
:param fileitem: 源文件
:param target_storage: 目标存储
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_file: 目标路径
:param transfer_type: 整理方式
"""
org_path = Path(fileitem.path)
# 列出所有音轨文件
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件整理")
return False, f"不支持的文件存储:{fileitem.storage}"
# 查找上级文件项
parent_item: FileItem = storage_oper.get_parent(fileitem)
parent_item: FileItem = source_oper.get_parent(fileitem)
if not parent_item:
return False, f"{org_path} 上级目录获取失败"
file_list: List[FileItem] = storage_oper.list(parent_item)
file_list: List[FileItem] = source_oper.list(parent_item)
# 匹配音轨文件
pending_file_list: List[FileItem] = [file for file in file_list
if Path(file.name).stem == org_path.stem
@@ -740,6 +795,8 @@ class FileManagerModule(_ModuleBase):
logger.info(f"正在整理音轨文件:{track_file}{new_track_file}")
new_item, errmsg = self.__transfer_command(fileitem=track_file,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=new_track_file,
transfer_type=transfer_type)
if new_item:
@@ -750,21 +807,19 @@ class FileManagerModule(_ModuleBase):
logger.error(f"音轨文件 {org_path.name} 整理失败:{str(error)}")
return True, ""
def __transfer_dir(self, fileitem: FileItem, mediainfo: MediaInfo, transfer_type: str,
target_storage: str, target_path: Path) -> Tuple[Optional[FileItem], str]:
def __transfer_dir(self, fileitem: FileItem, mediainfo: MediaInfo,
source_oper: StorageBase, target_oper: StorageBase,
transfer_type: str, target_storage: str, target_path: Path) -> Tuple[Optional[FileItem], str]:
"""
整理整个文件夹
:param fileitem: 源文件
:param mediainfo: 媒体信息
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param transfer_type: 整理方式
:param target_storage: 目标存储
:param target_path: 目标路径
"""
# 获取目标目录
target_oper: StorageBase = self.__get_storage_oper(target_storage)
if not target_oper:
return None, f"不支持的文件存储:{target_storage}"
logger.info(f"正在整理目录:{fileitem.path}{target_path}")
target_item = target_oper.get_folder(target_path)
if not target_item:
@@ -788,6 +843,8 @@ class FileManagerModule(_ModuleBase):
# 处理所有文件
state, errmsg = self.__transfer_dir_files(fileitem=fileitem,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_path=target_path,
transfer_type=transfer_type)
if state:
@@ -795,29 +852,29 @@ class FileManagerModule(_ModuleBase):
else:
return None, errmsg
def __transfer_dir_files(self, fileitem: FileItem, transfer_type: str,
target_storage: str, target_path: Path) -> Tuple[bool, str]:
def __transfer_dir_files(self, fileitem: FileItem, target_storage: str,
source_oper: StorageBase, target_oper: StorageBase,
transfer_type: str, target_path: Path) -> Tuple[bool, str]:
"""
按目录结构整理目录下所有文件
:param fileitem: 源文件
:param target_storage: 目标存储
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_path: 目标路径
:param transfer_type: 整理方式
"""
# 列出所有文件
storage_oper = self.__get_storage_oper(fileitem.storage)
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的文件整理")
return False, f"不支持的文件存储:{fileitem.storage}"
file_list: List[FileItem] = storage_oper.list(fileitem)
file_list: List[FileItem] = source_oper.list(fileitem)
# 整理文件
for item in file_list:
if item.type == "dir":
# 递归整理目录
new_path = target_path / item.name
state, errmsg = self.__transfer_dir_files(fileitem=item,
transfer_type=transfer_type,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
transfer_type=transfer_type,
target_path=new_path)
if not state:
return False, errmsg
@@ -826,6 +883,8 @@ class FileManagerModule(_ModuleBase):
new_file = target_path / item.name
new_item, errmsg = self.__transfer_command(fileitem=item,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=new_file,
transfer_type=transfer_type)
if not new_item:
@@ -833,16 +892,23 @@ class FileManagerModule(_ModuleBase):
# 返回成功
return True, ""
def __transfer_file(self, fileitem: FileItem, mediainfo: MediaInfo, target_storage: str, target_file: Path,
transfer_type: str, over_flag: bool = False) -> Tuple[Optional[FileItem], str]:
def __transfer_file(self, fileitem: FileItem, mediainfo: MediaInfo,
source_oper: StorageBase, target_oper: StorageBase,
target_storage: str, target_file: Path,
transfer_type: str, over_flag: Optional[bool] = False) -> Tuple[
Optional[FileItem], str]:
"""
整理一个文件,同时处理其他相关文件
:param fileitem: 原文件
:param mediainfo: 媒体信息
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param target_storage: 目标存储
:param target_file: 新文件
:param transfer_type: 整理方式
:param over_flag: 是否覆盖为True时会先删除再整理
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
"""
logger.info(f"正在整理文件:【{fileitem.storage}{fileitem.path} 到 【{target_storage}{target_file}"
f"操作类型:{transfer_type}")
@@ -874,12 +940,16 @@ class FileManagerModule(_ModuleBase):
target_file.unlink()
new_item, errmsg = self.__transfer_command(fileitem=fileitem,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=target_file,
transfer_type=transfer_type)
if new_item:
# 处理其他相关文件
self.__transfer_other_files(fileitem=fileitem,
target_storage=target_storage,
source_oper=source_oper,
target_oper=target_oper,
target_file=target_file,
transfer_type=transfer_type)
return new_item, errmsg
@@ -888,7 +958,7 @@ class FileManagerModule(_ModuleBase):
@staticmethod
def __get_dest_path(mediainfo: MediaInfo, target_path: Path,
need_type_folder: bool = False, need_category_folder: bool = False):
need_type_folder: Optional[bool] = False, need_category_folder: Optional[bool] = False):
"""
获取目标路径
"""
@@ -900,7 +970,7 @@ class FileManagerModule(_ModuleBase):
@staticmethod
def __get_dest_dir(mediainfo: MediaInfo, target_dir: TransferDirectoryConf,
need_type_folder: bool = None, need_category_folder: bool = None) -> Path:
need_type_folder: Optional[bool] = None, need_category_folder: Optional[bool] = None) -> Path:
"""
根据设置并装媒体库目录
:param mediainfo: 媒体信息
@@ -936,11 +1006,13 @@ class FileManagerModule(_ModuleBase):
target_storage: str,
target_path: Path,
transfer_type: str,
need_scrape: bool = False,
need_rename: bool = True,
need_notify: bool = True,
overwrite_mode: str = None,
episodes_info: List[TmdbEpisode] = None,
source_oper: StorageBase,
target_oper: StorageBase,
need_scrape: Optional[bool] = False,
need_rename: Optional[bool] = True,
need_notify: Optional[bool] = True,
overwrite_mode: Optional[str] = None,
episodes_info: List[TmdbEpisode] = None
) -> TransferInfo:
"""
识别并整理一个文件或者一个目录下的所有文件
@@ -950,6 +1022,8 @@ class FileManagerModule(_ModuleBase):
:param target_storage: 目标存储
:param target_path: 目标路径
:param transfer_type: 文件整理方式
:param source_oper: 源存储操作对象
:param target_oper: 目标存储操作对象
:param need_scrape: 是否需要刮削
:param need_rename: 是否需要重命名
:param need_notify: 是否需要通知
@@ -977,6 +1051,8 @@ class FileManagerModule(_ModuleBase):
# 整理目录
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
mediainfo=mediainfo,
source_oper=source_oper,
target_oper=target_oper,
target_storage=target_storage,
target_path=new_path,
transfer_type=transfer_type)
@@ -1040,8 +1116,6 @@ class FileManagerModule(_ModuleBase):
# 判断是否要覆盖
overflag = False
# 目的操作对象
target_oper: StorageBase = self.__get_storage_oper(target_storage)
# 计算重命名中的文件夹层级
rename_format_level = len(rename_format.split("/")) - 1
folder_path = new_file.parents[rename_format_level - 1]
@@ -1067,50 +1141,51 @@ class FileManagerModule(_ModuleBase):
if not overflag:
# 目标文件已存在
logger.info(f"目的文件系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}")
match overwrite_mode:
case 'always':
# 总是覆盖同名文件
if overwrite_mode == 'always':
# 总是覆盖同名文件
overflag = True
elif overwrite_mode == 'size':
# 存在时大覆盖小
if target_item.size < fileitem.size:
logger.info(f"目标文件文件大小更小,将覆盖:{new_file}")
overflag = True
case 'size':
# 存在时大覆盖小
if target_item.size < fileitem.size:
logger.info(f"目标文件文件大小更小,将覆盖:{new_file}")
overflag = True
else:
return TransferInfo(success=False,
message=f"媒体库存在同名文件,且质量更好",
fileitem=fileitem,
target_item=target_item,
target_diritem=target_diritem,
fail_list=[fileitem.path],
transfer_type=transfer_type,
need_notify=need_notify)
case 'never':
# 存在不覆盖
else:
return TransferInfo(success=False,
message=f"媒体库存在同名文件,当前覆盖模式为不覆盖",
message=f"媒体库存在同名文件,且质量更好",
fileitem=fileitem,
target_item=target_item,
target_diritem=target_diritem,
fail_list=[fileitem.path],
transfer_type=transfer_type,
need_notify=need_notify)
case 'latest':
# 仅保留最新版本
logger.info(f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}")
overflag = True
elif overwrite_mode == 'never':
# 存在不覆盖
return TransferInfo(success=False,
message=f"媒体库存在同名文件,当前覆盖模式为不覆盖",
fileitem=fileitem,
target_item=target_item,
target_diritem=target_diritem,
fail_list=[fileitem.path],
transfer_type=transfer_type,
need_notify=need_notify)
elif overwrite_mode == 'latest':
# 仅保留最新版本
logger.info(f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}")
overflag = True
else:
if overwrite_mode == 'latest':
# 文件不存在,但仅保留最新版本
logger.info(f"当前整理覆盖模式设置为 {overwrite_mode},仅保留最新版本,正在删除已有版本文件 ...")
self.__delete_version_files(target_storage, new_file)
self.__delete_version_files(target_oper, new_file)
# 整理文件
new_item, err_msg = self.__transfer_file(fileitem=fileitem,
mediainfo=mediainfo,
target_storage=target_storage,
target_file=new_file,
transfer_type=transfer_type,
over_flag=overflag)
over_flag=overflag,
source_oper=source_oper,
target_oper=target_oper)
if not new_item:
logger.error(f"文件 {fileitem.path} 整理失败:{err_msg}")
return TransferInfo(success=False,
@@ -1134,7 +1209,7 @@ class FileManagerModule(_ModuleBase):
need_notify=need_notify)
@staticmethod
def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: str = None,
def __get_naming_dict(meta: MetaBase, mediainfo: MediaInfo, file_ext: Optional[str] = None,
episodes_info: List[TmdbEpisode] = None) -> dict:
"""
根据媒体信息返回Format字典
@@ -1143,97 +1218,8 @@ class FileManagerModule(_ModuleBase):
:param file_ext: 文件扩展名
:param episodes_info: 当前季的全部集信息
"""
def __convert_invalid_characters(filename: str):
if not filename:
return filename
invalid_characters = r'\/:*?"<>|'
# 创建半角到全角字符的转换表
halfwidth_chars = "".join([chr(i) for i in range(33, 127)])
fullwidth_chars = "".join([chr(i + 0xFEE0) for i in range(33, 127)])
translation_table = str.maketrans(halfwidth_chars, fullwidth_chars)
# 将不支持的字符替换为对应的全角字符
for char in invalid_characters:
filename = filename.replace(char, char.translate(translation_table))
return filename
# 获取集标题
episode_title = None
if meta.begin_episode and episodes_info:
for episode in episodes_info:
if episode.episode_number == meta.begin_episode:
episode_title = episode.name
break
# 获取集播出日期
episode_date = None
if meta.begin_episode and episodes_info:
for episode in episodes_info:
if episode.episode_number == meta.begin_episode:
episode_date = episode.air_date
break
return {
# 标题
"title": __convert_invalid_characters(mediainfo.title),
# 英文标题
"en_title": __convert_invalid_characters(mediainfo.en_title),
# 原语种标题
"original_title": __convert_invalid_characters(mediainfo.original_title),
# 原文件名
"original_name": meta.title,
# 识别名称(优先使用中文)
"name": meta.name,
# 识别的英文名称(可能为空)
"en_name": meta.en_name,
# 年份
"year": mediainfo.year or meta.year,
# 季年份根据season值获取
"season_year": mediainfo.season_years.get(
int(meta.season_seq),
None) if (mediainfo.season_years and meta.season_seq) else None,
# 资源类型
"resourceType": meta.resource_type,
# 特效
"effect": meta.resource_effect,
# 版本
"edition": meta.edition,
# 分辨率
"videoFormat": meta.resource_pix,
# 制作组/字幕组
"releaseGroup": meta.resource_team,
# 视频编码
"videoCodec": meta.video_encode,
# 音频编码
"audioCodec": meta.audio_encode,
# TMDBID
"tmdbid": mediainfo.tmdb_id,
# IMDBID
"imdbid": mediainfo.imdb_id,
# 豆瓣ID
"doubanid": mediainfo.douban_id,
# 季号
"season": meta.season_seq,
# 集号
"episode": meta.episode_seqs,
# 季集 SxxExx
"season_episode": "%s%s" % (meta.season, meta.episode),
# 段/节
"part": meta.part,
# 剧集标题
"episode_title": __convert_invalid_characters(episode_title),
# 剧集日期根据episodes_info值获取
"episode_date": episode_date,
# 文件后缀
"fileExt": file_ext,
# 自定义占位符
"customization": meta.customization,
# 文件元数据
"__meta__": meta,
# 识别的媒体信息
"__mediainfo__": mediainfo,
# 当前季的全部集信息
"__episodes_info__": episodes_info,
}
return TemplateHelper().builder.build(meta=meta, mediainfo=mediainfo,
file_extension=file_ext, episodes_info=episodes_info)
@staticmethod
def get_rename_path(template_string: str, rename_dict: dict, path: Path = None) -> Path:
@@ -1352,14 +1338,14 @@ class FileManagerModule(_ModuleBase):
logger.info(f"{mediainfo.title_year} 在本地文件系统中找到了这些季集:{seasons}")
return ExistMediaInfo(type=MediaType.TV, seasons=seasons)
def __delete_version_files(self, target_storage: str, path: Path) -> bool:
@staticmethod
def __delete_version_files(storage_oper: StorageBase, path: Path) -> bool:
"""
删除目录下的所有版本文件
:param target_storage: 存储类型
:param storage_oper: 存储操作对象
:param path: 目录路径
"""
# 存储
storage_oper = self.__get_storage_oper(target_storage)
if not storage_oper:
return False
# 识别文件中的季集信息

View File

@@ -69,7 +69,7 @@ class StorageBase(metaclass=ABCMeta):
pass
@abstractmethod
def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
"""
浏览文件
"""
@@ -128,7 +128,7 @@ class StorageBase(metaclass=ABCMeta):
pass
@abstractmethod
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
"""
上传文件
:param fileitem: 上传目录项

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict
@@ -102,20 +101,20 @@ class Alist(StorageBase, metaclass=Singleton):
"""
if resp is None:
logger.warning("请求登录失败无法连接alist服务")
logger.warning("【alist】请求登录失败无法连接alist服务")
return ""
if resp.status_code != 200:
logger.warning(f"更新令牌请求发送失败,状态码:{resp.status_code}")
logger.warning(f"【alist】更新令牌请求发送失败,状态码:{resp.status_code}")
return ""
result = resp.json()
if result["code"] != 200:
logger.critical(f'更新令牌,错误信息:{result["message"]}')
logger.critical(f'【alist】更新令牌,错误信息:{result["message"]}')
return ""
logger.debug("AList获取令牌成功")
logger.debug("【alist】AList获取令牌成功")
return result["data"]["token"]
def __get_header_with_token(self) -> dict:
@@ -133,11 +132,11 @@ class Alist(StorageBase, metaclass=Singleton):
def list(
self,
fileitem: schemas.FileItem,
password: str = "",
password: Optional[str] = "",
page: int = 1,
per_page: int = 0,
refresh: bool = False,
) -> Optional[List[schemas.FileItem]]:
) -> List[schemas.FileItem]:
"""
浏览文件
:param fileitem: 文件项
@@ -150,7 +149,7 @@ class Alist(StorageBase, metaclass=Singleton):
item = self.get_item(Path(fileitem.path))
if item:
return [item]
return None
return []
resp: Response = RequestUtils(
headers=self.__get_header_with_token()
).post_res(
@@ -200,21 +199,21 @@ class Alist(StorageBase, metaclass=Singleton):
"""
if resp is None:
logging.warning(f"请求获取目录 {fileitem.path} 的文件列表失败无法连接alist服务")
return None
logger.warn(f"【alist】请求获取目录 {fileitem.path} 的文件列表失败无法连接alist服务")
return []
if resp.status_code != 200:
logging.warning(
f"请求获取目录 {fileitem.path} 的文件列表失败,状态码:{resp.status_code}"
logger.warn(
f"【alist】请求获取目录 {fileitem.path} 的文件列表失败,状态码:{resp.status_code}"
)
return None
return []
result = resp.json()
if result["code"] != 200:
logging.warning(
f'获取目录 {fileitem.path} 的文件列表失败,错误信息:{result["message"]}'
logger.warn(
f'【alist】获取目录 {fileitem.path} 的文件列表失败,错误信息:{result["message"]}'
)
return None
return []
return [
schemas.FileItem(
@@ -258,15 +257,15 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if resp is None:
logging.warning(f"请求创建目录 {path} 失败无法连接alist服务")
logger.warn(f"【alist】请求创建目录 {path} 失败无法连接alist服务")
return None
if resp.status_code != 200:
logging.warning(f"请求创建目录 {path} 失败,状态码:{resp.status_code}")
logger.warn(f"【alist】请求创建目录 {path} 失败,状态码:{resp.status_code}")
return None
result = resp.json()
if result["code"] != 200:
logging.warning(f'创建目录 {path} 失败,错误信息:{result["message"]}')
logger.warn(f'【alist】创建目录 {path} 失败,错误信息:{result["message"]}')
return None
return self.get_item(path)
@@ -291,7 +290,7 @@ class Alist(StorageBase, metaclass=Singleton):
def get_item(
self,
path: Path,
password: str = "",
password: Optional[str] = "",
page: int = 1,
per_page: int = 0,
refresh: bool = False,
@@ -348,15 +347,15 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if resp is None:
logging.warning(f"请求获取文件 {path} 失败无法连接alist服务")
logger.warn(f"【alist】请求获取文件 {path} 失败无法连接alist服务")
return None
if resp.status_code != 200:
logging.warning(f"请求获取文件 {path} 失败,状态码:{resp.status_code}")
logger.warn(f"【alist】请求获取文件 {path} 失败,状态码:{resp.status_code}")
return None
result = resp.json()
if result["code"] != 200:
logging.debug(f'获取文件 {path} 失败,错误信息:{result["message"]}')
logger.debug(f'【alist】获取文件 {path} 失败,错误信息:{result["message"]}')
return None
return schemas.FileItem(
@@ -405,18 +404,18 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if resp is None:
logging.warning(f"请求删除文件 {fileitem.path} 失败无法连接alist服务")
logger.warn(f"【alist】请求删除文件 {fileitem.path} 失败无法连接alist服务")
return False
if resp.status_code != 200:
logging.warning(
f"请求删除文件 {fileitem.path} 失败,状态码:{resp.status_code}"
logger.warn(
f"【alist】请求删除文件 {fileitem.path} 失败,状态码:{resp.status_code}"
)
return False
result = resp.json()
if result["code"] != 200:
logging.warning(
f'删除文件 {fileitem.path} 失败,错误信息:{result["message"]}'
logger.warn(
f'【alist】删除文件 {fileitem.path} 失败,错误信息:{result["message"]}'
)
return False
return True
@@ -447,18 +446,18 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if not resp:
logging.warning(f"请求重命名文件 {fileitem.path} 失败无法连接alist服务")
logger.warn(f"【alist】请求重命名文件 {fileitem.path} 失败无法连接alist服务")
return False
if resp.status_code != 200:
logging.warning(
f"请求重命名文件 {fileitem.path} 失败,状态码:{resp.status_code}"
logger.warn(
f"【alist】请求重命名文件 {fileitem.path} 失败,状态码:{resp.status_code}"
)
return False
result = resp.json()
if result["code"] != 200:
logging.warning(
f'重命名文件 {fileitem.path} 失败,错误信息:{result["message"]}'
logger.warn(
f'【alist】重命名文件 {fileitem.path} 失败,错误信息:{result["message"]}'
)
return False
@@ -468,7 +467,7 @@ class Alist(StorageBase, metaclass=Singleton):
self,
fileitem: schemas.FileItem,
path: Path = None,
password: str = "",
password: Optional[str] = "",
) -> Optional[Path]:
"""
下载文件,保存到本地,返回本地临时文件地址
@@ -512,15 +511,15 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if not resp:
logging.warning(f"请求获取文件 {path} 失败无法连接alist服务")
logger.warn(f"【alist】请求获取文件 {path} 失败无法连接alist服务")
return None
if resp.status_code != 200:
logging.warning(f"请求获取文件 {path} 失败,状态码:{resp.status_code}")
logger.warn(f"【alist】请求获取文件 {path} 失败,状态码:{resp.status_code}")
return None
result = resp.json()
if result["code"] != 200:
logging.warning(f'获取文件 {path} 失败,错误信息:{result["message"]}')
logger.warn(f'【alist】获取文件 {path} 失败,错误信息:{result["message"]}')
return None
if result["data"]["raw_url"]:
@@ -547,7 +546,7 @@ class Alist(StorageBase, metaclass=Singleton):
return None
def upload(
self, fileitem: schemas.FileItem, path: Path, new_name: str = None, task: bool = False
self, fileitem: schemas.FileItem, path: Path, new_name: Optional[str] = None, task: bool = False
) -> Optional[schemas.FileItem]:
"""
上传文件
@@ -568,7 +567,7 @@ class Alist(StorageBase, metaclass=Singleton):
)
if resp.status_code != 200:
logging.warning(f"请求上传文件 {path} 失败,状态码:{resp.status_code}")
logger.warn(f"【alist】请求上传文件 {path} 失败,状态码:{resp.status_code}")
return None
new_item = self.get_item(Path(fileitem.path) / path.name)
@@ -617,20 +616,20 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if resp is None:
logging.warning(
f"请求复制文件 {fileitem.path} 失败无法连接alist服务"
logger.warn(
f"【alist】请求复制文件 {fileitem.path} 失败无法连接alist服务"
)
return False
if resp.status_code != 200:
logging.warning(
f"请求复制文件 {fileitem.path} 失败,状态码:{resp.status_code}"
logger.warn(
f"【alist】请求复制文件 {fileitem.path} 失败,状态码:{resp.status_code}"
)
return False
result = resp.json()
if result["code"] != 200:
logging.warning(
f'复制文件 {fileitem.path} 失败,错误信息:{result["message"]}'
logger.warn(
f'【alist】复制文件 {fileitem.path} 失败,错误信息:{result["message"]}'
)
return False
# 重命名
@@ -676,20 +675,20 @@ class Alist(StorageBase, metaclass=Singleton):
}
"""
if resp is None:
logging.warning(
f"请求移动文件 {fileitem.path} 失败无法连接alist服务"
logger.warn(
f"【alist】请求移动文件 {fileitem.path} 失败无法连接alist服务"
)
return False
if resp.status_code != 200:
logging.warning(
f"请求移动文件 {fileitem.path} 失败,状态码:{resp.status_code}"
logger.warn(
f"【alist】请求移动文件 {fileitem.path} 失败,状态码:{resp.status_code}"
)
return False
result = resp.json()
if result["code"] != 200:
logging.warning(
f'移动文件 {fileitem.path} 失败,错误信息:{result["message"]}'
logger.warn(
f'【alist】移动文件 {fileitem.path} 失败,错误信息:{result["message"]}'
)
return False
return True

View File

@@ -65,7 +65,7 @@ class LocalStorage(StorageBase):
modify_time=path.stat().st_mtime,
)
def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
"""
浏览文件
"""
@@ -95,7 +95,7 @@ class LocalStorage(StorageBase):
# 遍历目录
path_obj = Path(path)
if not path_obj.exists():
logger.warn(f"目录不存在:{path}")
logger.warn(f"【local】目录不存在:{path}")
return []
# 如果是文件
@@ -167,7 +167,7 @@ class LocalStorage(StorageBase):
else:
shutil.rmtree(path_obj, ignore_errors=True)
except Exception as e:
logger.error(f"删除文件失败:{e}")
logger.error(f"【local】删除文件失败:{e}")
return False
return True
@@ -181,7 +181,7 @@ class LocalStorage(StorageBase):
try:
path_obj.rename(path_obj.parent / name)
except Exception as e:
logger.error(f"重命名文件失败:{e}")
logger.error(f"【local】重命名文件失败:{e}")
return False
return True
@@ -191,7 +191,7 @@ class LocalStorage(StorageBase):
"""
return Path(fileitem.path)
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
"""
上传文件
:param fileitem: 上传目录项
@@ -202,7 +202,7 @@ class LocalStorage(StorageBase):
target_path = dir_path / (new_name or path.name)
code, message = SystemUtils.move(path, target_path)
if code != 0:
logger.error(f"移动文件失败:{message}")
logger.error(f"【local】移动文件失败:{message}")
return None
return self.get_item(target_path)
@@ -213,7 +213,7 @@ class LocalStorage(StorageBase):
file_path = Path(fileitem.path)
code, message = SystemUtils.link(file_path, target_file)
if code != 0:
logger.error(f"硬链接文件失败:{message}")
logger.error(f"【local】硬链接文件失败:{message}")
return False
return True
@@ -224,7 +224,7 @@ class LocalStorage(StorageBase):
file_path = Path(fileitem.path)
code, message = SystemUtils.softlink(file_path, target_file)
if code != 0:
logger.error(f"软链接文件失败:{message}")
logger.error(f"【local】软链接文件失败:{message}")
return False
return True
@@ -238,7 +238,7 @@ class LocalStorage(StorageBase):
file_path = Path(fileitem.path)
code, message = SystemUtils.copy(file_path, path / new_name)
if code != 0:
logger.error(f"复制文件失败:{message}")
logger.error(f"【local】复制文件失败:{message}")
return False
return True
@@ -252,7 +252,7 @@ class LocalStorage(StorageBase):
file_path = Path(fileitem.path)
code, message = SystemUtils.move(file_path, path / new_name)
if code != 0:
logger.error(f"移动文件失败:{message}")
logger.error(f"【local】移动文件失败:{message}")
return False
return True

View File

@@ -39,8 +39,8 @@ class Rclone(StorageBase):
super().set_config(conf)
filepath = conf.get("filepath")
if not filepath:
logger.warn("Rclone保存配置失败未设置配置文件路径")
logger.info(f"Rclone配置写入文件{filepath}")
logger.warn("【rclone保存配置失败:未设置配置文件路径")
logger.info(f"【rclone配置写入文件:{filepath}")
path = Path(filepath)
if not path.parent.exists():
path.parent.mkdir(parents=True)
@@ -56,7 +56,7 @@ class Rclone(StorageBase):
else:
return None
def __get_rcloneitem(self, item: dict, parent: str = "/") -> schemas.FileItem:
def __get_rcloneitem(self, item: dict, parent: Optional[str] = "/") -> schemas.FileItem:
"""
获取rclone文件项
"""
@@ -95,10 +95,10 @@ class Rclone(StorageBase):
if retcode == 0:
return True
except Exception as err:
logger.error(f"rclone存储检查失败{err}")
logger.error(f"rclone存储检查失败:{err}")
return False
def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
"""
浏览文件
"""
@@ -117,7 +117,7 @@ class Rclone(StorageBase):
items = json.loads(ret.stdout)
return [self.__get_rcloneitem(item, parent=fileitem.path) for item in items]
except Exception as err:
logger.error(f"rclone浏览文件失败{err}")
logger.error(f"rclone浏览文件失败:{err}")
return []
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
@@ -137,7 +137,7 @@ class Rclone(StorageBase):
if retcode == 0:
return self.get_item(Path(fileitem.path) / name)
except Exception as err:
logger.error(f"rclone创建目录失败{err}")
logger.error(f"rclone创建目录失败:{err}")
return None
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
@@ -161,17 +161,15 @@ class Rclone(StorageBase):
if folder:
return folder
# 逐级查找和创建目录
fileitem = schemas.FileItem(path="/")
for part in path.parts:
if part == "/":
continue
fileitem = schemas.FileItem(storage=self.schema.value, path="/")
for part in path.parts[1:]:
dir_file = __find_dir(fileitem, part)
if dir_file:
fileitem = dir_file
else:
dir_file = self.create_folder(fileitem, part)
if not dir_file:
logger.warn(f"rclone创建目录 {fileitem.path}{part} 失败!")
logger.warn(f"rclone创建目录 {fileitem.path}{part} 失败!")
return None
fileitem = dir_file
return fileitem
@@ -196,7 +194,7 @@ class Rclone(StorageBase):
return self.__get_rcloneitem(item, parent=str(path.parent) + "/")
return None
except Exception as err:
logger.debug(f"rclone获取文件项失败{err}")
logger.debug(f"rclone获取文件项失败:{err}")
return None
def delete(self, fileitem: schemas.FileItem) -> bool:
@@ -214,7 +212,7 @@ class Rclone(StorageBase):
if retcode == 0:
return True
except Exception as err:
logger.error(f"rclone删除文件失败{err}")
logger.error(f"rclone删除文件失败:{err}")
return False
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
@@ -233,7 +231,7 @@ class Rclone(StorageBase):
if retcode == 0:
return True
except Exception as err:
logger.error(f"rclone重命名文件失败{err}")
logger.error(f"rclone重命名文件失败:{err}")
return False
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
@@ -253,10 +251,11 @@ class Rclone(StorageBase):
if retcode == 0:
return path
except Exception as err:
logger.error(f"rclone复制文件失败{err}")
logger.error(f"rclone复制文件失败:{err}")
return None
def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]:
def upload(self, fileitem: schemas.FileItem, path: Path,
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
"""
上传文件
:param fileitem: 上传目录项
@@ -276,7 +275,7 @@ class Rclone(StorageBase):
if retcode == 0:
return self.get_item(new_path)
except Exception as err:
logger.error(f"rclone上传文件失败{err}")
logger.error(f"rclone上传文件失败:{err}")
return None
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
@@ -296,7 +295,7 @@ class Rclone(StorageBase):
items = json.loads(ret.stdout)
return self.__get_rcloneitem(items[0])
except Exception as err:
logger.error(f"rclone获取文件详情失败{err}")
logger.error(f"rclone获取文件详情失败:{err}")
return None
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
@@ -318,7 +317,7 @@ class Rclone(StorageBase):
if retcode == 0:
return True
except Exception as err:
logger.error(f"rclone移动文件失败{err}")
logger.error(f"rclone移动文件失败:{err}")
return False
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
@@ -340,7 +339,7 @@ class Rclone(StorageBase):
if retcode == 0:
return True
except Exception as err:
logger.error(f"rclone复制文件失败{err}")
logger.error(f"rclone复制文件失败:{err}")
return False
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
@@ -382,5 +381,5 @@ class Rclone(StorageBase):
available=items.get("free")
)
except Exception as err:
logger.error(f"rclone获取存储使用情况失败{err}")
logger.error(f"rclone获取存储使用情况失败:{err}")
return None

File diff suppressed because it is too large Load Diff

View File

@@ -77,8 +77,8 @@ class IndexerModule(_ModuleBase):
def search_torrents(self, site: dict,
keywords: List[str] = None,
mtype: MediaType = None,
cat: str = None,
page: int = 0) -> List[TorrentInfo]:
cat: Optional[str] = None,
page: Optional[int] = 0) -> List[TorrentInfo]:
"""
搜索一个站点
:param site: 站点
@@ -218,10 +218,10 @@ class IndexerModule(_ModuleBase):
@staticmethod
def __spider_search(indexer: dict,
search_word: str = None,
search_word: Optional[str] = None,
mtype: MediaType = None,
cat: str = None,
page: int = 0) -> Tuple[bool, List[dict]]:
cat: Optional[str] = None,
page: Optional[int] = 0) -> Tuple[bool, List[dict]]:
"""
根据关键字搜索单个站点
:param: indexer: 站点配置
@@ -241,7 +241,7 @@ class IndexerModule(_ModuleBase):
return _spider.is_error, _spider.get_torrents()
def refresh_torrents(self, site: dict,
keyword: str = None, cat: str = None, page: int = 0) -> Optional[List[TorrentInfo]]:
keyword: Optional[str] = None, cat: Optional[str] = None, page: Optional[int] = 0) -> Optional[List[TorrentInfo]]:
"""
获取站点最新一页的种子,多个站点需要多线程处理
:param site: 站点

View File

@@ -47,7 +47,7 @@ class SiteParserBase(metaclass=ABCMeta):
apikey: str,
token: str,
session: Session = None,
ua: str = None,
ua: Optional[str] = None,
emulate: bool = False,
proxy: bool = None):
super().__init__()

Some files were not shown because too many files have changed in this diff Show More