Compare commits

..

300 Commits
v ... v1.3.8

Author SHA1 Message Date
jxxghp
b23f78e94d v1.3.8 2023-10-31 11:50:18 +08:00
jxxghp
812a9a55d0 fix Plugin Alerts UI 2023-10-31 11:48:36 +08:00
jxxghp
2e289e80d1 fix 2023-10-31 10:37:36 +08:00
jxxghp
0d3dfdcbda feat 服务增加清理缓存 2023-10-31 10:32:56 +08:00
jxxghp
87eae72f51 fix #1024 2023-10-31 07:08:28 +08:00
jxxghp
17fa7101bd fix so 2023-10-30 13:35:19 +08:00
jxxghp
312bd53079 fix #1012 2023-10-29 16:08:34 +08:00
jxxghp
4bc7d47576 Merge pull request #1012 from Shurelol/main 2023-10-29 15:56:34 +08:00
Shurelol
71445b56f1 feat: 名称识别支持tmdbid等标记 2023-10-29 13:49:08 +08:00
jxxghp
9ce9e0a4ef fix #1006 2023-10-28 20:27:48 +08:00
jxxghp
ae196f1aeb Merge pull request #1006 from thsrite/main
fix bug
2023-10-28 20:27:25 +08:00
thsrite
38e09b894d fix bug 2023-10-28 20:24:16 +08:00
jxxghp
247d5ff255 Merge pull request #999 from honue/main
enhance 定期清理插件
2023-10-28 20:20:28 +08:00
jxxghp
0091e462fa Merge pull request #1004 from thsrite/main
fix 下载进度推送username
2023-10-28 20:19:12 +08:00
thsrite
7b314970b5 fix 下载进度推送username 2023-10-28 19:47:27 +08:00
jxxghp
7ac881e3e3 Merge pull request #1003 from thsrite/main 2023-10-28 19:31:29 +08:00
thsrite
8874723632 fix 认证失败后插件站点缺失bug 2023-10-28 19:14:08 +08:00
thsrite
262bda94c4 fix 目录监控入库消息延迟支持自定义 2023-10-28 18:52:46 +08:00
honue
d6e2cab5ef 兼容1.3.7版本清理插件配置 2023-10-28 18:40:27 +08:00
Summer⛱
6d3e33a05d Merge branch 'main' into main 2023-10-28 18:30:01 +08:00
jxxghp
f2d0bec0ac fix README 2023-10-28 17:46:57 +08:00
jxxghp
dea78f4bfd fix 2023-10-28 17:45:28 +08:00
jxxghp
f85f4b1342 Merge pull request #1000 from WithdewHua/qb
feat: qb 支持强制继续
2023-10-28 17:45:03 +08:00
WithdewHua
d03771f8ab feat: qb 支持强制继续 2023-10-28 17:41:31 +08:00
jxxghp
4b655dfac4 fix #957
fix #982
2023-10-28 17:41:22 +08:00
honue
cdfcdd80bf fix 2023-10-28 17:16:59 +08:00
honue
64d3942ba9 enhance 定期清理插件 2023-10-28 17:11:48 +08:00
jxxghp
16cce73f82 Merge pull request #996 from honue/main 2023-10-28 13:15:36 +08:00
honue
846edff84a fix 豆瓣榜单插件 2023-10-28 13:13:06 +08:00
jxxghp
d038bf31d3 Merge pull request #995 from honue/main 2023-10-28 12:41:50 +08:00
honue
376a69af5c fix 豆瓣榜单插件 2023-10-28 12:36:06 +08:00
jxxghp
380bb9bb3d Merge pull request #994 from thsrite/main 2023-10-28 12:26:27 +08:00
thsrite
f59e10ae1d fix qb按顺序下载支持变量配置 2023-10-28 12:24:24 +08:00
jxxghp
c8d2d80cc5 feat 支持配置多个认证站点 2023-10-28 11:50:50 +08:00
jxxghp
f0bb9ddfca Merge pull request #993 from thsrite/main
fix 目录监控消息电影不用等待直接发送入库消息
2023-10-28 11:46:23 +08:00
thsrite
9ab86e4a85 fix 目录监控消息电影不用等待直接发送入库消息 2023-10-28 11:41:44 +08:00
jxxghp
e33f1a3ffc Merge pull request #992 from thsrite/main
fix plugin api
2023-10-28 11:28:36 +08:00
jxxghp
e2213e1ef6 fix 远程搜索选择序号问题 2023-10-28 11:25:24 +08:00
thsrite
bbc4a1bfa5 fix plugin api 2023-10-28 11:22:37 +08:00
jxxghp
61e7ec9a36 Merge pull request #991 from honue/main 2023-10-28 11:06:25 +08:00
jxxghp
534ad0bad6 Merge pull request #987 from thsrite/main 2023-10-28 11:04:21 +08:00
thsrite
db3040a50e fix 2023-10-28 11:02:34 +08:00
honue
8dd74e7dd8 fix 完善页面download传参username 2023-10-28 11:02:26 +08:00
jxxghp
206cdb2663 Merge pull request #988 from khalid586/main 2023-10-28 10:58:24 +08:00
jxxghp
ca334813b7 更新 __init__.py 2023-10-28 10:55:02 +08:00
jxxghp
5fc93ee8e6 Merge pull request #986 from WithdewHua/fix-mediaserver 2023-10-28 10:52:53 +08:00
Khalid Abdullah
9cef7b2615 Update __init__.py(typos fixed) 2023-10-27 23:56:37 +06:00
thsrite
a3916207ae fix division by zero 2023-10-27 23:13:56 +08:00
thsrite
b6e1702051 fix add plugins api 2023-10-27 21:28:37 +08:00
WithdewHua
2cfc8b1ec7 fix: 重连判断 2023-10-27 20:31:52 +08:00
jxxghp
2f7570eec1 Merge pull request #983 from thsrite/main 2023-10-27 17:06:36 +08:00
thsrite
070481cab0 fix 正在下载显示剩余下载时间 2023-10-27 16:52:47 +08:00
jxxghp
26cd2c6cfe Merge pull request #982 from honue/main 2023-10-27 16:25:34 +08:00
honue
1ff571eb46 fix 定时清理媒体库,增加username字段 2023-10-27 15:12:10 +08:00
jxxghp
d8fcb4d240 Merge pull request #980 from thsrite/main 2023-10-27 10:42:45 +08:00
thsrite
778f97c1f3 fix log友好提示 2023-10-27 10:41:31 +08:00
jxxghp
1d6d9aa96d v1.3.7 2023-10-26 17:14:43 +08:00
jxxghp
3bdd96a8ee fix #951 不缓存网络错误导致的TMDB信息None 2023-10-26 17:07:01 +08:00
jxxghp
935ad73d32 fix #955 2023-10-26 16:45:09 +08:00
jxxghp
a85d55f3a8 fix 2023-10-26 16:17:29 +08:00
jxxghp
d7c659b736 fix 2023-10-26 16:15:31 +08:00
jxxghp
e5cedab873 Merge pull request #975 from Shurelol/main 2023-10-26 16:03:22 +08:00
jxxghp
3653d73f4f Merge pull request #974 from thsrite/main 2023-10-26 16:02:21 +08:00
Shurelol
4af57ed861 feat: 增加转移覆盖模式配置 2023-10-26 15:55:09 +08:00
Shurelol
10445c6f56 feat: 增加转移覆盖模式配置 2023-10-26 15:51:17 +08:00
Shurelol
dc6051f0b0 feat: 增加转移覆盖模式配置 2023-10-26 15:49:33 +08:00
Shurelol
2a524eaf22 feat: 增加转移覆盖模式配置 2023-10-26 15:28:41 +08:00
Shurelol
9a810f440d feat: 增加转移覆盖模式配置 2023-10-26 15:24:39 +08:00
thsrite
27ba8db4ea fix images 2023-10-26 15:07:39 +08:00
thsrite
7130194d5f fix 2023-10-26 14:55:08 +08:00
thsrite
d70afc36c9 fix 2023-10-26 14:48:46 +08:00
thsrite
78017b8a0e fix del image 2023-10-26 14:40:57 +08:00
thsrite
e87fdc896c fix tmdbinfo images 2023-10-26 14:34:58 +08:00
thsrite
7bb6d448ed feat 云盘文件删除插件 2023-10-26 13:38:48 +08:00
jxxghp
6415fd9286 Merge pull request #966 from thsrite/main 2023-10-26 13:13:16 +08:00
thsrite
2dd4395698 fix #935 2023-10-26 11:28:07 +08:00
thsrite
68b6e67a93 fix #970 2023-10-26 11:07:11 +08:00
thsrite
71b35e39ab fix 18262f98 2023-10-26 09:10:59 +08:00
thsrite
9ff6015fec fix Cloudflare IP优选插件描述… 2023-10-25 16:53:51 +08:00
thsrite
124817b733 fix 自定义hosts插件描述… 2023-10-25 16:52:19 +08:00
thsrite
8f8f3af7cd fix 药丸签到定时任务描述 2023-10-25 16:37:02 +08:00
thsrite
882fe6cd00 fix 关于路径映射描述…… 2023-10-25 16:33:09 +08:00
thsrite
18262f98f7 fix 同步删除通知图片 2023-10-25 10:52:33 +08:00
jxxghp
fe5a90ac2f Merge pull request #964 from thsrite/main 2023-10-25 10:20:39 +08:00
thsrite
22869b7932 fix #962 2023-10-25 10:10:17 +08:00
jxxghp
e702c16a74 Merge pull request #959 from thsrite/main 2023-10-24 11:32:13 +08:00
thsrite
408690c0ae fix 4aaf5997 2023-10-24 11:22:47 +08:00
thsrite
4aaf5997df fix 登录页海报支持自定义tmdb/bing 2023-10-24 11:17:32 +08:00
jxxghp
f50104bc86 Merge pull request #953 from thsrite/main 2023-10-23 12:01:02 +08:00
thsrite
ee10fc18a7 fix #952 2023-10-23 09:12:53 +08:00
jxxghp
818ef63aec fix #948 2023-10-22 08:23:04 +08:00
jxxghp
4af374f86d Merge remote-tracking branch 'origin/main' 2023-10-22 08:20:10 +08:00
jxxghp
277b252ad8 fix #949 2023-10-22 08:20:00 +08:00
jxxghp
cc7671efd0 Merge pull request #950 from LWLLR/bugfix/rss-proxy 2023-10-22 08:04:10 +08:00
LWLLR
419276eb85 fix: RSS订阅插件下载没有启用代理问题 2023-10-22 06:42:59 +08:00
jxxghp
7d97b9142a Merge pull request #945 from DDS-Derek/main 2023-10-21 15:08:08 +08:00
DDSDerek
c3c041f675 fix: docker buildx cache 2023-10-21 14:40:45 +08:00
jxxghp
d790e6b731 Merge pull request #944 from DDS-Derek/main 2023-10-21 14:33:24 +08:00
DDSRem
8b714a4710 feat: emphasis mark 2023-10-21 14:30:42 +08:00
jxxghp
1f0d01d2ed Merge remote-tracking branch 'origin/main' 2023-10-21 14:23:18 +08:00
jxxghp
7727cd4f58 v1.3.6 2023-10-21 14:22:25 +08:00
jxxghp
bb6b3a57af Merge pull request #943 from DDS-Derek/main
fix: running environment is required
2023-10-21 14:20:37 +08:00
DDSRem
a70a4c272c fix: running environment is required 2023-10-21 14:16:31 +08:00
jxxghp
99bd4da54b Merge pull request #942 from DDS-Derek/main
feat: issue add operating environment
2023-10-21 14:16:06 +08:00
jxxghp
3e09a5e57f fix plugin caption 2023-10-21 14:15:35 +08:00
DDSRem
1375179138 feat: issue add operating environment 2023-10-21 14:15:10 +08:00
jxxghp
a8b1fbbef0 fix #884 2023-10-21 14:04:37 +08:00
jxxghp
d490fcf5af fix init paths 2023-10-21 13:52:06 +08:00
jxxghp
cdbe5b2e2f feat 加大缓存使用 && 订阅搜索休眠 2023-10-21 08:17:10 +08:00
jxxghp
15b1c756a7 fix plugin name 2023-10-20 16:58:18 +08:00
jxxghp
3dfad93977 Merge pull request #932 from thsrite/main 2023-10-20 16:41:20 +08:00
thsrite
3fcd83b0a7 fix 2023-10-20 16:39:22 +08:00
thsrite
03e48881a6 fix 6eb0b4cb 2023-10-20 16:36:31 +08:00
thsrite
6eb0b4cb5b fix 签到bug 2023-10-20 16:29:17 +08:00
jxxghp
556d8586a7 Merge pull request #929 from thsrite/main 2023-10-20 14:33:27 +08:00
thsrite
0ce6e51925 fix 同步删除msg 2023-10-20 14:04:48 +08:00
jxxghp
c2dec7b955 fix download 2023-10-20 13:49:06 +08:00
thsrite
b3733ed9ed feat 站点自动登录插件 2023-10-20 12:54:16 +08:00
jxxghp
be5106c819 fix #927 2023-10-20 12:54:06 +08:00
jxxghp
4875db08e8 fix #927 2023-10-20 12:48:43 +08:00
thsrite
f0593996a1 fix cf优选支持交互命令执行 2023-10-20 12:37:35 +08:00
thsrite
6e113cc9c6 fix 签到失败过多自动触发cf优选 2023-10-20 12:08:07 +08:00
jxxghp
7ffc5e6624 Merge pull request #925 from thsrite/main 2023-10-20 09:58:16 +08:00
thsrite
d1689300b9 fix get_local_version 2023-10-20 09:20:08 +08:00
jxxghp
5fc7a7dd8a - 修复v1.3.5版本的问题 2023-10-20 08:07:38 +08:00
jxxghp
f59609131c fix #921 2023-10-20 07:38:34 +08:00
jxxghp
efd1733b56 Merge pull request #922 from WithdewHua/fix-db 2023-10-20 06:53:04 +08:00
WithdewHua
31289d24e2 fix: 将 detached 对象重新关联到 Session 2023-10-20 02:40:13 +08:00
jxxghp
98be091ca6 Merge pull request #918 from thsrite/main 2023-10-19 20:41:05 +08:00
thsrite
4bfdf1dede fix add 提示 2023-10-19 20:21:40 +08:00
thsrite
c6a43a5dde fix auto restart 2023-10-19 20:20:09 +08:00
thsrite
3a20946f62 fix remove update 2023-10-19 20:11:19 +08:00
thsrite
d892400ca7 Merge remote-tracking branch 'origin/main' into main 2023-10-19 19:23:20 +08:00
thsrite
44b7199087 fix 更新系统 2023-10-19 19:23:05 +08:00
jxxghp
c3fe22a76f Merge pull request #917 from thsrite/main
fix 更新通知
2023-10-19 18:11:06 +08:00
thsrite
7d9a3d39b3 fix 更新通知 2023-10-19 18:09:49 +08:00
jxxghp
c932d2b7f0 fix bug 2023-10-19 18:08:56 +08:00
jxxghp
4739d43c45 v1.3.5 2023-10-19 17:55:40 +08:00
jxxghp
b33e777028 fix bug 2023-10-19 17:39:15 +08:00
jxxghp
e5718a50b2 fix bug 2023-10-19 17:15:46 +08:00
jxxghp
a911bab7b0 fix db session 2023-10-19 16:58:38 +08:00
jxxghp
21908bdc6f fix 优化豆瓣插件媒体识别 2023-10-19 08:13:13 +08:00
jxxghp
573a943467 更新 site.py 2023-10-18 21:34:28 +08:00
jxxghp
bcc29afa2b fix #885 2023-10-18 21:14:17 +08:00
jxxghp
ce693435df fix #900 2023-10-18 20:50:37 +08:00
jxxghp
dad5d76664 fix bug 2023-10-18 20:46:58 +08:00
jxxghp
897369d300 fix dblock 2023-10-18 19:42:46 +08:00
jxxghp
3d34c26731 fix 2023-10-18 19:26:21 +08:00
jxxghp
2e4536edb6 fix db session 2023-10-18 18:30:09 +08:00
jxxghp
68e16d18fe fix #909 2023-10-18 12:36:46 +08:00
jxxghp
0cd071813f fix #908 2023-10-18 12:25:51 +08:00
jxxghp
49f7aa30c8 fix #904 2023-10-18 12:24:16 +08:00
jxxghp
74caf8a482 fix 2023-10-18 08:38:30 +08:00
jxxghp
fb78a07662 fix db session 2023-10-18 08:35:16 +08:00
jxxghp
84f5ce8a0b db lock 2023-10-17 21:07:38 +08:00
jxxghp
3f5f689965 db lock 2023-10-17 20:57:09 +08:00
jxxghp
0591b59837 more log 2023-10-17 20:36:28 +08:00
jxxghp
4cc2551487 fix 消息翻页后数据减少问题 2023-10-17 19:59:31 +08:00
jxxghp
f15ccadc2d fix 优化连接参数 2023-10-17 19:39:19 +08:00
jxxghp
453ef94e4d feat 多线程处理事件 2023-10-17 19:04:42 +08:00
jxxghp
e57b6adba1 Merge pull request #896 from thsrite/main 2023-10-17 18:52:17 +08:00
thsrite
acf8c67681 fix bug 2023-10-17 18:38:23 +08:00
jxxghp
be4df15d01 fix #890 2023-10-17 17:24:25 +08:00
jxxghp
bd2ef934d9 Merge pull request #894 from thsrite/main 2023-10-17 17:04:04 +08:00
thsrite
b90622a88e fix 2023-10-17 16:54:00 +08:00
thsrite
6868712b4e fix 入库失败输入手动处理通知 2023-10-17 16:50:46 +08:00
thsrite
4099c5e1b5 fix 2023-10-17 16:43:29 +08:00
thsrite
e018f77e37 fix #895 2023-10-17 16:27:29 +08:00
thsrite
3aac617f35 feat MoviePilot更新推送插件 2023-10-17 16:16:17 +08:00
thsrite
8a46ebc4a0 fix add update交互命令 2023-10-17 15:44:26 +08:00
thsrite
26f63c4ea7 fix 2023-10-17 15:12:02 +08:00
thsrite
0e92e9fc60 fix 交互重启完发送消息,交互获取当前版本 2023-10-17 15:10:02 +08:00
jxxghp
0fe911b6b4 fix #889 2023-10-17 13:34:49 +08:00
jxxghp
592b9a89c9 fix dict 2023-10-17 08:17:32 +08:00
jxxghp
08e0df1abc fix bug 2023-10-16 18:55:05 +08:00
jxxghp
8f012eee50 Merge pull request #881 from thsrite/main
fix 目录监控消息间隔
2023-10-16 18:23:55 +08:00
thsrite
da766a400d fix 目录监控消息间隔 2023-10-16 17:38:08 +08:00
jxxghp
ec309180da v1.3.4 2023-10-16 17:36:01 +08:00
jxxghp
ab3b674a6e fix 目录监控通知间隔 2023-10-16 17:30:25 +08:00
jxxghp
9231144518 fix 2023-10-16 17:11:45 +08:00
jxxghp
13c04de87c fix 2023-10-16 17:10:22 +08:00
jxxghp
70f533684f fix #607 目录监控全量同步 2023-10-16 17:04:51 +08:00
jxxghp
c94866631b 更新 bug_report.yml 2023-10-16 12:33:51 +08:00
jxxghp
40a77b438e feat 下载中信息增加用户 2023-10-16 08:28:54 +08:00
jxxghp
f5de48ca30 fix #867 2023-10-16 07:16:47 +08:00
jxxghp
89a2c00e64 fix #867 2023-10-16 07:16:02 +08:00
jxxghp
35afb50b26 fix #867 2023-10-16 07:14:36 +08:00
jxxghp
0e3e01bf9c fix #864 2023-10-16 07:06:10 +08:00
jxxghp
6e3ebd73c6 Merge remote-tracking branch 'origin/main' 2023-10-16 07:04:59 +08:00
jxxghp
add9b875aa fix #863 2023-10-16 07:04:31 +08:00
jxxghp
b1790ee730 v1.3.3 2023-10-15 15:00:15 +08:00
jxxghp
47d7800250 fix #788 Rclone远程刮削 2023-10-15 14:56:04 +08:00
jxxghp
4849c281d3 fix #788 2023-10-15 14:37:20 +08:00
jxxghp
c36acd7bb4 fix #830 2023-10-15 14:19:36 +08:00
jxxghp
986e96a88e Merge pull request #847 from thsrite/main 2023-10-14 20:55:00 +08:00
thsrite
493b7c2d24 fix 重启系统发送消息 2023-10-14 20:49:03 +08:00
jxxghp
0539ddab85 fix bug 2023-10-14 20:27:04 +08:00
jxxghp
202fdf8905 fix bug 2023-10-14 20:16:15 +08:00
jxxghp
9191ed0a21 fix bug 2023-10-14 20:12:02 +08:00
jxxghp
9697cf3901 fix icon 2023-10-14 13:31:21 +08:00
jxxghp
e6a11294fd Merge pull request #842 from lightolly/dev/20231014
feat:增加自定义站点配置,仅为统计和签到使用
2023-10-14 13:25:36 +08:00
olly
cd046d8023 feat:增加自定义站点配置,仅为统计和签到使用 2023-10-14 13:22:06 +08:00
jxxghp
4d08928b8c Merge remote-tracking branch 'origin/main' 2023-10-14 13:05:13 +08:00
jxxghp
bc8a243a6d feat 整合历史记录Api 2023-10-14 13:05:00 +08:00
jxxghp
3b804e13a8 Merge pull request #839 from thsrite/main
fix 缺失消息发给交互人
2023-10-14 12:42:26 +08:00
thsrite
f126f927b4 fix 缺失消息发给交互人 2023-10-14 12:35:38 +08:00
jxxghp
d4f202c2b1 fix #838 2023-10-14 11:52:05 +08:00
jxxghp
77a1d56c5b Merge pull request #838 from thsrite/main
fix 演职人员刮削插件增加刮削条件、debug日志
2023-10-14 11:48:17 +08:00
jxxghp
7415f94da2 Merge pull request #834 from DDS-Derek/main
feat: pip add cache
2023-10-14 11:47:26 +08:00
thsrite
fa50d8b884 fix 演职人员刮削插件增加刮削条件、debug日志 2023-10-14 11:40:21 +08:00
DDSDerek
40776c10bc feat: pip add cache 2023-10-14 01:08:13 +08:00
jxxghp
6578a2f977 Merge pull request #832 from DDS-Derek/main
docs fix
2023-10-13 21:52:06 +08:00
DDSRem
e780485fc6 docs: fix 2023-10-13 21:46:55 +08:00
DDSRem
8213cdba63 docs: fix 2023-10-13 21:44:28 +08:00
DDSRem
8d5b0d4035 docs: fix 2023-10-13 21:43:19 +08:00
DDSRem
3eaa22d068 docs: fix 2023-10-13 21:42:27 +08:00
jxxghp
4797983f43 Merge pull request #831 from DDS-Derek/main
fix: rclone version is too low
2023-10-13 21:32:11 +08:00
jxxghp
0e7e2fc44b fix #829 默认过滤规则拆分 2023-10-13 21:31:13 +08:00
DDSRem
9a51286c54 docs: fix 2023-10-13 21:20:42 +08:00
DDSRem
ddbf93f2c5 docs: fix 2023-10-13 21:16:59 +08:00
DDSRem
418411b10d fix: connector error 2023-10-13 21:14:43 +08:00
DDSRem
dceb7340dd fix: rclone version is too low 2023-10-13 21:07:13 +08:00
jxxghp
e7e9ca539d fix #810 2023-10-13 15:32:11 +08:00
jxxghp
333d187615 Merge pull request #821 from thsrite/main
fix 从下载历史获取tmdbid入库刮削
2023-10-13 15:10:33 +08:00
thsrite
761e66b200 fix 从下载历史获取tmdbid入库刮削 2023-10-13 14:29:07 +08:00
thsrite
eec52fa5ba fix 下载用户精简下载进度消息 2023-10-13 13:56:23 +08:00
jxxghp
b6c3c03748 Merge pull request #819 from thsrite/main 2023-10-13 11:52:33 +08:00
thsrite
4eebaa5d75 fix 删种 2023-10-13 11:45:18 +08:00
jxxghp
f6dfe9cb88 fix rules 2023-10-13 11:41:12 +08:00
thsrite
c36c94971e fix 插件记录同步 2023-10-13 11:09:33 +08:00
jxxghp
e83a15ad1f fix plugin log 2023-10-13 11:01:58 +08:00
thsrite
16aa353cf6 Merge remote-tracking branch 'origin/main' 2023-10-13 10:26:49 +08:00
thsrite
5adfa89d10 fix bug 2023-10-13 10:26:40 +08:00
jxxghp
b1805c1a46 add logs 2023-10-13 07:18:12 +08:00
jxxghp
7e51d70cd6 - 修复了搜索页面过滤失效的问题 2023-10-12 22:46:42 +08:00
jxxghp
b5cba64227 fix 2023-10-12 21:30:35 +08:00
jxxghp
f20c81efae fix rclone 2023-10-12 20:47:23 +08:00
jxxghp
bfbd93b912 fix 2023-10-12 20:35:44 +08:00
jxxghp
6be074e647 fix doubaninfo 2023-10-12 20:31:59 +08:00
jxxghp
5f96a562d4 v1.3.2 2023-10-12 20:11:22 +08:00
jxxghp
cefbd70469 fix #807 2023-10-12 20:06:58 +08:00
jxxghp
30c9c66087 fix 豆瓣来源订阅 2023-10-12 19:59:47 +08:00
jxxghp
1ecbc2f0be fix bug 2023-10-12 18:08:09 +08:00
jxxghp
884a0feb62 fix bug 2023-10-12 17:56:15 +08:00
jxxghp
5f44f07515 fixme 2023-10-12 17:51:20 +08:00
jxxghp
a902b79684 fix #800 2023-10-12 17:07:26 +08:00
jxxghp
4e13f59b36 fix #804 2023-10-12 16:03:36 +08:00
jxxghp
cbccac87f0 feat 清理无效的插件事件响应 2023-10-12 12:48:40 +08:00
jxxghp
eb3c09a3d3 fix bug 2023-10-12 11:50:22 +08:00
jxxghp
2a9a36ac88 feat 辅助识别异步接口 && ChatGPT插件支持辅助名称识别 2023-10-12 11:41:57 +08:00
jxxghp
af2f52a050 fix 优先级规则支持杜比全景声 2023-10-12 09:46:48 +08:00
jxxghp
7a61fa1ee2 feat 订阅支持更多过滤规则 2023-10-12 08:58:59 +08:00
jxxghp
ac3009d58f fix bug 2023-10-12 08:40:56 +08:00
jxxghp
e835feb056 更新 douban.py 2023-10-11 18:11:13 +08:00
jxxghp
cd391d14f9 fix plex 2023-10-11 17:27:10 +08:00
jxxghp
d7844968ab v1.3.1 2023-10-11 17:20:11 +08:00
jxxghp
70ea398f14 fix 优化豆瓣匹配 2023-10-11 16:32:34 +08:00
jxxghp
860d55a0e2 feat 热门动漫 2023-10-11 16:13:15 +08:00
jxxghp
0e35cec6e2 fix #743 支持Rclone 2023-10-11 12:16:41 +08:00
jxxghp
5778e86260 fix #775 增加日志打印 2023-10-11 11:01:53 +08:00
jxxghp
967d0b1205 fix #778 2023-10-11 08:32:48 +08:00
jxxghp
0b2d419000 fix spec 2023-10-11 08:19:44 +08:00
jxxghp
149104063c fix #784 PROXY_HOST仅环境变量配置 2023-10-11 07:44:37 +08:00
jxxghp
498168a2d3 fix #783 2023-10-10 22:23:03 +08:00
jxxghp
88e307416d fix Dockerfile 2023-10-10 22:09:16 +08:00
jxxghp
3bb2eedb33 fix icon 2023-10-10 21:26:30 +08:00
jxxghp
36c046ad6a - 优化Windows打包 2023-10-10 20:25:00 +08:00
jxxghp
85396df221 - 优化Windows打包 2023-10-10 20:06:52 +08:00
jxxghp
2f0f58783e fix spec 2023-10-10 19:45:18 +08:00
jxxghp
2d989d4229 更新 main.py 2023-10-10 18:19:46 +08:00
jxxghp
ecc8b6b385 fix spec 2023-10-10 17:35:28 +08:00
jxxghp
aa90c5d5c0 fix build 2023-10-10 16:38:51 +08:00
jxxghp
5f7d93f170 fix startup 2023-10-10 16:23:57 +08:00
jxxghp
0fbe51f257 fix bug 2023-10-10 16:17:22 +08:00
jxxghp
be941ebdd1 fix #770 2023-10-10 16:14:12 +08:00
jxxghp
4d900c2eb0 fix #777 豆瓣流控改为随机休眠3-10秒 2023-10-10 16:02:14 +08:00
jxxghp
93c473afe7 - 优化Windows打包 2023-10-10 15:48:44 +08:00
jxxghp
4c9a66f586 fix trayicon 2023-10-10 14:44:27 +08:00
jxxghp
375e16e0dc fix trayicon 2023-10-10 14:38:50 +08:00
jxxghp
91085d13a3 Merge remote-tracking branch 'origin/main' 2023-10-10 13:58:02 +08:00
jxxghp
3f83894dc6 add trayicon 2023-10-10 13:57:56 +08:00
jxxghp
5946684ee6 Merge pull request #776 from thsrite/main
fix 更新log
2023-10-10 13:44:39 +08:00
thsrite
7e3f25879f fix 更新log 2023-10-10 13:41:39 +08:00
jxxghp
48dcc3ee1b - 优化Windows打包 2023-10-10 13:32:05 +08:00
jxxghp
fca0a4b511 Merge remote-tracking branch 'origin/main' 2023-10-10 13:27:11 +08:00
jxxghp
d6831a8881 - 优化Windows打包 2023-10-10 13:27:00 +08:00
jxxghp
39a646ed92 更新 README.md 2023-10-10 12:50:11 +08:00
jxxghp
595965c5d0 Merge pull request #774 from thsrite/main 2023-10-10 11:47:00 +08:00
thsrite
3bb6f8a0c0 Merge remote-tracking branch 'origin/main' 2023-10-10 11:15:45 +08:00
thsrite
1924a2017e fix #773 2023-10-10 11:15:26 +08:00
jxxghp
60140fd2e6 - 优化Windows打包 2023-10-10 09:50:37 +08:00
jxxghp
65b5219e45 fix TZ 2023-10-10 07:58:26 +08:00
jxxghp
ae2f649aee fix README.md 2023-10-09 21:44:39 +08:00
jxxghp
bf3e860a18 fix README.md 2023-10-09 21:34:08 +08:00
jxxghp
0b44a91493 fix README.md 2023-10-09 21:33:53 +08:00
jxxghp
16077b3341 fix #769 2023-10-09 21:13:36 +08:00
jxxghp
a7cedde721 fix build 2023-10-09 20:53:23 +08:00
jxxghp
ecd53192dc fix build 2023-10-09 20:53:11 +08:00
jxxghp
a03c76e211 更新 build-windows.yml 2023-10-09 20:35:18 +08:00
jxxghp
de427fd7a9 fix 2023-10-09 20:14:54 +08:00
176 changed files with 5169 additions and 1890 deletions

View File

@@ -9,8 +9,9 @@ body:
请确认以下信息:
1. 请按此模板提交issues不按模板提交的问题将直接关闭。
2. 如果你的问题可以直接在以往 issue 或者 Telegram频道 中找到,那么你的 issue 将会被直接关闭。
3. 提交问题务必描述清楚、附上日志,描述不清导致无法理解和分析的问题会被直接关闭。
3. **$\color{red}{提交问题务必描述清楚、附上日志}$**,描述不清导致无法理解和分析的问题会被直接关闭。
4. 此仓库为后端仓库,如果是前端 WebUI 问题请在[前端仓库](https://github.com/jxxghp/MoviePilot-Frontend)提 issue。
5. **$\color{red}{不要通过issues来寻求解决你的环境问题、配置安装类问题、咨询类问题}$**,否则直接关闭并加入用户 $\color{red}{黑名单}$ !实在没有精力陪一波又一波的伸手党玩。
- type: checkboxes
id: ensure
attributes:
@@ -32,6 +33,16 @@ body:
description: 遇到问题时程序所在的版本号
validations:
required: true
- type: dropdown
id: environment
attributes:
label: 运行环境
description: 当前程序运行环境
options:
- Docker
- Windows
validations:
required: true
- type: dropdown
id: type
attributes:
@@ -40,7 +51,6 @@ body:
options:
- 主程序运行问题
- 插件问题
- Docker或运行环境问题
- 其他问题
validations:
required: true

View File

@@ -14,6 +14,16 @@ body:
description: 目前使用的程序版本
validations:
required: true
- type: dropdown
id: environment
attributes:
label: 运行环境
description: 当前程序运行环境
options:
- Docker
- Windows
validations:
required: true
- type: dropdown
id: type
attributes:
@@ -22,7 +32,6 @@ body:
options:
- 主程序
- 插件
- Docker
- 其他
validations:
required: true

View File

@@ -1,65 +0,0 @@
name: MoviePilot Windows Builder
on:
workflow_dispatch:
push:
branches:
- main
paths:
- version.py
jobs:
Windows-build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Release Version
id: release_version
run: |
$app_version = Select-String -Path "version.py" -Pattern "APP_VERSION\s=\s'v(.*)'" | ForEach-Object { $_.Matches.Groups[1].Value }
$env:GITHUB_ENV += "app_version=$app_version"
- name: Init Python 3.11.4
uses: actions/setup-python@v4
with:
python-version: '3.11.4'
- name: Install Dependent Packages
run: |
python -m pip install --upgrade pip
pip install wheel pyinstaller
pip install -r requirements.txt
shell: pwsh
- name: Pyinstaller
run: |
pyinstaller windows.spec
shell: pwsh
- name: Upload Windows File
uses: actions/upload-artifact@v3
with:
name: windows
path: dist/MoviePilot.exe
- name: Generate Release
id: generate_release
uses: actions/create-release@latest
with:
tag_name: v${{ env.app_version }}
release_name: v${{ env.app_version }}
body: ${{ github.event.commits[0].message }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release Asset
uses: dwenegar/upload-release-assets@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release_id: ${{ steps.generate_release.outputs.id }}
assets_path: |
dist/MoviePilot.exe

View File

@@ -1,4 +1,4 @@
name: MoviePilot Docker Builder
name: MoviePilot Builder
on:
workflow_dispatch:
push:
@@ -21,7 +21,7 @@ jobs:
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Docker meta
- name: Docker Meta
id: meta
uses: docker/metadata-action@v5
with:
@@ -55,5 +55,99 @@ jobs:
MOVIEPILOT_VERSION=${{ env.app_version }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}
cache-to: type=gha, scope=${{ github.workflow }}
cache-from: type=gha, scope=${{ github.workflow }}-docker
cache-to: type=gha, scope=${{ github.workflow }}-docker
Windows-build:
runs-on: windows-latest
name: Build Windows Binary
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Init Python 3.11.4
uses: actions/setup-python@v4
with:
python-version: '3.11.4'
cache: 'pip'
- name: Install Dependent Packages
run: |
python -m pip install --upgrade pip
pip install wheel pyinstaller
pip install -r requirements.txt
shell: pwsh
- name: Prepare Frontend
run: |
Invoke-WebRequest -Uri "http://nginx.org/download/nginx-1.25.2.zip" -OutFile "nginx.zip"
Expand-Archive -Path "nginx.zip" -DestinationPath "nginx-1.25.2"
Move-Item -Path "nginx-1.25.2/nginx-1.25.2" -Destination "nginx"
Remove-Item -Path "nginx.zip"
Remove-Item -Path "nginx-1.25.2" -Recurse -Force
$FRONTEND_VERSION = (Invoke-WebRequest -Uri "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest" | ConvertFrom-Json).tag_name
Invoke-WebRequest -Uri "https://github.com/jxxghp/MoviePilot-Frontend/releases/download/$FRONTEND_VERSION/dist.zip" -OutFile "dist.zip"
Expand-Archive -Path "dist.zip" -DestinationPath "dist"
Move-Item -Path "dist/dist/*" -Destination "nginx/html" -Force
Remove-Item -Path "dist.zip"
Remove-Item -Path "dist" -Recurse -Force
Move-Item -Path "nginx/html/nginx.conf" -Destination "nginx/conf/nginx.conf" -Force
New-Item -Path "nginx/temp" -ItemType Directory -Force
New-Item -Path "nginx/temp/__keep__.txt" -ItemType File -Force
New-Item -Path "nginx/logs" -ItemType Directory -Force
New-Item -Path "nginx/logs/__keep__.txt" -ItemType File -Force
shell: pwsh
- name: Pyinstaller
run: |
pyinstaller windows.spec
shell: pwsh
- name: Upload Windows File
uses: actions/upload-artifact@v3
with:
name: windows
path: dist/MoviePilot.exe
Create-release:
permissions: write-all
runs-on: ubuntu-latest
needs: [ Windows-build, Docker-build ]
steps:
- uses: actions/checkout@v2
- name: Release Version
id: release_version
run: |
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "app_version=$app_version" >> $GITHUB_ENV
- name: Download Artifact
uses: actions/download-artifact@v3
- name: get release_informations
shell: bash
run: |
mkdir releases
mv ./windows/MoviePilot.exe ./releases/MoviePilot_v${{ env.app_version }}.exe
- name: Create Release
id: create_release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ env.app_version }}
release_name: v${{ env.app_version }}
body: ${{ github.event.commits[0].message }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: dwenegar/upload-release-assets@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release_id: ${{ steps.create_release.outputs.id }}
assets_path: |
./releases/

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.idea/
*.c
build/
dist/
nginx/
test.py
app/helper/sites.py
config/user.db

View File

@@ -1,16 +1,20 @@
FROM python:3.11.4-slim-bullseye
ARG MOVIEPILOT_VERSION
ENV LANG="C.UTF-8" \
TZ="Asia/Shanghai" \
HOME="/moviepilot" \
CONFIG_DIR="/config" \
TERM="xterm" \
PUID=0 \
PGID=0 \
UMASK=000 \
PORT=3001 \
NGINX_PORT=3000 \
PROXY_HOST="" \
MOVIEPILOT_AUTO_UPDATE=true \
MOVIEPILOT_AUTO_UPDATE_DEV=false \
CONFIG_DIR="/config"
AUTH_SITE="iyuu" \
IYUU_SIGN=""
WORKDIR "/app"
RUN apt-get update -y \
&& apt-get -y install \
@@ -27,12 +31,14 @@ RUN apt-get update -y \
dumb-init \
jq \
haproxy \
fuse3 \
&& \
if [ "$(uname -m)" = "x86_64" ]; \
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \
elif [ "$(uname -m)" = "aarch64" ]; \
then ln -s /usr/lib/aarch64-linux-musl/libc.so /lib/libc.musl-aarch64.so.1; \
fi \
&& curl https://rclone.org/install.sh | bash \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf \

View File

@@ -4,8 +4,6 @@
# 仅用于学习交流使用,请勿在任何国内平台宣传该项目!
Dockerhttps://hub.docker.com/r/jxxghp/moviepilot
发布频道https://t.me/moviepilot_channel
## 主要特性
@@ -33,55 +31,63 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
### 4. **安装MoviePilot**
目前仅提供docker镜像,点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令:
- Docker镜像
```shell
docker pull jxxghp/moviepilot:latest
```
点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令:
```shell
docker pull jxxghp/moviepilot:latest
```
- Windows
下载 [MoviePilot.exe](https://github.com/jxxghp/MoviePilot/releases),双击运行后自动生成配置文件目录。
## 配置
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
-docker环境变量部分进行参数配置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
-Docker环境变量部分或Wdinows系统环境变量中进行参数配置,如未自动显示配置项则需要手动增加对应环境变量。
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 配置文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,部分参数如路径映射、站点认证、权限端口等必须通过环境变量进行配置。
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,**部分参数如路径映射、站点认证、权限端口、时区等必须通过环境变量进行配置**
> $\color{red}{*}$ 号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
> 号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
### 1. **基础设置**
- **NGINX_PORT $\color{red}{*}$ ** WEB服务端口默认`3000`可自行修改不能与API服务端口冲突仅支持环境变量配置
- **PORT $\color{red}{*}$ ** API服务端口默认`3001`可自行修改不能与WEB服务端口冲突仅支持环境变量配置
- **NGINX_PORT** WEB服务端口默认`3000`可自行修改不能与API服务端口冲突仅支持环境变量配置
- **PORT** API服务端口默认`3001`可自行修改不能与WEB服务端口冲突仅支持环境变量配置
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**(仅支持环境变量配置)
- **PROXY_HOST** 网络代理访问themoviedb或者重启更新需要使用代理访问格式为`http(s)://ip:port`、`socks5://user:pass@host:port`(仅支持环境变量配置)
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`**(仅支持环境变量配置)
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`(仅支持环境变量配置)
---
- **SUPERUSER $\color{red}{*}$ ** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
- **SUPERUSER_PASSWORD $\color{red}{*}$ ** 超级管理员初始密码,默认`password`,建议修改为复杂密码
- **API_TOKEN $\color{red}{*}$ ** API密钥默认`moviepilot`在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
- **PROXY_HOST** 网络代理访问themoviedb或者重启更新需要使用代理访问格式为`http(s)://ip:port``socks5://user:pass@host:port`(可选)
- **SUPERUSER** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
- **SUPERUSER_PASSWORD** 超级管理员初始密码,默认`password`,建议修改为复杂密码
- **API_TOKEN** API密钥默认`moviepilot`在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
- **TMDB_API_DOMAIN** TMDB API地址默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
- **TMDB_IMAGE_DOMAIN** TMDB图片地址默认`image.tmdb.org`可配置为其它中转代理以加速TMDB图片显示`static-mdb.v.geilijiasu.com`
- **WALLPAPER** 登录首页电影海报,`tmdb`/`bing`,默认`tmdb`
---
- **SCRAP_METADATA** 刮削入库的媒体文件,`true`/`false`,默认`true`
- **SCRAP_SOURCE** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
- **SCRAP_FOLLOW_TMDB** 新增已入库媒体是否跟随TMDB信息变化`true`/`false`,默认`true`
---
- **TRANSFER_TYPE $\color{red}{*}$ ** 整理转移方式,支持`link`/`copy`/`move`/`softlink` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响**
- **LIBRARY_PATH $\color{red}{*}$ ** 媒体库目录,多个目录使用`,`分隔
- **TRANSFER_TYPE** 整理转移方式,支持`link`/`copy`/`move`/`softlink`/`rclone_copy`/`rclone_move` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响rclone需要自行映射rclone配置目录到容器中或在容器内完成rclone配置节点名称必须为`MP`**
- **❗OVERWRITE_MODE** 转移覆盖模式,默认为`size`,支持`nerver`/`size`/`always`,分别表示`不覆盖`/`根据文件大小覆盖(大覆盖小)`/`总是覆盖`
- **❗LIBRARY_PATH** 媒体库目录,多个目录使用`,`分隔
- **LIBRARY_MOVIE_NAME** 电影媒体库目录名称(不是完整路径),默认`电影`
- **LIBRARY_TV_NAME** 电视剧媒体库目录称(不是完整路径),默认`电视剧`
- **LIBRARY_ANIME_NAME** 动漫媒体库目录称(不是完整路径),默认`电视剧/动漫`
- **LIBRARY_CATEGORY** 媒体库二级分类开关,`true`/`false`,默认`false`,开启后会根据配置 [category.yaml](https://github.com/jxxghp/MoviePilot/raw/main/config/category.yaml) 自动在媒体库目录下建立二级目录分类
---
- **COOKIECLOUD_HOST $\color{red}{*}$ ** CookieCloud服务器地址格式`http(s)://ip:port`,不配置默认使用内建服务器`https://movie-pilot.org/cookiecloud`
- **COOKIECLOUD_KEY $\color{red}{*}$ ** CookieCloud用户KEY
- **COOKIECLOUD_PASSWORD $\color{red}{*}$ ** CookieCloud端对端加密密码
- **COOKIECLOUD_INTERVAL $\color{red}{*}$ ** CookieCloud同步间隔分钟
- **USER_AGENT $\color{red}{*}$ ** CookieCloud保存Cookie对应的浏览器UA建议配置设置后可增加连接站点的成功率同步站点后可以在管理界面中修改
- **COOKIECLOUD_HOST** CookieCloud服务器地址格式`http(s)://ip:port`,不配置默认使用内建服务器`https://movie-pilot.org/cookiecloud`
- **COOKIECLOUD_KEY** CookieCloud用户KEY
- **COOKIECLOUD_PASSWORD** CookieCloud端对端加密密码
- **COOKIECLOUD_INTERVAL** CookieCloud同步间隔分钟
- **USER_AGENT** CookieCloud保存Cookie对应的浏览器UA建议配置设置后可增加连接站点的成功率同步站点后可以在管理界面中修改
- **OCR_HOST** OCR识别服务器地址格式`http(s)://ip:port`用于识别站点验证码实现自动登录获取Cookie等不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
---
- **SUBSCRIBE_MODE** 订阅模式,`rss`/`spider`,默认`spider``rss`模式通过定时刷新RSS来匹配订阅RSS地址会自动获取也可手动维护对站点压力小同时可设置订阅刷新周期24小时运行但订阅和下载通知不能过滤和显示免费推荐使用rss模式。
@@ -90,7 +96,7 @@ docker pull jxxghp/moviepilot:latest
- **SEARCH_SOURCE** 媒体信息搜索来源,`themoviedb`/`douban`,默认`themoviedb`
---
- **AUTO_DOWNLOAD_USER** 远程交互搜索时自动择优下载的用户ID多个用户使用,分割,未设置需要选择资源或者回复`0`
- **MESSAGER $\color{red}{*}$ ** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
- **MESSAGER** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
- `wechat`设置项:
@@ -121,7 +127,7 @@ docker pull jxxghp/moviepilot:latest
- **SYNOLOGYCHAT_TOKEN** SynologyChat机器人`令牌`
---
- **DOWNLOAD_PATH $\color{red}{*}$ ** 下载保存目录,**注意:需要将`moviepilot``下载器`的映射路径保持一致**,否则会导致下载文件无法转移
- **DOWNLOAD_PATH** 下载保存目录,**注意:需要将`moviepilot`及`下载器`的映射路径保持一致**,否则会导致下载文件无法转移
- **DOWNLOAD_MOVIE_PATH** 电影下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
- **DOWNLOAD_TV_PATH** 电视剧下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
- **DOWNLOAD_ANIME_PATH** 动漫下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
@@ -129,7 +135,7 @@ docker pull jxxghp/moviepilot:latest
- **DOWNLOAD_SUBTITLE** 下载站点字幕,`true`/`false`,默认`true`
- **DOWNLOADER_MONITOR** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
- **TORRENT_TAG** 下载器种子标签,默认为`MOVIEPILOT`设置后只有MoviePilot添加的下载才会处理留空所有下载器中的任务均会处理
- **DOWNLOADER $\color{red}{*}$ ** 下载器,支持`qbittorrent`/`transmission`QB版本号要求>= 4.3.9TR版本号要求>= 3.0,同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`qbittorrent`
- **DOWNLOADER** 下载器,支持`qbittorrent`/`transmission`QB版本号要求>= 4.3.9TR版本号要求>= 3.0,同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`qbittorrent`
- `qbittorrent`设置项:
@@ -137,6 +143,8 @@ docker pull jxxghp/moviepilot:latest
- **QB_USER** qbittorrent用户名
- **QB_PASSWORD** qbittorrent密码
- **QB_CATEGORY** qbittorrent分类自动管理`true`/`false`,默认`false`,开启后会将下载二级分类传递到下载器,由下载器管理下载目录,需要同步开启`DOWNLOAD_CATEGORY`
- **QB_SEQUENTIAL** qbittorrent按顺序下载`true`/`false`,默认`true`
- **QB_FORCE_RESUME** qbittorrent忽略队列限制强制继续`true`/`false`,默认 `false`
- `transmission`设置项:
@@ -146,7 +154,7 @@ docker pull jxxghp/moviepilot:latest
---
- **REFRESH_MEDIASERVER** 入库后是否刷新媒体服务器,`true`/`false`,默认`true`
- **MEDIASERVER $\color{red}{*}$ ** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
- **MEDIASERVER** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
- `emby`设置项:
@@ -169,9 +177,11 @@ docker pull jxxghp/moviepilot:latest
### 2. **用户认证**
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过docker环境变量配置**
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过环境变量配置**
- **AUTH_SITE $\color{red}{*}$ ** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
`AUTH_SITE`支持配置多个认证站点,使用`,`分隔,如:`iyuu,hhclub`,会依次执行认证操作,直到有一个站点认证成功。
- **❗AUTH_SITE** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
| 站点 | 参数 |
|:------------:|:-----------------------------------------------------:|

View File

@@ -1,4 +1,3 @@
from pathlib import Path
from typing import Any, List, Optional
from fastapi import APIRouter, Depends
@@ -17,12 +16,11 @@ router = APIRouter()
@router.get("/statistic", summary="媒体数量统计", response_model=schemas.Statistic)
def statistic(db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询媒体数量统计信息
"""
media_statistics: Optional[List[schemas.Statistic]] = DashboardChain(db).media_statistic()
media_statistics: Optional[List[schemas.Statistic]] = DashboardChain().media_statistic()
if media_statistics:
# 汇总各媒体库统计信息
ret_statistic = schemas.Statistic()
@@ -57,13 +55,12 @@ def processes(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/downloader", summary="下载器信息", response_model=schemas.DownloaderInfo)
def downloader(db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def downloader(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询下载器信息
"""
transfer_info = DashboardChain(db).downloader_info()
free_space = SystemUtils.free_space(Path(settings.DOWNLOAD_PATH))
transfer_info = DashboardChain().downloader_info()
free_space = SystemUtils.free_space(settings.SAVE_PATH)
if transfer_info:
return schemas.DownloaderInfo(
download_speed=transfer_info.download_speed,

View File

@@ -1,14 +1,12 @@
from typing import List, Any
from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app import schemas
from app.chain.douban import DoubanChain
from app.core.config import settings
from app.core.context import MediaInfo
from app.core.security import verify_token
from app.db import get_db
from app.schemas import MediaType
from app.utils.http import RequestUtils
@@ -32,13 +30,12 @@ def douban_img(imgurl: str) -> Any:
@router.get("/recognize/{doubanid}", summary="豆瓣ID识别", response_model=schemas.Context)
def recognize_doubanid(doubanid: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID识别媒体信息
"""
# 识别媒体信息
context = DoubanChain(db).recognize_by_doubanid(doubanid=doubanid)
context = DoubanChain().recognize_by_doubanid(doubanid=doubanid)
if context:
return context.to_dict()
else:
@@ -48,12 +45,11 @@ def recognize_doubanid(doubanid: str,
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
def movie_showing(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣正在热映
"""
movies = DoubanChain(db).movie_showing(page=page, count=count)
movies = DoubanChain().movie_showing(page=page, count=count)
if not movies:
return []
medias = [MediaInfo(douban_info=movie) for movie in movies]
@@ -65,13 +61,12 @@ def douban_movies(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣电影信息
"""
movies = DoubanChain(db).douban_discover(mtype=MediaType.MOVIE,
sort=sort, tags=tags, page=page, count=count)
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
sort=sort, tags=tags, page=page, count=count)
if not movies:
return []
medias = [MediaInfo(douban_info=movie) for movie in movies]
@@ -86,13 +81,12 @@ def douban_tvs(sort: str = "R",
tags: str = "",
page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
tvs = DoubanChain(db).douban_discover(mtype=MediaType.TV,
sort=sort, tags=tags, page=page, count=count)
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
sort=sort, tags=tags, page=page, count=count)
if not tvs:
return []
medias = [MediaInfo(douban_info=tv) for tv in tvs]
@@ -106,47 +100,54 @@ def douban_tvs(sort: str = "R",
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
def movie_top250(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览豆瓣剧集信息
"""
movies = DoubanChain(db).movie_top250(page=page, count=count)
movies = DoubanChain().movie_top250(page=page, count=count)
return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
def tv_weekly_chinese(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
中国每周剧集口碑榜
"""
tvs = DoubanChain(db).tv_weekly_chinese(page=page, count=count)
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
def tv_weekly_global(page: int = 1,
count: int = 30,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
全球每周剧集口碑榜
"""
tvs = DoubanChain(db).tv_weekly_global(page=page, count=count)
tvs = DoubanChain().tv_weekly_global(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
def tv_animation(page: int = 1,
count: int = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门动画剧集
"""
tvs = DoubanChain().tv_animation(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/{doubanid}", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
def douban_info(doubanid: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID查询豆瓣媒体信息
"""
doubaninfo = DoubanChain(db).douban_info(doubanid=doubanid)
doubaninfo = DoubanChain().douban_info(doubanid=doubanid)
if doubaninfo:
return MediaInfo(douban_info=doubaninfo).to_dict()
else:

View File

@@ -1,16 +1,16 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import schemas
from app.db.models.user import User
from app.db.userauth import get_current_active_user
from app.chain.douban import DoubanChain
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.core.context import MediaInfo, Context, TorrentInfo
from app.core.metainfo import MetaInfo
from app.core.security import verify_token
from app.db import get_db
from app.schemas import NotExistMediaInfo, MediaType
router = APIRouter()
@@ -18,19 +18,18 @@ router = APIRouter()
@router.get("/", summary="正在下载", response_model=List[schemas.DownloadingTorrent])
def read_downloading(
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询正在下载的任务
"""
return DownloadChain(db).downloading()
return DownloadChain().downloading()
@router.post("/", summary="添加下载", response_model=schemas.Response)
def add_downloading(
media_in: schemas.MediaInfo,
torrent_in: schemas.TorrentInfo,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
添加下载任务
@@ -49,7 +48,7 @@ def add_downloading(
media_info=mediainfo,
torrent_info=torrentinfo
)
did = DownloadChain(db).download_single(context=context)
did = DownloadChain().download_single(context=context, username=current_user.name)
return schemas.Response(success=True if did else False, data={
"download_id": did
})
@@ -57,7 +56,6 @@ def add_downloading(
@router.post("/notexists", summary="查询缺失媒体信息", response_model=List[NotExistMediaInfo])
def exists(media_in: schemas.MediaInfo,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询缺失媒体信息
@@ -68,19 +66,19 @@ def exists(media_in: schemas.MediaInfo,
if media_in.tmdb_id:
mediainfo.from_dict(media_in.dict())
elif media_in.douban_id:
context = DoubanChain(db).recognize_by_doubanid(doubanid=media_in.douban_id)
context = DoubanChain().recognize_by_doubanid(doubanid=media_in.douban_id)
if context:
mediainfo = context.media_info
meta = context.meta_info
else:
context = MediaChain(db).recognize_by_title(title=f"{media_in.title} {media_in.year}")
context = MediaChain().recognize_by_title(title=f"{media_in.title} {media_in.year}")
if context:
mediainfo = context.media_info
meta = context.meta_info
# 查询缺失信息
if not mediainfo or not mediainfo.tmdb_id:
raise HTTPException(status_code=404, detail="媒体信息不存在")
exist_flag, no_exists = DownloadChain(db).get_no_exists_info(meta=meta, mediainfo=mediainfo)
exist_flag, no_exists = DownloadChain().get_no_exists_info(meta=meta, mediainfo=mediainfo)
if mediainfo.type == MediaType.MOVIE:
# 电影已存在时返回空列表,存在时返回空对像列表
return [] if exist_flag else [NotExistMediaInfo()]
@@ -93,34 +91,31 @@ def exists(media_in: schemas.MediaInfo,
@router.get("/start/{hashString}", summary="开始任务", response_model=schemas.Response)
def start_downloading(
hashString: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
开如下载任务
"""
ret = DownloadChain(db).set_downloading(hashString, "start")
ret = DownloadChain().set_downloading(hashString, "start")
return schemas.Response(success=True if ret else False)
@router.get("/stop/{hashString}", summary="暂停任务", response_model=schemas.Response)
def stop_downloading(
hashString: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
控制下载任务
"""
ret = DownloadChain(db).set_downloading(hashString, "stop")
ret = DownloadChain().set_downloading(hashString, "stop")
return schemas.Response(success=True if ret else False)
@router.delete("/{hashString}", summary="删除下载任务", response_model=schemas.Response)
def remove_downloading(
hashString: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
控制下载任务
"""
ret = DownloadChain(db).remove_downloading(hashString)
ret = DownloadChain().remove_downloading(hashString)
return schemas.Response(success=True if ret else False)

View File

@@ -11,7 +11,6 @@ from app.core.security import verify_token
from app.db import get_db
from app.db.models.downloadhistory import DownloadHistory
from app.db.models.transferhistory import TransferHistory
from app.schemas import MediaType
from app.schemas.types import EventType
router = APIRouter()
@@ -76,10 +75,14 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
return schemas.Response(success=False, msg="记录不存在")
# 册除媒体库文件
if deletedest and history.dest:
TransferChain(db).delete_files(Path(history.dest))
state, msg = TransferChain().delete_files(Path(history.dest))
if not state:
return schemas.Response(success=False, msg=msg)
# 删除源文件
if deletesrc and history.src:
TransferChain(db).delete_files(Path(history.src))
state, msg = TransferChain().delete_files(Path(history.src))
if not state:
return schemas.Response(success=False, msg=msg)
# 发送事件
eventmanager.send_event(
EventType.DownloadFileDeleted,
@@ -90,23 +93,3 @@ def delete_transfer_history(history_in: schemas.TransferHistory,
# 删除记录
TransferHistory.delete(db, history_in.id)
return schemas.Response(success=True)
@router.post("/transfer", summary="历史记录重新转移", response_model=schemas.Response)
def redo_transfer_history(history_in: schemas.TransferHistory,
mtype: str = None,
new_tmdbid: int = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
历史记录重新转移,不输入 mtype 和 new_tmdbid 时,自动使用文件名重新识别
"""
if mtype and new_tmdbid:
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id,
mtype=MediaType(mtype), tmdbid=new_tmdbid)
else:
state, errmsg = TransferChain(db).re_transfer(logid=history_in.id)
if state:
return schemas.Response(success=True)
else:
return schemas.Response(success=False, message=errmsg)

View File

@@ -35,7 +35,7 @@ async def login_access_token(
if not user:
# 请求协助认证
logger.warn("登录用户本地不匹配,尝试辅助认证 ...")
token = UserChain(db).user_authenticate(form_data.username, form_data.password)
token = UserChain().user_authenticate(form_data.username, form_data.password)
if not token:
raise HTTPException(status_code=401, detail="用户名或密码不正确")
else:
@@ -49,10 +49,10 @@ async def login_access_token(
user.create(db)
elif not user.is_active:
raise HTTPException(status_code=403, detail="用户未启用")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return schemas.Token(
access_token=security.create_access_token(
user.id, expires_delta=access_token_expires
user.id,
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
),
token_type="bearer",
super_user=user.is_superuser,
@@ -61,6 +61,18 @@ async def login_access_token(
)
@router.get("/wallpaper", summary="登录页面电影海报", response_model=schemas.Response)
def wallpaper() -> Any:
"""
获取登录页面电影海报
"""
if settings.WALLPAPER == "tmdb":
return tmdb_wallpaper()
elif settings.WALLPAPER == "bing":
return bing_wallpaper()
return schemas.Response(success=False)
@router.get("/bing", summary="Bing每日壁纸", response_model=schemas.Response)
def bing_wallpaper() -> Any:
"""
@@ -68,17 +80,19 @@ def bing_wallpaper() -> Any:
"""
url = WebUtils.get_bing_wallpaper()
if url:
return schemas.Response(success=False,
message=url)
return schemas.Response(
success=True,
message=url
)
return schemas.Response(success=False)
@router.get("/tmdb", summary="TMDB电影海报", response_model=schemas.Response)
def tmdb_wallpaper(db: Session = Depends(get_db)) -> Any:
def tmdb_wallpaper() -> Any:
"""
获取TMDB电影海报
"""
wallpager = TmdbChain(db).get_random_wallpager()
wallpager = TmdbChain().get_random_wallpager()
if wallpager:
return schemas.Response(
success=True,

View File

@@ -20,13 +20,12 @@ router = APIRouter()
@router.get("/recognize", summary="识别媒体信息(种子)", response_model=schemas.Context)
def recognize(title: str,
subtitle: str = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据标题、副标题识别媒体信息
"""
# 识别媒体信息
context = MediaChain(db).recognize_by_title(title=title, subtitle=subtitle)
context = MediaChain().recognize_by_title(title=title, subtitle=subtitle)
if context:
return context.to_dict()
return schemas.Context()
@@ -34,13 +33,12 @@ def recognize(title: str,
@router.get("/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context)
def recognize(path: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据文件路径识别媒体信息
"""
# 识别媒体信息
context = MediaChain(db).recognize_by_path(path)
context = MediaChain().recognize_by_path(path)
if context:
return context.to_dict()
return schemas.Context()
@@ -50,12 +48,11 @@ def recognize(path: str,
def search_by_title(title: str,
page: int = 1,
count: int = 8,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
模糊搜索媒体信息列表
"""
_, medias = MediaChain(db).search(title=title)
_, medias = MediaChain().search(title=title)
if medias:
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
return []
@@ -85,21 +82,20 @@ def exists(title: str = None,
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
def tmdb_info(mediaid: str, type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧
"""
mtype = MediaType(type_name)
if mediaid.startswith("tmdb:"):
result = TmdbChain(db).tmdb_info(int(mediaid[5:]), mtype)
result = TmdbChain().tmdb_info(int(mediaid[5:]), mtype)
return MediaInfo(tmdb_info=result).to_dict()
elif mediaid.startswith("douban:"):
# 查询豆瓣信息
doubaninfo = DoubanChain(db).douban_info(doubanid=mediaid[7:])
doubaninfo = DoubanChain().douban_info(doubanid=mediaid[7:])
if not doubaninfo:
return schemas.MediaInfo()
result = DoubanChain(db).recognize_by_doubaninfo(doubaninfo)
result = DoubanChain().recognize_by_doubaninfo(doubaninfo)
if result:
# TMDB
return result.media_info.to_dict()

View File

@@ -2,14 +2,12 @@ from typing import Union, Any, List
from fastapi import APIRouter, BackgroundTasks, Depends
from fastapi import Request
from sqlalchemy.orm import Session
from starlette.responses import PlainTextResponse
from app import schemas
from app.chain.message import MessageChain
from app.core.config import settings
from app.core.security import verify_token
from app.db import get_db
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
@@ -19,23 +17,22 @@ from app.schemas.types import SystemConfigKey, NotificationType
router = APIRouter()
def start_message_chain(db: Session, body: Any, form: Any, args: Any):
def start_message_chain(body: Any, form: Any, args: Any):
"""
启动链式任务
"""
MessageChain(db).process(body=body, form=form, args=args)
MessageChain().process(body=body, form=form, args=args)
@router.post("/", summary="接收用户消息", response_model=schemas.Response)
async def user_message(background_tasks: BackgroundTasks, request: Request,
db: Session = Depends(get_db)):
async def user_message(background_tasks: BackgroundTasks, request: Request):
"""
用户消息响应
"""
body = await request.body()
form = await request.form()
args = request.query_params
background_tasks.add_task(start_message_chain, db, body, form, args)
background_tasks.add_task(start_message_chain, body, form, args)
return schemas.Response(success=True)
@@ -51,7 +48,7 @@ def wechat_verify(echostr: str, msg_signature: str,
sEncodingAESKey=settings.WECHAT_ENCODING_AESKEY,
sReceiveId=settings.WECHAT_CORPID)
except Exception as err:
logger.error(f"微信请求验证失败: {err}")
logger.error(f"微信请求验证失败: {str(err)}")
return str(err)
ret, sEchoStr = wxcpt.VerifyURL(sMsgSignature=msg_signature,
sTimeStamp=timestamp,

View File

@@ -1,25 +1,22 @@
from typing import List, Any
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas
from app.chain.douban import DoubanChain
from app.chain.search import SearchChain
from app.core.security import verify_token
from app.db import get_db
from app.schemas.types import MediaType
router = APIRouter()
@router.get("/last", summary="查询搜索结果", response_model=List[schemas.Context])
async def search_latest(db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
async def search_latest(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
查询搜索结果
"""
torrents = SearchChain(db).last_search_results()
torrents = SearchChain().last_search_results()
return [torrent.to_dict() for torrent in torrents]
@@ -27,7 +24,6 @@ async def search_latest(db: Session = Depends(get_db),
def search_by_tmdbid(mediaid: str,
mtype: str = None,
area: str = "title",
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID/豆瓣ID精确搜索站点资源 tmdb:/douban:/
@@ -36,16 +32,16 @@ def search_by_tmdbid(mediaid: str,
tmdbid = int(mediaid.replace("tmdb:", ""))
if mtype:
mtype = MediaType(mtype)
torrents = SearchChain(db).search_by_tmdbid(tmdbid=tmdbid, mtype=mtype, area=area)
torrents = SearchChain().search_by_tmdbid(tmdbid=tmdbid, mtype=mtype, area=area)
elif mediaid.startswith("douban:"):
doubanid = mediaid.replace("douban:", "")
# 识别豆瓣信息
context = DoubanChain(db).recognize_by_doubanid(doubanid)
context = DoubanChain().recognize_by_doubanid(doubanid)
if not context or not context.media_info or not context.media_info.tmdb_id:
return []
torrents = SearchChain(db).search_by_tmdbid(tmdbid=context.media_info.tmdb_id,
mtype=context.media_info.type,
area=area)
torrents = SearchChain().search_by_tmdbid(tmdbid=context.media_info.tmdb_id,
mtype=context.media_info.type,
area=area)
else:
return []
return [torrent.to_dict() for torrent in torrents]
@@ -55,10 +51,9 @@ def search_by_tmdbid(mediaid: str,
async def search_by_title(keyword: str = None,
page: int = 0,
site: int = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据名称模糊搜索站点资源,支持分页,关键词为空是返回首页资源
"""
torrents = SearchChain(db).search_by_title(title=keyword, page=page, site=site)
torrents = SearchChain().search_by_title(title=keyword, page=page, site=site)
return [torrent.to_dict() for torrent in torrents]

View File

@@ -45,7 +45,7 @@ def add_site(
domain = StringUtils.get_url_domain(site_in.url)
site_info = SitesHelper().get_indexer(domain)
if not site_info:
return schemas.Response(success=False, message="该站点不支持")
return schemas.Response(success=False, message="该站点不支持或用户未通过认证")
if Site.get_by_domain(db, domain):
return schemas.Response(success=False, message=f"{domain} 站点己存在")
# 保存站点信息
@@ -139,9 +139,9 @@ def update_cookie(
detail=f"站点 {site_id} 不存在!",
)
# 更新Cookie
state, message = SiteChain(db).update_cookie(site_info=site_info,
username=username,
password=password)
state, message = SiteChain().update_cookie(site_info=site_info,
username=username,
password=password)
return schemas.Response(success=state, message=message)
@@ -158,7 +158,7 @@ def test_site(site_id: int,
status_code=404,
detail=f"站点 {site_id} 不存在",
)
status, message = SiteChain(db).test(site.domain)
status, message = SiteChain().test(site.domain)
return schemas.Response(success=status, message=message)

View File

@@ -18,13 +18,13 @@ from app.schemas.types import MediaType
router = APIRouter()
def start_subscribe_add(db: Session, title: str, year: str,
def start_subscribe_add(title: str, year: str,
mtype: MediaType, tmdbid: int, season: int, username: str):
"""
启动订阅任务
"""
SubscribeChain(db).add(title=title, year=year,
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
SubscribeChain().add(title=title, year=year,
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
@@ -45,7 +45,6 @@ def read_subscribes(
def create_subscribe(
*,
subscribe_in: schemas.Subscribe,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
@@ -61,15 +60,15 @@ def create_subscribe(
title = subscribe_in.name
else:
title = None
sid, message = SubscribeChain(db).add(mtype=mtype,
title=title,
year=subscribe_in.year,
tmdbid=subscribe_in.tmdbid,
season=subscribe_in.season,
doubanid=subscribe_in.doubanid,
username=current_user.name,
best_version=subscribe_in.best_version,
exist_ok=True)
sid, message = SubscribeChain().add(mtype=mtype,
title=title,
year=subscribe_in.year,
tmdbid=subscribe_in.tmdbid,
season=subscribe_in.season,
doubanid=subscribe_in.doubanid,
username=current_user.name,
best_version=subscribe_in.best_version,
exist_ok=True)
return schemas.Response(success=True if sid else False, message=message, data={
"id": sid
})
@@ -195,8 +194,10 @@ def read_subscribe(
"""
根据订阅编号查询订阅信息
"""
if not subscribe_id:
return Subscribe()
subscribe = Subscribe.get(db, subscribe_id)
if subscribe.sites:
if subscribe and subscribe.sites:
subscribe.sites = json.loads(subscribe.sites)
return subscribe
@@ -240,7 +241,6 @@ def delete_subscribe(
@router.post("/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=schemas.Response)
async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
authorization: str = Header(None)) -> Any:
"""
Jellyseerr/Overseerr订阅
@@ -268,7 +268,6 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
# 添加订阅
if media_type == MediaType.MOVIE:
background_tasks.add_task(start_subscribe_add,
db=db,
mtype=media_type,
tmdbid=tmdbId,
title=subject,
@@ -283,7 +282,6 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
break
for season in seasons:
background_tasks.add_task(start_subscribe_add,
db=db,
mtype=media_type,
tmdbid=tmdbId,
title=subject,

View File

@@ -6,13 +6,11 @@ from typing import Union
import tailer
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app import schemas
from app.chain.search import SearchChain
from app.core.config import settings
from app.core.security import verify_token
from app.db import get_db
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.message import MessageHelper
from app.helper.progress import ProgressHelper
@@ -174,7 +172,6 @@ def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
def ruletest(title: str,
subtitle: str = None,
ruletype: str = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)):
"""
过滤规则测试,规则类型 1-订阅2-洗版3-搜索
@@ -193,8 +190,8 @@ def ruletest(title: str,
return schemas.Response(success=False, message="优先级规则未设置!")
# 过滤
result = SearchChain(db).filter_torrents(rule_string=rule_string,
torrent_list=[torrent])
result = SearchChain().filter_torrents(rule_string=rule_string,
torrent_list=[torrent])
if not result:
return schemas.Response(success=False, message="不符合优先级规则!")
return schemas.Response(success=True, data={
@@ -222,8 +219,5 @@ def execute_command(jobid: str,
"""
if not jobid:
return schemas.Response(success=False, message="命令不能为空!")
if jobid == "subscribe_search":
Scheduler().start(jobid, state = 'R')
else:
Scheduler().start(jobid)
return schemas.Response(success=True)
Scheduler().start(jobid)
return schemas.Response(success=True)

View File

@@ -1,25 +1,22 @@
from typing import List, Any
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas
from app.chain.tmdb import TmdbChain
from app.core.context import MediaInfo
from app.core.security import verify_token
from app.db import get_db
from app.schemas.types import MediaType
router = APIRouter()
@router.get("/seasons/{tmdbid}", summary="TMDB所有季", response_model=List[schemas.TmdbSeason])
def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def tmdb_seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询themoviedb所有季信息
"""
seasons_info = TmdbChain(db).tmdb_seasons(tmdbid=tmdbid)
seasons_info = TmdbChain().tmdb_seasons(tmdbid=tmdbid)
if not seasons_info:
return []
else:
@@ -29,16 +26,15 @@ def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
@router.get("/similar/{tmdbid}/{type_name}", summary="类似电影/电视剧", response_model=List[schemas.MediaInfo])
def tmdb_similar(tmdbid: int,
type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询类似电影/电视剧type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_similar(tmdbid=tmdbid)
tmdbinfos = TmdbChain().movie_similar(tmdbid=tmdbid)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_similar(tmdbid=tmdbid)
tmdbinfos = TmdbChain().tv_similar(tmdbid=tmdbid)
else:
return []
if not tmdbinfos:
@@ -50,16 +46,15 @@ def tmdb_similar(tmdbid: int,
@router.get("/recommend/{tmdbid}/{type_name}", summary="推荐电影/电视剧", response_model=List[schemas.MediaInfo])
def tmdb_recommend(tmdbid: int,
type_name: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询推荐电影/电视剧type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_recommend(tmdbid=tmdbid)
tmdbinfos = TmdbChain().movie_recommend(tmdbid=tmdbid)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_recommend(tmdbid=tmdbid)
tmdbinfos = TmdbChain().tv_recommend(tmdbid=tmdbid)
else:
return []
if not tmdbinfos:
@@ -72,16 +67,15 @@ def tmdb_recommend(tmdbid: int,
def tmdb_credits(tmdbid: int,
type_name: str,
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询演员阵容type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
tmdbinfos = TmdbChain(db).movie_credits(tmdbid=tmdbid, page=page)
tmdbinfos = TmdbChain().movie_credits(tmdbid=tmdbid, page=page)
elif mediatype == MediaType.TV:
tmdbinfos = TmdbChain(db).tv_credits(tmdbid=tmdbid, page=page)
tmdbinfos = TmdbChain().tv_credits(tmdbid=tmdbid, page=page)
else:
return []
if not tmdbinfos:
@@ -92,12 +86,11 @@ def tmdb_credits(tmdbid: int,
@router.get("/person/{person_id}", summary="人物详情", response_model=schemas.TmdbPerson)
def tmdb_person(person_id: int,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物详情
"""
tmdbinfo = TmdbChain(db).person_detail(person_id=person_id)
tmdbinfo = TmdbChain().person_detail(person_id=person_id)
if not tmdbinfo:
return schemas.TmdbPerson()
else:
@@ -107,12 +100,11 @@ 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,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据人物ID查询人物参演作品
"""
tmdbinfo = TmdbChain(db).person_credits(person_id=person_id, page=page)
tmdbinfo = TmdbChain().person_credits(person_id=person_id, page=page)
if not tmdbinfo:
return []
else:
@@ -124,16 +116,15 @@ def tmdb_movies(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB电影信息
"""
movies = TmdbChain(db).tmdb_discover(mtype=MediaType.MOVIE,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
movies = TmdbChain().tmdb_discover(mtype=MediaType.MOVIE,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
if not movies:
return []
return [MediaInfo(tmdb_info=movie).to_dict() for movie in movies]
@@ -144,16 +135,15 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
with_genres: str = "",
with_original_language: str = "",
page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""
tvs = TmdbChain(db).tmdb_discover(mtype=MediaType.TV,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
tvs = TmdbChain().tmdb_discover(mtype=MediaType.TV,
sort_by=sort_by,
with_genres=with_genres,
with_original_language=with_original_language,
page=page)
if not tvs:
return []
return [MediaInfo(tmdb_info=tv).to_dict() for tv in tvs]
@@ -161,12 +151,11 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
def tmdb_trending(page: int = 1,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
浏览TMDB剧集信息
"""
infos = TmdbChain(db).tmdb_trending(page=page)
infos = TmdbChain().tmdb_trending(page=page)
if not infos:
return []
return [MediaInfo(tmdb_info=info).to_dict() for info in infos]
@@ -174,12 +163,11 @@ def tmdb_trending(page: int = 1,
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
def tmdb_season_episodes(tmdbid: int, season: int,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询某季的所有信信息
"""
episodes_info = TmdbChain(db).tmdb_episodes(tmdbid=tmdbid, season=season)
episodes_info = TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season)
if not episodes_info:
return []
else:

View File

@@ -8,13 +8,15 @@ from app import schemas
from app.chain.transfer import TransferChain
from app.core.security import verify_token
from app.db import get_db
from app.db.models.transferhistory import TransferHistory
from app.schemas import MediaType
router = APIRouter()
@router.post("/manual", summary="手动转移", response_model=schemas.Response)
def manual_transfer(path: str,
def manual_transfer(path: str = None,
logid: int = None,
target: str = None,
tmdbid: int = None,
type_name: str = None,
@@ -28,8 +30,9 @@ def manual_transfer(path: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
手动转移,支持自定义剧集识别格式
手动转移,文件或历史记录,支持自定义剧集识别格式
:param path: 转移路径或文件
:param logid: 转移历史记录ID
:param target: 目标路径
:param type_name: 媒体类型、电影/电视剧
:param tmdbid: tmdbid
@@ -43,11 +46,32 @@ def manual_transfer(path: str,
:param db: 数据库
:param _: Token校验
"""
in_path = Path(path)
if target:
force = False
if logid:
# 查询历史记录
history = TransferHistory.get(db, logid)
if not history:
return schemas.Response(success=False, message=f"历史记录不存在ID{logid}")
# 强制转移
force = True
# 源路径
in_path = Path(history.src)
# 目的路径
if history.dest and str(history.dest) != "None":
# 删除旧的已整理文件
TransferChain().delete_files(Path(history.dest))
if not target:
target = history.dest
elif path:
in_path = Path(path)
else:
return schemas.Response(success=False, message=f"缺少参数path/logid")
if target and target != "None":
target = Path(target)
if not target.exists():
return schemas.Response(success=False, message=f"目标路径不存在")
else:
target = None
# 类型
mtype = MediaType(type_name) if type_name else None
# 自定义格式
@@ -60,7 +84,7 @@ def manual_transfer(path: str,
offset=episode_offset,
)
# 开始转移
state, errormsg = TransferChain(db).manual_transfer(
state, errormsg = TransferChain().manual_transfer(
in_path=in_path,
target=target,
tmdbid=tmdbid,
@@ -68,7 +92,8 @@ def manual_transfer(path: str,
season=season,
transfer_type=transfer_type,
epformat=epformat,
min_filesize=min_filesize
min_filesize=min_filesize,
force=force
)
# 失败
if not state:

View File

@@ -1,27 +1,25 @@
from typing import Any
from fastapi import APIRouter, BackgroundTasks, Request, Depends
from sqlalchemy.orm import Session
from fastapi import APIRouter, BackgroundTasks, Request
from app import schemas
from app.chain.webhook import WebhookChain
from app.core.config import settings
from app.db import get_db
router = APIRouter()
def start_webhook_chain(db: Session, body: Any, form: Any, args: Any):
def start_webhook_chain(body: Any, form: Any, args: Any):
"""
启动链式任务
"""
WebhookChain(db).message(body=body, form=form, args=args)
WebhookChain().message(body=body, form=form, args=args)
@router.post("/", summary="Webhook消息响应", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request,
db: Session = Depends(get_db),) -> Any:
) -> Any:
"""
Webhook响应
"""
@@ -30,19 +28,18 @@ async def webhook_message(background_tasks: BackgroundTasks,
body = await request.body()
form = await request.form()
args = request.query_params
background_tasks.add_task(start_webhook_chain, db, body, form, args)
background_tasks.add_task(start_webhook_chain, body, form, args)
return schemas.Response(success=True)
@router.get("/", summary="Webhook消息响应", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request,
db: Session = Depends(get_db)) -> Any:
token: str, request: Request) -> Any:
"""
Webhook响应
"""
if token != settings.API_TOKEN:
return schemas.Response(success=False, message="token认证不通过")
args = request.query_params
background_tasks.add_task(start_webhook_chain, db, None, None, args)
background_tasks.add_task(start_webhook_chain, None, None, args)
return schemas.Response(success=True)

View File

@@ -301,11 +301,11 @@ def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> A
)
tmdbid = term.replace("tmdb:", "")
# 查询媒体信息
mediainfo = MediaChain(db).recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
if not mediainfo:
return [RadarrMovie()]
# 查询是否已存在
exists = MediaChain(db).media_exists(mediainfo=mediainfo)
exists = MediaChain().media_exists(mediainfo=mediainfo)
if not exists:
# 文件不存在
hasfile = False
@@ -390,11 +390,11 @@ def arr_add_movie(apikey: str,
"id": subscribe.id
}
# 添加订阅
sid, message = SubscribeChain(db).add(title=movie.title,
year=movie.year,
mtype=MediaType.MOVIE,
tmdbid=movie.tmdbId,
userid="Seerr")
sid, message = SubscribeChain().add(title=movie.title,
year=movie.year,
mtype=MediaType.MOVIE,
tmdbid=movie.tmdbId,
userid="Seerr")
if sid:
return {
"id": sid
@@ -581,8 +581,8 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
# 获取TVDBID
if not term.startswith("tvdb:"):
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(term),
mtype=MediaType.TV)
mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
mtype=MediaType.TV)
if not mediainfo:
return [SonarrSeries()]
tvdbid = mediainfo.tvdb_id
@@ -593,7 +593,7 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
tvdbid = int(term.replace("tvdb:", ""))
# 查询TVDB信息
tvdbinfo = MediaChain(db).tvdb_info(tvdbid=tvdbid)
tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
if not tvdbinfo:
return [SonarrSeries()]
@@ -605,11 +605,11 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
# 根据TVDB查询媒体信息
if not mediainfo:
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
mtype=MediaType.TV)
mediainfo = MediaChain().recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
mtype=MediaType.TV)
# 查询是否存在
exists = MediaChain(db).media_exists(mediainfo)
exists = MediaChain().media_exists(mediainfo)
if exists:
hasfile = True
else:
@@ -732,12 +732,12 @@ def arr_add_series(apikey: str, tv: schemas.SonarrSeries,
for season in left_seasons:
if not season.get("monitored"):
continue
sid, message = SubscribeChain(db).add(title=tv.title,
year=tv.year,
season=season.get("seasonNumber"),
tmdbid=tv.tmdbId,
mtype=MediaType.TV,
userid="Seerr")
sid, message = SubscribeChain().add(title=tv.title,
year=tv.year,
season=season.get("seasonNumber"),
tmdbid=tv.tmdbId,
mtype=MediaType.TV,
userid="Seerr")
if sid:
return {

View File

@@ -7,7 +7,6 @@ from typing import Optional, Any, Tuple, List, Set, Union, Dict
from qbittorrentapi import TorrentFilesList
from ruamel.yaml import CommentedMap
from sqlalchemy.orm import Session
from transmission_rpc import File
from app.core.config import settings
@@ -28,11 +27,10 @@ class ChainBase(metaclass=ABCMeta):
处理链基类
"""
def __init__(self, db: Session = None):
def __init__(self):
"""
公共初始化
"""
self._db = db
self.modulemanager = ModuleManager()
self.eventmanager = EventManager()
@@ -47,7 +45,7 @@ class ChainBase(metaclass=ABCMeta):
with open(cache_path, 'rb') as f:
return pickle.load(f)
except Exception as err:
logger.error(f"加载缓存 {filename} 出错:{err}")
logger.error(f"加载缓存 {filename} 出错:{str(err)}")
return None
@staticmethod
@@ -59,12 +57,21 @@ class ChainBase(metaclass=ABCMeta):
with open(settings.TEMP_PATH / filename, 'wb') as f:
pickle.dump(cache, f)
except Exception as err:
logger.error(f"保存缓存 {filename} 出错:{err}")
logger.error(f"保存缓存 {filename} 出错:{str(err)}")
finally:
# 主动资源回收
del cache
gc.collect()
@staticmethod
def remove_cache(filename: str) -> None:
"""
删除本地缓存
"""
cache_path = settings.TEMP_PATH / filename
if cache_path.exists():
Path(cache_path).unlink()
def run_module(self, method: str, *args, **kwargs) -> Any:
"""
运行包含该方法的所有模块,然后返回结果
@@ -100,7 +107,7 @@ class ChainBase(metaclass=ABCMeta):
# 中止继续执行
break
except Exception as err:
logger.error(f"运行模块 {method} 出错:{module.__class__.__name__} - {err}\n{traceback.print_exc()}")
logger.error(f"运行模块 {method} 出错:{module.__class__.__name__} - {str(err)}\n{traceback.print_exc()}")
return result
def recognize_media(self, meta: MetaBase = None,
@@ -113,18 +120,22 @@ class ChainBase(metaclass=ABCMeta):
:param tmdbid: tmdbid
:return: 识别的媒体信息,包括剧集信息
"""
if not tmdbid and hasattr(meta, "tmdbid"):
tmdbid = meta.tmdbid
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
def match_doubaninfo(self, name: str, mtype: str = None,
year: str = None, season: int = None) -> Optional[dict]:
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> Optional[dict]:
"""
搜索和匹配豆瓣信息
:param name: 标题
:param imdbid: imdbid
:param mtype: 类型
:param year: 年份
:param season: 季
"""
return self.run_module("match_doubaninfo", name=name, mtype=mtype, year=year, season=season)
return self.run_module("match_doubaninfo", name=name, imdbid=imdbid,
mtype=mtype, year=year, season=season)
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
@@ -396,14 +407,15 @@ class ChainBase(metaclass=ABCMeta):
"""
return self.run_module("post_torrents_message", message=message, torrents=torrents)
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
"""
刮削元数据
:param path: 媒体文件路径
:param mediainfo: 识别的媒体信息
:param transfer_type: 转移模式
:return: 成功或失败
"""
self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
self.run_module("scrape_metadata", path=path, mediainfo=mediainfo, transfer_type=transfer_type)
def register_commands(self, commands: Dict[str, dict]) -> None:
"""

View File

@@ -3,7 +3,6 @@ from typing import Tuple, Optional
from urllib.parse import urljoin
from lxml import etree
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.chain.site import SiteChain
@@ -25,13 +24,13 @@ class CookieCloudChain(ChainBase):
CookieCloud处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
self.siteoper = SiteOper(self._db)
self.siteiconoper = SiteIconOper(self._db)
def __init__(self):
super().__init__()
self.siteoper = SiteOper()
self.siteiconoper = SiteIconOper()
self.siteshelper = SitesHelper()
self.rsshelper = RssHelper()
self.sitechain = SiteChain(self._db)
self.sitechain = SiteChain()
self.message = MessageHelper()
self.cookiecloud = CookieCloudHelper(
server=settings.COOKIECLOUD_HOST,

View File

@@ -2,9 +2,10 @@ from typing import Optional, List
from app import schemas
from app.chain import ChainBase
from app.utils.singleton import Singleton
class DashboardChain(ChainBase):
class DashboardChain(ChainBase, metaclass=Singleton):
"""
各类仪表板统计处理链
"""

View File

@@ -6,11 +6,12 @@ from app.core.context import MediaInfo
from app.core.metainfo import MetaInfo
from app.log import logger
from app.schemas import MediaType
from app.utils.singleton import Singleton
class DoubanChain(ChainBase):
class DoubanChain(ChainBase, metaclass=Singleton):
"""
豆瓣处理链
豆瓣处理链,单例运行
"""
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
@@ -29,18 +30,32 @@ class DoubanChain(ChainBase):
"""
根据豆瓣信息识别媒体信息
"""
# 使用原标题匹配
meta = MetaInfo(title=doubaninfo.get("original_title") or doubaninfo.get("title"))
# 优先使用原标题匹配
season_meta = None
if doubaninfo.get("original_title"):
meta = MetaInfo(title=doubaninfo.get("original_title"))
season_meta = MetaInfo(title=doubaninfo.get("title"))
# 合并季
meta.begin_season = season_meta.begin_season
else:
meta = MetaInfo(title=doubaninfo.get("title"))
# 年份
if doubaninfo.get("year"):
meta.year = doubaninfo.get("year")
# 处理类型
if isinstance(doubaninfo.get('media_type'), MediaType):
meta.type = doubaninfo.get('media_type')
else:
meta.type = MediaType.MOVIE if doubaninfo.get("type") == "movie" else MediaType.TV
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type)
# 使用原标题识别媒体信息
mediainfo = self.recognize_media(meta=meta, mtype=meta.type)
if not mediainfo:
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
if season_meta and season_meta.name != meta.name:
# 使用主标题识别媒体信息
mediainfo = self.recognize_media(meta=season_meta, mtype=season_meta.type)
if not mediainfo:
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
logger.info(f'识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year} {meta.season}')
mediainfo.set_douban_info(doubaninfo)
return Context(meta_info=meta, media_info=mediainfo)
@@ -84,3 +99,9 @@ class DoubanChain(ChainBase):
"""
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) -> List[dict]:
"""
获取动画剧集
"""
return self.run_module("tv_animation", page=page, count=count)

View File

@@ -1,12 +1,11 @@
import base64
import copy
import json
import re
import time
from pathlib import Path
from typing import List, Optional, Tuple, Set, Dict, Union
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.core.config import settings
from app.core.context import MediaInfo, TorrentInfo, Context
@@ -27,11 +26,11 @@ class DownloadChain(ChainBase):
下载处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
def __init__(self):
super().__init__()
self.torrent = TorrentHelper()
self.downloadhis = DownloadHistoryOper(self._db)
self.mediaserver = MediaServerOper(self._db)
self.downloadhis = DownloadHistoryOper()
self.mediaserver = MediaServerOper()
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo,
channel: MessageChannel = None,
@@ -171,7 +170,8 @@ class DownloadChain(ChainBase):
episodes: Set[int] = None,
channel: MessageChannel = None,
save_path: str = None,
userid: Union[str, int] = None) -> Optional[str]:
userid: Union[str, int] = None,
username: str = None) -> Optional[str]:
"""
下载及发送通知
:param context: 资源上下文
@@ -180,6 +180,7 @@ class DownloadChain(ChainBase):
:param channel: 通知渠道
:param save_path: 保存路径
:param userid: 用户ID
:param username: 调用下载的用户名/插件名
"""
_torrent = context.torrent_info
_media = context.media_info
@@ -203,33 +204,31 @@ class DownloadChain(ChainBase):
# 开启下载二级目录
if _media.type == MediaType.MOVIE:
# 电影
download_dir = Path(settings.DOWNLOAD_MOVIE_PATH or settings.DOWNLOAD_PATH) / _media.category
download_dir = settings.SAVE_MOVIE_PATH / _media.category
else:
if settings.DOWNLOAD_ANIME_PATH \
and _media.genre_ids \
if _media.genre_ids \
and set(_media.genre_ids).intersection(set(settings.ANIME_GENREIDS)):
# 动漫
download_dir = Path(settings.DOWNLOAD_ANIME_PATH)
download_dir = settings.SAVE_ANIME_PATH
else:
# 电视剧
download_dir = Path(settings.DOWNLOAD_TV_PATH or settings.DOWNLOAD_PATH) / _media.category
download_dir = settings.SAVE_TV_PATH / _media.category
elif _media:
# 未开启下载二级目录
if _media.type == MediaType.MOVIE:
# 电影
download_dir = Path(settings.DOWNLOAD_MOVIE_PATH or settings.DOWNLOAD_PATH)
download_dir = settings.SAVE_MOVIE_PATH
else:
if settings.DOWNLOAD_ANIME_PATH \
and _media.genre_ids \
if _media.genre_ids \
and set(_media.genre_ids).intersection(set(settings.ANIME_GENREIDS)):
# 动漫
download_dir = Path(settings.DOWNLOAD_ANIME_PATH)
download_dir = settings.SAVE_ANIME_PATH
else:
# 电视剧
download_dir = Path(settings.DOWNLOAD_TV_PATH or settings.DOWNLOAD_PATH)
download_dir = settings.SAVE_TV_PATH
else:
# 未识别
download_dir = Path(settings.DOWNLOAD_PATH)
download_dir = settings.SAVE_PATH
else:
# 自定义下载目录
download_dir = Path(save_path)
@@ -270,6 +269,7 @@ class DownloadChain(ChainBase):
torrent_description=_torrent.description,
torrent_site=_torrent.site_name,
userid=userid,
username=username,
channel=channel.value if channel else None,
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
)
@@ -324,7 +324,8 @@ class DownloadChain(ChainBase):
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
save_path: str = None,
channel: MessageChannel = None,
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
userid: str = None,
username: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
"""
根据缺失数据,自动种子列表中组合择优下载
:param contexts: 资源上下文列表
@@ -332,6 +333,7 @@ class DownloadChain(ChainBase):
:param save_path: 保存路径
:param channel: 通知渠道
:param userid: 用户ID
:param username: 调用下载的用户名/插件名
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
"""
# 已下载的项目
@@ -347,11 +349,13 @@ class DownloadChain(ChainBase):
# 剩余季数
need = list(set(_need).difference(set(_current)))
# 清除已下载的季信息
for _sea in list(no_exists.get(_tmdbid)):
seas = copy.deepcopy(no_exists.get(_tmdbid))
for _sea in list(seas):
if _sea not in need:
no_exists[_tmdbid].pop(_sea)
if not no_exists.get(_tmdbid) and no_exists.get(_tmdbid) is not None:
no_exists.pop(_tmdbid)
break
return need
def __update_episodes(_tmdbid: int, _sea: int, _need: list, _current: set) -> list:
@@ -396,7 +400,7 @@ class DownloadChain(ChainBase):
for context in contexts:
if context.media_info.type == MediaType.MOVIE:
if self.download_single(context, save_path=save_path,
channel=channel, userid=userid):
channel=channel, userid=userid, username=username):
# 下载成功
downloaded_list.append(context)
@@ -447,13 +451,13 @@ class DownloadChain(ChainBase):
logger.info(f"{meta.org_string} 解析文件集数为 {torrent_episodes}")
if not torrent_episodes:
continue
# 总集数
# 更新集数范围
begin_ep = min(torrent_episodes)
end_ep = max(torrent_episodes)
meta.set_episodes(begin=begin_ep, end=end_ep)
# 需要总集数
need_total = __get_season_episodes(need_tmdbid, torrent_season[0])
if len(torrent_episodes) < need_total:
# 更新集数范围
begin_ep = min(torrent_episodes)
end_ep = max(torrent_episodes)
meta.set_episodes(begin=begin_ep, end=end_ep)
logger.info(
f"{meta.org_string} 解析文件集数发现不是完整合集")
continue
@@ -464,12 +468,13 @@ class DownloadChain(ChainBase):
torrent_file=content if isinstance(content, Path) else None,
save_path=save_path,
channel=channel,
userid=userid
userid=userid,
username=username
)
else:
# 下载
download_id = self.download_single(context, save_path=save_path,
channel=channel, userid=userid)
channel=channel, userid=userid, username=username)
if download_id:
# 下载成功
@@ -487,8 +492,9 @@ class DownloadChain(ChainBase):
need_tv = no_exists.get(need_tmdbid)
if not need_tv:
continue
need_tv_copy = copy.deepcopy(no_exists.get(need_tmdbid))
# 循环每一季
for sea, tv in need_tv.items():
for sea, tv in need_tv_copy.items():
# 当前需要季
need_season = sea
# 当前需要集
@@ -528,7 +534,7 @@ class DownloadChain(ChainBase):
if torrent_episodes.issubset(set(need_episodes)):
# 下载
download_id = self.download_single(context, save_path=save_path,
channel=channel, userid=userid)
channel=channel, userid=userid, username=username)
if download_id:
# 下载成功
downloaded_list.append(context)
@@ -606,15 +612,17 @@ class DownloadChain(ChainBase):
episodes=selected_episodes,
save_path=save_path,
channel=channel,
userid=userid
userid=userid,
username=username
)
if not download_id:
continue
# 把识别的集更新到上下文
context.meta_info.begin_episode = min(selected_episodes)
context.meta_info.end_episode = max(selected_episodes)
# 下载成功
downloaded_list.append(context)
# 更新种子集数范围
begin_ep = min(torrent_episodes)
end_ep = max(torrent_episodes)
meta.set_episodes(begin=begin_ep, end=end_ep)
# 更新仍需集数
need_episodes = __update_episodes(_tmdbid=need_tmdbid,
_need=need_episodes,
@@ -786,6 +794,7 @@ class DownloadChain(ChainBase):
for torrent in torrents:
history = self.downloadhis.get_by_hash(torrent.hash)
if history:
# 媒体信息
torrent.media = {
"tmdbid": history.tmdbid,
"type": history.type,
@@ -794,6 +803,8 @@ class DownloadChain(ChainBase):
"episode": history.episodes,
"image": history.image,
}
# 下载用户
torrent.userid = history.userid
ret_torrents.append(torrent)
return ret_torrents

View File

@@ -1,18 +1,31 @@
import copy
import time
from pathlib import Path
from threading import Lock
from typing import Optional, List, Tuple
from app.chain import ChainBase
from app.core.context import Context, MediaInfo
from app.core.event import eventmanager, Event
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo, MetaInfoPath
from app.log import logger
from app.schemas.types import EventType, MediaType
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
class MediaChain(ChainBase):
recognize_lock = Lock()
class MediaChain(ChainBase, metaclass=Singleton):
"""
媒体信息处理链
媒体信息处理链,单例运行
"""
# 临时识别标题
recognize_title: Optional[str] = None
# 临时识别结果 {title, name, year, season, episode}
recognize_temp: Optional[dict] = None
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
@@ -24,14 +37,104 @@ class MediaChain(ChainBase):
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta_info=metainfo)
# 偿试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(EventType.NameRecognize):
logger.info(f'请求辅助识别,标题:{title} ...')
mediainfo = self.recognize_help(title=title, org_meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta_info=metainfo)
# 识别成功
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 返回上下文
return Context(meta_info=metainfo, media_info=mediainfo)
def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[MediaInfo]:
"""
请求辅助识别,返回媒体信息
:param title: 标题
:param org_meta: 原始元数据
"""
with recognize_lock:
self.recognize_temp = None
self.recognize_title = title
# 发送请求事件
eventmanager.send_event(
EventType.NameRecognize,
{
'title': title,
}
)
# 每0.5秒循环一次等待结果直到10秒后超时
for i in range(10):
if self.recognize_temp is not None:
break
time.sleep(0.5)
# 加锁
with recognize_lock:
mediainfo = None
if not self.recognize_temp or self.recognize_title != title:
# 没有识别结果或者识别标题已改变
return None
# 有识别结果
meta_dict = copy.deepcopy(self.recognize_temp)
logger.info(f'获取到辅助识别结果:{meta_dict}')
if meta_dict.get("name") == org_meta.name and meta_dict.get("year") == org_meta.year:
logger.info(f'辅助识别结果与原始识别结果一致')
else:
logger.info(f'辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...')
org_meta.name = meta_dict.get("name")
org_meta.year = meta_dict.get("year")
org_meta.begin_season = meta_dict.get("season")
org_meta.begin_episode = meta_dict.get("episode")
if org_meta.begin_season or org_meta.begin_episode:
org_meta.type = MediaType.TV
# 重新识别
mediainfo = self.recognize_media(meta=org_meta)
return mediainfo
@eventmanager.register(EventType.NameRecognizeResult)
def recognize_result(self, event: Event):
"""
监控识别结果事件,获取辅助识别结果,结果格式:{title, name, year, season, episode}
"""
if not event:
return
event_data = event.event_data or {}
# 加锁
with recognize_lock:
# 不是原标题的结果不要
if event_data.get("title") != self.recognize_title:
return
# 标志收到返回
self.recognize_temp = {}
# 处理数据格式
file_title, file_year, season_number, episode_number = None, None, None, None
if event_data.get("name"):
file_title = str(event_data["name"]).split("/")[0].strip().replace(".", " ")
if event_data.get("year"):
file_year = str(event_data["year"]).split("/")[0].strip()
if event_data.get("season") and str(event_data["season"]).isdigit():
season_number = int(event_data["season"])
if event_data.get("episode") and str(event_data["episode"]).isdigit():
episode_number = int(event_data["episode"])
if not file_title:
return
if file_title == 'Unknown':
return
if not str(file_year).isdigit():
file_year = None
# 结果赋值
self.recognize_temp = {
"name": file_title,
"year": file_year,
"season": season_number,
"episode": episode_number
}
def recognize_by_path(self, path: str) -> Optional[Context]:
"""
根据文件路径识别媒体信息
@@ -43,8 +146,13 @@ class MediaChain(ChainBase):
# 识别媒体信息
mediainfo = self.recognize_media(meta=file_meta)
if not mediainfo:
logger.warn(f'{path} 未识别到媒体信息')
return Context(meta_info=file_meta)
# 偿试使用辅助识别,如果有注册响应事件的话
if eventmanager.check(EventType.NameRecognize):
logger.info(f'请求辅助识别,标题:{file_path.name} ...')
mediainfo = self.recognize_help(title=path, org_meta=file_meta)
if not mediainfo:
logger.warn(f'{path} 未识别到媒体信息')
return Context(meta_info=file_meta)
logger.info(f'{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)

View File

@@ -1,13 +1,10 @@
import json
import threading
from typing import List, Union, Generator
from sqlalchemy.orm import Session
from typing import List, Union
from app import schemas
from app.chain import ChainBase
from app.core.config import settings
from app.db import SessionFactory
from app.db.mediaserver_oper import MediaServerOper
from app.log import logger
@@ -19,8 +16,9 @@ class MediaServerChain(ChainBase):
媒体服务器处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
def __init__(self):
super().__init__()
self.dboper = MediaServerOper()
def librarys(self, server: str) -> List[schemas.MediaServerLibrary]:
"""
@@ -51,13 +49,10 @@ class MediaServerChain(ChainBase):
同步媒体库所有数据到本地数据库
"""
with lock:
# 媒体服务器同步使用独立的会话
_db = SessionFactory()
_dbOper = MediaServerOper(_db)
# 汇总统计
total_count = 0
# 清空登记薄
_dbOper.empty(server=settings.MEDIASERVER)
self.dboper.empty(server=settings.MEDIASERVER)
# 同步黑名单
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
@@ -79,6 +74,7 @@ class MediaServerChain(ChainBase):
continue
if not item.item_id:
continue
logger.debug(f"正在同步 {item.title} ...")
# 计数
library_count += 1
seasoninfo = {}
@@ -93,11 +89,8 @@ class MediaServerChain(ChainBase):
item_dict = item.dict()
item_dict['seasoninfo'] = json.dumps(seasoninfo)
item_dict['item_type'] = item_type
_dbOper.add(**item_dict)
self.dboper.add(**item_dict)
logger.info(f"{mediaserver} 媒体库 {library.name} 同步完成,共同步数量:{library_count}")
# 总数累加
total_count += library_count
# 关闭数据库连接
if _db:
_db.close()
logger.info("【MediaServer】媒体库数据同步完成同步数量%s" % total_count)

View File

@@ -1,3 +1,4 @@
import copy
from typing import Any
from app.chain.download import *
@@ -27,12 +28,12 @@ class MessageChain(ChainBase):
# 每页数据量
_page_size: int = 8
def __init__(self, db: Session = None):
super().__init__(db)
self.downloadchain = DownloadChain(self._db)
self.subscribechain = SubscribeChain(self._db)
self.searchchain = SearchChain(self._db)
self.medtachain = MediaChain(self._db)
def __init__(self):
super().__init__()
self.downloadchain = DownloadChain()
self.subscribechain = SubscribeChain()
self.searchchain = SearchChain()
self.medtachain = MediaChain()
self.torrent = TorrentHelper()
self.eventmanager = EventManager()
self.torrenthelper = TorrentHelper()
@@ -86,13 +87,15 @@ class MessageChain(ChainBase):
# 发送消息
self.post_message(Notification(channel=channel, title="输入有误!", userid=userid))
return
# 选择的序号
_choice = int(text) + _current_page * self._page_size - 1
# 缓存类型
cache_type: str = cache_data.get('type')
# 缓存列表
cache_list: list = cache_data.get('items')
cache_list: list = copy.deepcopy(cache_data.get('items'))
# 选择
if cache_type == "Search":
mediainfo: MediaInfo = cache_list[int(text) + _current_page * self._page_size - 1]
mediainfo: MediaInfo = cache_list[_choice]
_current_media = mediainfo
# 查询缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=_current_meta,
@@ -111,7 +114,8 @@ class MessageChain(ChainBase):
f"{sea} 季缺失 {StringUtils.str_series(no_exist.episodes) if no_exist.episodes else no_exist.total_episode}"
for sea, no_exist in no_exists.get(mediainfo.tmdb_id).items()]
self.post_message(Notification(channel=channel,
title=f"{mediainfo.title_year}\n" + "\n".join(messages)))
title=f"{mediainfo.title_year}\n" + "\n".join(messages),
userid=userid))
# 搜索种子,过滤掉不需要的剧集,以便选择
logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...")
self.post_message(
@@ -156,7 +160,7 @@ class MessageChain(ChainBase):
elif cache_type == "Subscribe":
# 订阅媒体
mediainfo: MediaInfo = cache_list[int(text) - 1]
mediainfo: MediaInfo = cache_list[_choice]
# 查询缺失的媒体信息
exist_flag, _ = self.downloadchain.get_no_exists_info(meta=_current_meta,
mediainfo=mediainfo)
@@ -185,9 +189,9 @@ class MessageChain(ChainBase):
username=username)
else:
# 下载种子
context: Context = cache_list[int(text) - 1]
context: Context = cache_list[_choice]
# 下载
self.downloadchain.download_single(context, userid=userid, channel=channel)
self.downloadchain.download_single(context, userid=userid, channel=channel, username=username)
elif text.lower() == "p":
# 上一页
@@ -203,10 +207,11 @@ class MessageChain(ChainBase):
self.post_message(Notification(
channel=channel, title="已经是第一页了!", userid=userid))
return
cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items')
# 减一页
_current_page -= 1
cache_type: str = cache_data.get('type')
# 产生副本,避免修改原值
cache_list: list = copy.deepcopy(cache_data.get('items'))
if _current_page == 0:
start = 0
end = self._page_size
@@ -214,11 +219,6 @@ class MessageChain(ChainBase):
start = _current_page * self._page_size
end = start + self._page_size
if cache_type == "Torrent":
# 更新缓存
user_cache[userid] = {
"type": "Torrent",
"items": cache_list[start:end]
}
# 发送种子数据
self.__post_torrents_message(channel=channel,
title=_current_media.title,
@@ -242,7 +242,8 @@ class MessageChain(ChainBase):
channel=channel, title="输入有误!", userid=userid))
return
cache_type: str = cache_data.get('type')
cache_list: list = cache_data.get('items')
# 产生副本,避免修改原值
cache_list: list = copy.deepcopy(cache_data.get('items'))
total = len(cache_list)
# 加一页
cache_list = cache_list[
@@ -256,11 +257,6 @@ class MessageChain(ChainBase):
# 加一页
_current_page += 1
if cache_type == "Torrent":
# 更新缓存
user_cache[userid] = {
"type": "Torrent",
"items": cache_list
}
# 发送种子数据
self.__post_torrents_message(channel=channel,
title=_current_media.title,
@@ -349,7 +345,8 @@ class MessageChain(ChainBase):
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
no_exists=no_exists,
channel=channel,
userid=userid)
userid=userid,
username=username)
if downloads and not lefts:
# 全部下载完成
logger.info(f'{_current_media.title_year} 下载完成')

View File

@@ -5,8 +5,6 @@ from datetime import datetime
from typing import Dict
from typing import List, Optional
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.core.context import Context
from app.core.context import MediaInfo, TorrentInfo
@@ -26,8 +24,8 @@ class SearchChain(ChainBase):
站点资源搜索处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
def __init__(self):
super().__init__()
self.siteshelper = SitesHelper()
self.progress = ProgressHelper()
self.systemconfig = SystemConfigOper()
@@ -141,7 +139,7 @@ class SearchChain(ChainBase):
if not torrents:
logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源')
return []
# 使用默认过滤规则再次过滤
# 使用过滤规则再次过滤
torrents = self.filter_torrents_by_rule(torrents=torrents,
filter_rule=filter_rule)
if not torrents:
@@ -333,15 +331,21 @@ class SearchChain(ChainBase):
:param filter_rule: 过滤规则
"""
# 取默认过滤规则
if not filter_rule:
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
# 没有则取搜索默认过滤规则
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultSearchFilterRules)
if not filter_rule:
return torrents
# 包含
include = filter_rule.get("include")
# 排除
exclude = filter_rule.get("exclude")
# 质量
quality = filter_rule.get("quality")
# 分辨率
resolution = filter_rule.get("resolution")
# 特效
effect = filter_rule.get("effect")
def __filter_torrent(t: TorrentInfo) -> bool:
"""
@@ -359,6 +363,24 @@ class SearchChain(ChainBase):
f"{t.title} {t.description}", re.I):
logger.info(f"{t.title} 匹配排除规则 {exclude}")
return False
# 质量
if quality:
if not re.search(r"%s" % quality, t.title, re.I):
logger.info(f"{t.title} 不匹配质量规则 {quality}")
return False
# 分辨率
if resolution:
if not re.search(r"%s" % resolution, t.title, re.I):
logger.info(f"{t.title} 不匹配分辨率规则 {resolution}")
return False
# 特效
if effect:
if not re.search(r"%s" % effect, t.title, re.I):
logger.info(f"{t.title} 不匹配特效规则 {effect}")
return False
return True
# 使用默认过滤规则再次过滤

View File

@@ -1,8 +1,6 @@
import re
from typing import Union, Tuple
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.core.config import settings
from app.db.models.site import Site
@@ -23,9 +21,9 @@ class SiteChain(ChainBase):
站点管理处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
self.siteoper = SiteOper(self._db)
def __init__(self):
super().__init__()
self.siteoper = SiteOper()
self.cookiehelper = CookieHelper()
self.message = MessageHelper()

View File

@@ -1,11 +1,12 @@
import json
import random
import re
import time
from datetime import datetime
from typing import Dict, List, Optional, Union, Tuple
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.chain.douban import DoubanChain
from app.chain.download import DownloadChain
from app.chain.search import SearchChain
from app.chain.torrents import TorrentsChain
@@ -26,11 +27,11 @@ class SubscribeChain(ChainBase):
订阅管理处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
self.downloadchain = DownloadChain(self._db)
self.searchchain = SearchChain(self._db)
self.subscribeoper = SubscribeOper(self._db)
def __init__(self):
super().__init__()
self.downloadchain = DownloadChain()
self.searchchain = SearchChain()
self.subscribeoper = SubscribeOper()
self.torrentschain = TorrentsChain()
self.message = MessageHelper()
self.systemconfig = SystemConfigOper()
@@ -50,18 +51,28 @@ class SubscribeChain(ChainBase):
识别媒体信息并添加订阅
"""
logger.info(f'开始添加订阅,标题:{title} ...')
# 识别元数据
metainfo = MetaInfo(title)
if year:
metainfo.year = year
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
if not mediainfo:
metainfo = None
mediainfo = None
if not tmdbid and doubanid:
# 将豆瓣信息转换为TMDB信息
context = DoubanChain().recognize_by_doubanid(doubanid)
if context:
metainfo = context.meta_info
mediainfo = context.media_info
else:
# 识别元数据
metainfo = MetaInfo(title)
if year:
metainfo.year = year
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
# 识别失败
if not mediainfo or not metainfo or not mediainfo.tmdb_id:
logger.warn(f'未识别到媒体信息,标题:{title}tmdbid{tmdbid}')
return None, "未识别到媒体信息"
# 更新媒体图片
@@ -74,8 +85,8 @@ class SubscribeChain(ChainBase):
if not kwargs.get('total_episode'):
if not mediainfo.seasons:
# 补充媒体信息
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
mediainfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
return None, "媒体信息识别失败"
@@ -85,7 +96,7 @@ class SubscribeChain(ChainBase):
total_episode = len(mediainfo.seasons.get(season) or [])
if not total_episode:
logger.error(f'未获取到总集数,标题:{title}tmdbid{tmdbid}')
return None, "未获取到总集数"
return None, f"未获取到{season} 季的总集数"
kwargs.update({
'total_episode': total_episode
})
@@ -153,6 +164,11 @@ class SubscribeChain(ChainBase):
if (now - subscribe_time).total_seconds() < 60:
logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟暂不搜索...")
continue
# 随机休眠1-5分钟
if not sid and state == 'R':
sleep_time = random.randint(60, 300)
logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...')
time.sleep(sleep_time)
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
# 如果状态为N则更新为R
if subscribe.state == 'N':
@@ -176,66 +192,66 @@ class SubscribeChain(ChainBase):
totals = {
subscribe.season: subscribe.total_episode
}
# 查询缺失的媒体信息
# 查询媒体库缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
meta=meta,
mediainfo=mediainfo,
totals=totals
)
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
else:
# 洗版状态
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
subscribe.tmdbid: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 已存在
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
continue
# 电视剧订阅处理缺失集
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
# 站点范围
if subscribe.sites:
sites = json.loads(subscribe.sites)
else:
sites = None
# 优先级过滤规则
if subscribe.best_version:
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
else:
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
# 默认过滤规则
if subscribe.include or subscribe.exclude:
filter_rule = {
"include": subscribe.include,
"exclude": subscribe.exclude
}
else:
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
# 过滤规则
filter_rule = self.get_filter_rule(subscribe)
# 搜索,同时电视剧会过滤掉不需要的剧集
contexts = self.searchchain.process(mediainfo=mediainfo,
keyword=subscribe.keyword,
@@ -247,8 +263,10 @@ class SubscribeChain(ChainBase):
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
if meta.type == MediaType.TV:
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
continue
# 过滤
matched_contexts = []
for context in contexts:
@@ -278,11 +296,13 @@ class SubscribeChain(ChainBase):
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
if meta.type == MediaType.TV and not subscribe.best_version:
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
continue
# 自动下载
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
no_exists=no_exists)
no_exists=no_exists, username=subscribe.username)
# 更新已经下载的集数
if downloads \
and meta.type == MediaType.TV \
@@ -299,8 +319,9 @@ class SubscribeChain(ChainBase):
if meta.type == MediaType.TV and not subscribe.best_version:
# 更新订阅剩余集数和时间
update_date = True if downloads else False
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
mediainfo=mediainfo, update_date=update_date)
# 手动触发时发送系统消息
if manual:
if sid:
@@ -309,19 +330,19 @@ class SubscribeChain(ChainBase):
self.message.put('所有订阅搜索完成!')
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
mediainfo: MediaInfo, downloads: List[Context]):
mediainfo: MediaInfo, downloads: List[Context] = None):
"""
判断是否应完成订阅
"""
if not subscribe.best_version:
# 全部下载完成
logger.info(f'{mediainfo.title_year} 下载完成,完成订阅')
logger.info(f'{mediainfo.title_year} 完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
else:
elif downloads:
# 当前下载资源的优先级
priority = max([item.torrent_info.pri_order for item in downloads])
if priority == 100:
@@ -375,6 +396,67 @@ class SubscribeChain(ChainBase):
return ret_sites
def get_filter_rule(self, subscribe: Subscribe):
"""
获取订阅过滤规则,没有则返回默认规则
"""
# 默认过滤规则
if (subscribe.include
or subscribe.exclude
or subscribe.quality
or subscribe.resolution
or subscribe.effect):
return {
"include": subscribe.include,
"exclude": subscribe.exclude,
"quality": subscribe.quality,
"resolution": subscribe.resolution,
"effect": subscribe.effect,
}
# 订阅默认过滤规则
return self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
@staticmethod
def check_filter_rule(torrent_info: TorrentInfo, filter_rule: Dict[str, str]) -> bool:
"""
检查种子是否匹配订阅过滤规则
"""
if not filter_rule:
return True
# 包含
include = filter_rule.get("include")
if include:
if not re.search(r"%s" % include,
f"{torrent_info.title} {torrent_info.description}", re.I):
logger.info(f"{torrent_info.title} 不匹配包含规则 {include}")
return False
# 排除
exclude = filter_rule.get("exclude")
if exclude:
if re.search(r"%s" % exclude,
f"{torrent_info.title} {torrent_info.description}", re.I):
logger.info(f"{torrent_info.title} 匹配排除规则 {exclude}")
return False
# 质量
quality = filter_rule.get("quality")
if quality:
if not re.search(r"%s" % quality, torrent_info.title, re.I):
logger.info(f"{torrent_info.title} 不匹配质量规则 {quality}")
return False
# 分辨率
resolution = filter_rule.get("resolution")
if resolution:
if not re.search(r"%s" % resolution, torrent_info.title, re.I):
logger.info(f"{torrent_info.title} 不匹配分辨率规则 {resolution}")
return False
# 特效
effect = filter_rule.get("effect")
if effect:
if not re.search(r"%s" % effect, torrent_info.title, re.I):
logger.info(f"{torrent_info.title} 不匹配特效规则 {effect}")
return False
return True
def match(self, torrents: Dict[str, List[Context]]):
"""
从缓存中匹配订阅,并自动下载
@@ -411,46 +493,48 @@ class SubscribeChain(ChainBase):
mediainfo=mediainfo,
totals=totals
)
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
self.subscribeoper.delete(subscribe.id)
# 发送通知
self.post_message(Notification(mtype=NotificationType.Subscribe,
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
image=mediainfo.get_message_image()))
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
else:
# 洗版
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
subscribe.tmdbid: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 默认过滤规则
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
include = subscribe.include or default_filter.get("include")
exclude = subscribe.exclude or default_filter.get("exclude")
# 已存在
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
)
# 打印缺失集信息
if no_exists and no_exists.get(subscribe.tmdbid):
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
# 过滤规则
filter_rule = self.get_filter_rule(subscribe)
# 遍历缓存种子
_match_context = []
for domain, contexts in torrents.items():
@@ -465,11 +549,11 @@ class SubscribeChain(ChainBase):
continue
# 优先级过滤规则
if subscribe.best_version:
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
else:
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
result: List[TorrentInfo] = self.filter_torrents(
rule_string=filter_rule,
rule_string=priority_rule,
torrent_list=[torrent_info],
mediainfo=torrent_mediainfo)
if result is not None and not result:
@@ -510,7 +594,8 @@ class SubscribeChain(ChainBase):
set(torrent_meta.episode_list)
):
logger.info(
f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 未包含缺失的剧集')
f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 未包含缺失的剧集'
)
continue
# 过滤掉已经下载的集数
if self.__check_subscribe_note(subscribe, torrent_meta.episode_list):
@@ -522,26 +607,22 @@ class SubscribeChain(ChainBase):
if torrent_meta.episode_list:
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季')
continue
# 包含
if include:
if not re.search(r"%s" % include,
f"{torrent_info.title} {torrent_info.description}", re.I):
logger.info(f"{torrent_info.title} 不匹配包含规则 {include}")
continue
# 排除
if exclude:
if re.search(r"%s" % exclude,
f"{torrent_info.title} {torrent_info.description}", re.I):
logger.info(f"{torrent_info.title} 匹配排除规则 {exclude}")
continue
# 过滤规则
if not self.check_filter_rule(torrent_info=torrent_info,
filter_rule=filter_rule):
continue
# 匹配成功
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
_match_context.append(context)
# 开始下载
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
if _match_context:
# 批量择优下载
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists)
downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists,
username=subscribe.username)
# 更新已经下载的集数
if downloads and meta.type == MediaType.TV:
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
@@ -554,12 +635,13 @@ class SubscribeChain(ChainBase):
if meta.type == MediaType.TV and not subscribe.best_version:
update_date = True if downloads else False
# 未完成下载,计算剩余集数
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
mediainfo=mediainfo, update_date=update_date)
else:
if meta.type == MediaType.TV:
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
meta=meta, mediainfo=mediainfo)
def check(self):
"""
@@ -651,31 +733,36 @@ class SubscribeChain(ChainBase):
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
subscribe: Subscribe,
meta: MetaBase,
mediainfo: MediaInfo,
update_date: bool = False):
"""
更新订阅剩余集数
"""
left_seasons = lefts.get(mediainfo.tmdb_id) or {}
for season_info in left_seasons.values():
season = season_info.season
if season == subscribe.season:
left_episodes = season_info.episodes
if not left_episodes:
lack_episode = season_info.total_episode
else:
lack_episode = len(left_episodes)
logger.info(f'{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...')
if update_date:
# 同时更新最后时间
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode,
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode
})
left_seasons = lefts.get(mediainfo.tmdb_id)
if left_seasons:
for season_info in left_seasons.values():
season = season_info.season
if season == subscribe.season:
left_episodes = season_info.episodes
if not left_episodes:
lack_episode = season_info.total_episode
else:
lack_episode = len(left_episodes)
logger.info(f'{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...')
if update_date:
# 同时更新最后时间
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode,
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
else:
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode
})
else:
# 判断是否应完成订阅
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
"""

View File

@@ -1,13 +1,23 @@
import json
import re
from typing import Union
from app.chain import ChainBase
from app.core.config import settings
from app.log import logger
from app.schemas import Notification, MessageChannel
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.system import SystemUtils
class SystemChain(ChainBase):
class SystemChain(ChainBase, metaclass=Singleton):
"""
系统级处理链
"""
_restart_file = "__system_restart__"
def remote_clear_cache(self, channel: MessageChannel, userid: Union[int, str]):
"""
清理系统缓存
@@ -15,3 +25,95 @@ class SystemChain(ChainBase):
self.clear_cache()
self.post_message(Notification(channel=channel,
title=f"缓存清理完成!", userid=userid))
def restart(self, channel: MessageChannel, userid: Union[int, str]):
"""
重启系统
"""
if channel and userid:
self.post_message(Notification(channel=channel,
title="系统正在重启,请耐心等候!", userid=userid))
# 保存重启信息
self.save_cache({
"channel": channel.value,
"userid": userid
}, self._restart_file)
SystemUtils.restart()
def version(self, channel: MessageChannel, userid: Union[int, str]):
"""
查看当前版本、远程版本
"""
release_version = self.__get_release_version()
local_version = self.get_local_version()
if release_version == local_version:
title = f"当前版本:{local_version},已是最新版本"
else:
title = f"当前版本:{local_version},远程版本:{release_version}"
self.post_message(Notification(channel=channel,
title=title, userid=userid))
def restart_finish(self):
"""
如通过交互命令重启,
重启完发送msg
"""
# 重启消息
restart_channel = self.load_cache(self._restart_file)
if restart_channel:
# 发送重启完成msg
if not isinstance(restart_channel, dict):
restart_channel = json.loads(restart_channel)
channel = next(
(channel for channel in MessageChannel.__members__.values() if
channel.value == restart_channel.get('channel')), None)
userid = restart_channel.get('userid')
# 版本号
release_version = self.__get_release_version()
local_version = self.get_local_version()
if release_version == local_version:
title = f"当前版本:{local_version}"
else:
title = f"当前版本:{local_version},远程版本:{release_version}"
self.post_message(Notification(channel=channel,
title=f"系统已重启完成!{title}",
userid=userid))
self.remove_cache(self._restart_file)
@staticmethod
def __get_release_version():
"""
获取最新版本
"""
version_res = RequestUtils(proxies=settings.PROXY).get_res(
"https://api.github.com/repos/jxxghp/MoviePilot/releases/latest")
if version_res:
ver_json = version_res.json()
version = f"{ver_json['tag_name']}"
return version
else:
return None
@staticmethod
def get_local_version():
"""
查看当前版本
"""
version_file = settings.ROOT_PATH / "version.py"
if version_file.exists():
try:
with open(version_file, 'rb') as f:
version = f.read()
pattern = r"'([^']*)'"
match = re.search(pattern, str(version))
if match:
version = match.group(1)
return version
else:
logger.warn("未找到版本号")
return None
except Exception as err:
logger.error(f"加载版本文件 {version_file} 出错:{str(err)}")

View File

@@ -12,7 +12,7 @@ from app.utils.singleton import Singleton
class TmdbChain(ChainBase, metaclass=Singleton):
"""
TheMovieDB处理链
TheMovieDB处理链,单例运行
"""
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,

View File

@@ -7,7 +7,6 @@ from app.chain import ChainBase
from app.core.config import settings
from app.core.context import TorrentInfo, Context, MediaInfo
from app.core.metainfo import MetaInfo
from app.db import SessionFactory
from app.db.site_oper import SiteOper
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.rss import RssHelper
@@ -28,10 +27,9 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
_rss_file = "__rss_cache__"
def __init__(self):
self._db = SessionFactory()
super().__init__(self._db)
super().__init__()
self.siteshelper = SitesHelper()
self.siteoper = SiteOper(self._db)
self.siteoper = SiteOper()
self.rsshelper = RssHelper()
self.systemconfig = SystemConfigOper()
@@ -60,7 +58,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
else:
return self.load_cache(self._rss_file) or {}
@cached(cache=TTLCache(maxsize=128 if settings.BIG_MEMORY_MODE else 1, ttl=600))
@cached(cache=TTLCache(maxsize=128, ttl=600))
def browse(self, domain: str) -> List[TorrentInfo]:
"""
浏览站点首页内容返回种子清单TTL缓存10分钟
@@ -73,7 +71,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
return []
return self.refresh_torrents(site=site)
@cached(cache=TTLCache(maxsize=128 if settings.BIG_MEMORY_MODE else 1, ttl=300))
@cached(cache=TTLCache(maxsize=128, ttl=300))
def rss(self, domain: str) -> List[TorrentInfo]:
"""
获取站点RSS内容返回种子清单TTL缓存5分钟

View File

@@ -1,12 +1,9 @@
import glob
import re
import shutil
import threading
from pathlib import Path
from typing import List, Optional, Tuple, Union, Dict
from sqlalchemy.orm import Session
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.chain.tmdb import TmdbChain
@@ -36,13 +33,13 @@ class TransferChain(ChainBase):
文件转移处理链
"""
def __init__(self, db: Session = None):
super().__init__(db)
self.downloadhis = DownloadHistoryOper(self._db)
self.transferhis = TransferHistoryOper(self._db)
def __init__(self):
super().__init__()
self.downloadhis = DownloadHistoryOper()
self.transferhis = TransferHistoryOper()
self.progress = ProgressHelper()
self.mediachain = MediaChain(self._db)
self.tmdbchain = TmdbChain(self._db)
self.mediachain = MediaChain()
self.tmdbchain = TmdbChain()
self.systemconfig = SystemConfigOper()
def process(self) -> bool:
@@ -237,7 +234,7 @@ class TransferChain(ChainBase):
# 自定义识别
if formaterHandler:
# 开始集、结束集、PART
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.stem)
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.name)
if begin_ep is not None:
file_meta.begin_episode = begin_ep
file_meta.part = part
@@ -358,7 +355,9 @@ class TransferChain(ChainBase):
)
# 刮削单个文件
if settings.SCRAP_METADATA:
self.scrape_metadata(path=transferinfo.target_path, mediainfo=file_mediainfo)
self.scrape_metadata(path=transferinfo.target_path,
mediainfo=file_mediainfo,
transfer_type=transfer_type)
# 更新进度
processed_num += 1
self.progress.update(value=processed_num / total_num * 100,
@@ -489,7 +488,7 @@ class TransferChain(ChainBase):
def re_transfer(self, logid: int,
mtype: MediaType = None, tmdbid: int = None) -> Tuple[bool, str]:
"""
根据历史记录,重新识别转移,只处理对应的src目录
根据历史记录,重新识别转移,只支持简单条件
:param logid: 历史记录ID
:param mtype: 媒体类型
:param tmdbid: TMDB ID
@@ -499,7 +498,7 @@ class TransferChain(ChainBase):
if not history:
logger.error(f"历史记录不存在ID{logid}")
return False, "历史记录不存在"
# 没有下载记录,按源目录路径重新转移
# 按源目录路径重新转移
src_path = Path(history.src)
if not src_path.exists():
return False, f"源目录不存在:{src_path}"
@@ -539,9 +538,10 @@ class TransferChain(ChainBase):
season: int = None,
transfer_type: str = None,
epformat: EpisodeFormat = None,
min_filesize: int = 0) -> Tuple[bool, Union[str, list]]:
min_filesize: int = 0,
force: bool = False) -> Tuple[bool, Union[str, list]]:
"""
手动转移
手动转移,支持复杂条件,带进度显示
:param in_path: 源文件路径
:param target: 目标路径
:param tmdbid: TMDB ID
@@ -550,6 +550,7 @@ class TransferChain(ChainBase):
:param transfer_type: 转移类型
:param epformat: 剧集格式
:param min_filesize: 最小文件大小(MB)
:param force: 是否强制转移
"""
logger.info(f"手动转移:{in_path} ...")
@@ -569,9 +570,11 @@ class TransferChain(ChainBase):
path=in_path,
mediainfo=mediainfo,
target=target,
transfer_type=transfer_type,
season=season,
epformat=epformat,
min_filesize=min_filesize
min_filesize=min_filesize,
force=force
)
if not state:
return False, errmsg
@@ -586,7 +589,8 @@ class TransferChain(ChainBase):
transfer_type=transfer_type,
season=season,
epformat=epformat,
min_filesize=min_filesize)
min_filesize=min_filesize,
force=force)
return state, errmsg
def send_transfer_message(self, meta: MetaBase, mediainfo: MediaInfo,
@@ -613,14 +617,15 @@ class TransferChain(ChainBase):
title=msg_title, text=msg_str, image=mediainfo.get_message_image()))
@staticmethod
def delete_files(path: Path):
def delete_files(path: Path) -> Tuple[bool, str]:
"""
删除转移后的文件以及空目录
:param path: 文件路径
:return: 成功标识,错误信息
"""
logger.info(f"开始删除文件以及空目录:{path} ...")
if not path.exists():
return
return True, f"文件或目录不存在:{path}"
if path.is_file():
# 删除文件、nfo、jpg等同名文件
pattern = path.stem.replace('[', '?').replace(']', '?')
@@ -632,7 +637,7 @@ class TransferChain(ChainBase):
elif str(path.parent) == str(path.root):
# 根目录,不删除
logger.warn(f"根目录 {path} 不能删除!")
return
return False, f"根目录 {path} 不能删除!"
else:
# 非根目录,才删除目录
shutil.rmtree(path)
@@ -658,5 +663,10 @@ class TransferChain(ChainBase):
# 父目录非根目录,才删除父目录
if not SystemUtils.exits_files(parent_path, settings.RMT_MEDIAEXT):
# 当前路径下没有媒体文件则删除
shutil.rmtree(parent_path)
try:
shutil.rmtree(parent_path)
except Exception as e:
logger.error(f"删除目录 {parent_path} 失败:{str(e)}")
return False, f"删除目录 {parent_path} 失败:{str(e)}"
logger.warn(f"目录 {parent_path} 已删除")
return True, ""

View File

@@ -4,10 +4,11 @@ from typing import Any
from app.chain import ChainBase
from app.schemas import Notification
from app.schemas.types import EventType, MediaImageType, MediaType, NotificationType
from app.utils.singleton import Singleton
from app.utils.web import WebUtils
class WebhookChain(ChainBase):
class WebhookChain(ChainBase, metaclass=Singleton):
"""
Webhook处理链
"""

View File

@@ -1,5 +1,7 @@
import importlib
import threading
import traceback
from threading import Thread, Event
from threading import Thread
from typing import Any, Union, Dict
from app.chain import ChainBase
@@ -11,14 +13,13 @@ from app.chain.transfer import TransferChain
from app.core.event import Event as ManagerEvent
from app.core.event import eventmanager, EventManager
from app.core.plugin import PluginManager
from app.db import SessionFactory
from app.helper.thread import ThreadHelper
from app.log import logger
from app.scheduler import Scheduler
from app.schemas import Notification
from app.schemas.types import EventType, MessageChannel
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton
from app.utils.system import SystemUtils
class CommandChian(ChainBase):
@@ -38,19 +39,19 @@ class Command(metaclass=Singleton):
_commands = {}
# 退出事件
_event = Event()
_event = threading.Event()
def __init__(self):
# 数据库连接
self._db = SessionFactory()
# 事件管理器
self.eventmanager = EventManager()
# 插件管理器
self.pluginmanager = PluginManager()
# 处理链
self.chain = CommandChian(self._db)
self.chain = CommandChian()
# 定时服务管理
self.scheduler = Scheduler()
# 线程管理器
self.threader = ThreadHelper()
# 内置命令
self._commands = {
"/cookiecloud": {
@@ -60,23 +61,23 @@ class Command(metaclass=Singleton):
"category": "站点"
},
"/sites": {
"func": SiteChain(self._db).remote_list,
"func": SiteChain().remote_list,
"description": "查询站点",
"category": "站点",
"data": {}
},
"/site_cookie": {
"func": SiteChain(self._db).remote_cookie,
"func": SiteChain().remote_cookie,
"description": "更新站点Cookie",
"data": {}
},
"/site_enable": {
"func": SiteChain(self._db).remote_enable,
"func": SiteChain().remote_enable,
"description": "启用站点",
"data": {}
},
"/site_disable": {
"func": SiteChain(self._db).remote_disable,
"func": SiteChain().remote_disable,
"description": "禁用站点",
"data": {}
},
@@ -87,7 +88,7 @@ class Command(metaclass=Singleton):
"category": "管理"
},
"/subscribes": {
"func": SubscribeChain(self._db).remote_list,
"func": SubscribeChain().remote_list,
"description": "查询订阅",
"category": "订阅",
"data": {}
@@ -105,7 +106,7 @@ class Command(metaclass=Singleton):
"category": "订阅"
},
"/subscribe_delete": {
"func": SubscribeChain(self._db).remote_delete,
"func": SubscribeChain().remote_delete,
"description": "删除订阅",
"data": {}
},
@@ -115,7 +116,7 @@ class Command(metaclass=Singleton):
"description": "订阅元数据更新"
},
"/downloading": {
"func": DownloadChain(self._db).remote_downloading,
"func": DownloadChain().remote_downloading,
"description": "正在下载",
"category": "管理",
"data": {}
@@ -127,21 +128,27 @@ class Command(metaclass=Singleton):
"category": "管理"
},
"/redo": {
"func": TransferChain(self._db).remote_transfer,
"func": TransferChain().remote_transfer,
"description": "手动整理",
"data": {}
},
"/clear_cache": {
"func": SystemChain(self._db).remote_clear_cache,
"func": SystemChain().remote_clear_cache,
"description": "清理缓存",
"category": "管理",
"data": {}
},
"/restart": {
"func": SystemUtils.restart,
"func": SystemChain().restart,
"description": "重启系统",
"category": "管理",
"data": {}
},
"/version": {
"func": SystemChain().version,
"description": "当前版本",
"category": "管理",
"data": {}
}
}
# 汇总插件命令
@@ -163,6 +170,8 @@ class Command(metaclass=Singleton):
self._thread = Thread(target=self.__run)
# 启动事件处理线程
self._thread.start()
# 重启msg
SystemChain().restart_finish()
def __run(self):
"""
@@ -175,10 +184,31 @@ class Command(metaclass=Singleton):
for handler in handlers:
try:
names = handler.__qualname__.split(".")
if names[0] == "Command":
self.command_event(event)
[class_name, method_name] = names
if class_name in self.pluginmanager.get_plugin_ids():
# 插件事件
self.threader.submit(
self.pluginmanager.run_plugin_method,
class_name, method_name, event
)
else:
self.pluginmanager.run_plugin_method(names[0], names[1], event)
# 检查全局变量中是否存在
if class_name not in globals():
# 导入模块除了插件和Command本身只有chain能响应事件
module = importlib.import_module(
f"app.chain.{class_name[:-5].lower()}"
)
class_obj = getattr(module, class_name)()
else:
# 通过类名创建类实例
class_obj = globals()[class_name]()
# 检查类是否存在并调用方法
if hasattr(class_obj, method_name):
self.threader.submit(
getattr(class_obj, method_name),
event
)
except Exception as e:
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
@@ -238,8 +268,6 @@ class Command(metaclass=Singleton):
"""
self._event.set()
self._thread.join()
if self._db:
self._db.close()
def get_commands(self):
"""

View File

@@ -1,4 +1,3 @@
import os
import secrets
import sys
from pathlib import Path
@@ -26,6 +25,8 @@ class Settings(BaseSettings):
HOST: str = "0.0.0.0"
# API监听端口
PORT: int = 3001
# 前端监听端口
NGINX_PORT: int = 3000
# 是否调试模式
DEBUG: bool = False
# 是否开发模式
@@ -38,6 +39,8 @@ class Settings(BaseSettings):
SUPERUSER_PASSWORD: str = "password"
# API密钥需要更换
API_TOKEN: str = "moviepilot"
# 登录页面电影海报,tmdb/bing
WALLPAPER: str = "tmdb"
# 网络代理 IP:PORT
PROXY_HOST: str = None
# 媒体信息搜索来源
@@ -126,6 +129,10 @@ class Settings(BaseSettings):
QB_PASSWORD: str = None
# Qbittorrent分类自动管理
QB_CATEGORY: bool = False
# Qbittorrent按顺序下载
QB_SEQUENTIAL: bool = True
# Qbittorrent忽略队列限制强制继续
QB_FORCE_RESUME: bool = False
# Transmission地址IP:PORT
TR_HOST: str = None
# Transmission用户名
@@ -135,7 +142,7 @@ class Settings(BaseSettings):
# 种子标签
TORRENT_TAG: str = "MOVIEPILOT"
# 下载保存目录,容器内映射路径需要一致
DOWNLOAD_PATH: str = "/downloads"
DOWNLOAD_PATH: str = None
# 电影下载保存目录,容器内映射路径需要一致
DOWNLOAD_MOVIE_PATH: str = None
# 电视剧下载保存目录,容器内映射路径需要一致
@@ -201,6 +208,8 @@ class Settings(BaseSettings):
"/Season {{season}}" \
"/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}" \
"{{fileExt}}"
# 转移时覆盖模式
OVERWRITE_MODE: str = "size"
# 大内存模式
BIG_MEMORY_MODE: bool = False
@@ -274,7 +283,43 @@ class Settings(BaseSettings):
def LIBRARY_PATHS(self) -> List[Path]:
if self.LIBRARY_PATH:
return [Path(path) for path in self.LIBRARY_PATH.split(",")]
return []
return [self.CONFIG_PATH / "library"]
@property
def SAVE_PATH(self) -> Path:
"""
获取下载保存目录
"""
if self.DOWNLOAD_PATH:
return Path(self.DOWNLOAD_PATH)
return self.CONFIG_PATH / "downloads"
@property
def SAVE_MOVIE_PATH(self) -> Path:
"""
获取电影下载保存目录
"""
if self.DOWNLOAD_MOVIE_PATH:
return Path(self.DOWNLOAD_MOVIE_PATH)
return self.SAVE_PATH
@property
def SAVE_TV_PATH(self) -> Path:
"""
获取电视剧下载保存目录
"""
if self.DOWNLOAD_TV_PATH:
return Path(self.DOWNLOAD_TV_PATH)
return self.SAVE_PATH
@property
def SAVE_ANIME_PATH(self) -> Path:
"""
获取动漫下载保存目录
"""
if self.DOWNLOAD_ANIME_PATH:
return Path(self.DOWNLOAD_ANIME_PATH)
return self.SAVE_TV_PATH
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -290,6 +335,12 @@ class Settings(BaseSettings):
with self.LOG_PATH as p:
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
with self.SAVE_PATH as p:
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
for path in self.LIBRARY_PATHS:
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
class Config:
case_sensitive = True

View File

@@ -10,16 +10,13 @@ class EventManager(metaclass=Singleton):
事件管理器
"""
# 事件队列
_eventQueue: Queue = None
# 事件响应函数字典
_handlers: dict = {}
def __init__(self):
# 事件队列
self._eventQueue = Queue()
# 事件响应函数字典
self._handlers = {}
# 已禁用的事件响应
self._disabled_handlers = []
def get_event(self):
"""
@@ -27,11 +24,21 @@ class EventManager(metaclass=Singleton):
"""
try:
event = self._eventQueue.get(block=True, timeout=1)
handlerList = self._handlers.get(event.event_type)
return event, handlerList or []
handlerList = self._handlers.get(event.event_type) or []
if handlerList:
# 去除掉被禁用的事件响应
handlerList = [handler for handler in handlerList
if handler.__qualname__.split(".")[0] not in self._disabled_handlers]
return event, handlerList
except Empty:
return None, []
def check(self, etype: EventType):
"""
检查事件是否存在响应
"""
return etype.value in self._handlers
def add_event_listener(self, etype: EventType, handler: type):
"""
注册事件处理
@@ -45,18 +52,21 @@ class EventManager(metaclass=Singleton):
handlerList.append(handler)
logger.debug(f"Event Registed{etype.value} - {handler}")
def remove_event_listener(self, etype: EventType, handler: type):
def disable_events_hander(self, class_name: str):
"""
移除监听器的处理函数
标记对应类事件处理为不可用
"""
try:
handlerList = self._handlers[etype.value]
if handler in handlerList[:]:
handlerList.remove(handler)
if not handlerList:
del self._handlers[etype.value]
except KeyError:
pass
if class_name not in self._disabled_handlers:
self._disabled_handlers.append(class_name)
logger.debug(f"Event Disabled{class_name}")
def enable_events_hander(self, class_name: str):
"""
标记对应类事件处理为可用
"""
if class_name in self._disabled_handlers:
self._disabled_handlers.remove(class_name)
logger.debug(f"Event Enabled{class_name}")
def send_event(self, etype: EventType, data: dict = None):
"""

View File

@@ -87,6 +87,17 @@ class MetaBase(object):
return self.cn_name
return ""
@name.setter
def name(self, name: str):
"""
设置名称
"""
if StringUtils.is_all_chinese(name):
self.cn_name = name
else:
self.en_name = name
self.cn_name = None
def init_subtitle(self, title_text: str):
"""
副标题识别

View File

@@ -4,7 +4,6 @@ import cn2an
import regex as re
from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.schemas.types import SystemConfigKey
from app.utils.singleton import Singleton
@@ -61,8 +60,7 @@ class WordsMatcher(metaclass=Singleton):
if state:
appley_words.append(word)
else:
logger.debug(f"自定义识别词替换失败:{message}")
except Exception as err:
print(str(err))

View File

@@ -1,10 +1,12 @@
from pathlib import Path
from typing import Tuple
import regex as re
from app.core.config import settings
from app.core.meta import MetaAnime, MetaVideo, MetaBase
from app.core.meta.words import WordsMatcher
from app.schemas.types import MediaType
def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
@@ -18,6 +20,8 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
org_title = title
# 预处理标题
title, apply_words = WordsMatcher().prepare(title)
# 获取标题中媒体信息
title, metainfo = find_metainfo(title)
# 判断是否处理文件
if title and Path(title).suffix.lower() in settings.RMT_MEDIAEXT:
isfile = True
@@ -29,7 +33,23 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
meta.title = org_title
# 记录使用的识别词
meta.apply_words = apply_words or []
# 修正媒体信息
if metainfo.get('tmdbid'):
meta.tmdbid = metainfo['tmdbid']
if metainfo.get('type'):
meta.type = metainfo['type']
if metainfo.get('begin_season'):
meta.begin_season = metainfo['begin_season']
if metainfo.get('end_season'):
meta.end_season = metainfo['end_season']
if metainfo.get('total_season'):
meta.total_season = metainfo['total_season']
if metainfo.get('begin_episode'):
meta.begin_episode = metainfo['begin_episode']
if metainfo.get('end_episode'):
meta.end_episode = metainfo['end_episode']
if metainfo.get('total_episode'):
meta.total_episode = metainfo['total_episode']
return meta
@@ -65,3 +85,71 @@ def is_anime(name: str) -> bool:
if re.search(r'\[[+0-9XVPI-]+]\s*\[', name, re.IGNORECASE):
return True
return False
def find_metainfo(title: str) -> Tuple[str, dict]:
"""
从标题中提取媒体信息
"""
metainfo = {
'tmdbid': None,
'type': None,
'begin_season': None,
'end_season': None,
'total_season': None,
'begin_episode': None,
'end_episode': None,
'total_episode': None,
}
if not title:
return title, metainfo
# 从标题中提取媒体信息 格式为{[tmdbid=xxx;type=xxx;s=xxx;e=xxx]}
results = re.findall(r'(?<={\[)[\W\w]+(?=]})', title)
if not results:
return title, metainfo
for result in results:
tmdbid = re.findall(r'(?<=tmdbid=)\d+', result)
# 查找tmdbid信息
if tmdbid and tmdbid[0].isdigit():
metainfo['tmdbid'] = tmdbid[0]
# 查找媒体类型
mtype = re.findall(r'(?<=type=)\d+', result)
if mtype:
match mtype[0]:
case "movie":
metainfo['type'] = MediaType.MOVIE
case "tv":
metainfo['type'] = MediaType.TV
case _:
pass
# 查找季信息
begin_season = re.findall(r'(?<=s=)\d+', result)
if begin_season and begin_season[0].isdigit():
metainfo['begin_season'] = int(begin_season[0])
end_season = re.findall(r'(?<=s=\d+-)\d+', result)
if end_season and end_season[0].isdigit():
metainfo['end_season'] = int(end_season[0])
# 查找集信息
begin_episode = re.findall(r'(?<=e=)\d+', result)
if begin_episode and begin_episode[0].isdigit():
metainfo['begin_episode'] = int(begin_episode[0])
end_episode = re.findall(r'(?<=e=\d+-)\d+', result)
if end_episode and end_episode[0].isdigit():
metainfo['end_episode'] = int(end_episode[0])
# 去除title中该部分
if tmdbid or mtype or begin_season or end_season or begin_episode or end_episode:
title = title.replace(f"{{[{result}]}}", '')
# 计算季集总数
if metainfo.get('begin_season') and metainfo.get('end_season'):
if metainfo['begin_season'] > metainfo['end_season']:
metainfo['begin_season'], metainfo['end_season'] = metainfo['end_season'], metainfo['begin_season']
metainfo['total_season'] = metainfo['end_season'] - metainfo['begin_season'] + 1
elif metainfo.get('begin_season') and not metainfo.get('end_season'):
metainfo['total_season'] = 1
if metainfo.get('begin_episode') and metainfo.get('end_episode'):
if metainfo['begin_episode'] > metainfo['end_episode']:
metainfo['begin_episode'], metainfo['end_episode'] = metainfo['end_episode'], metainfo['begin_episode']
metainfo['total_episode'] = metainfo['end_episode'] - metainfo['begin_episode'] + 1
elif metainfo.get('begin_episode') and not metainfo.get('end_episode'):
metainfo['total_episode'] = 1
return title, metainfo

View File

@@ -1,6 +1,7 @@
import traceback
from typing import List, Any, Dict, Tuple
from app.core.event import eventmanager
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.module import ModuleHelper
from app.helper.sites import SitesHelper
@@ -58,6 +59,8 @@ class PluginManager(metaclass=Singleton):
self._plugins[plugin_id] = plugin
# 未安装的不加载
if plugin_id not in installed_plugins:
# 设置事件状态为不可用
eventmanager.disable_events_hander(plugin_id)
continue
# 生成实例
plugin_obj = plugin()
@@ -66,8 +69,10 @@ class PluginManager(metaclass=Singleton):
# 存储运行实例
self._running_plugins[plugin_id] = plugin_obj
logger.info(f"Plugin Loaded{plugin_id}")
# 设置事件注册状态可用
eventmanager.enable_events_hander(plugin_id)
except Exception as err:
logger.error(f"加载插件 {plugin_id} 出错:{err} - {traceback.format_exc()}")
logger.error(f"加载插件 {plugin_id} 出错:{str(err)} - {traceback.format_exc()}")
def reload_plugin(self, plugin_id: str, conf: dict):
"""
@@ -177,6 +182,12 @@ class PluginManager(metaclass=Singleton):
return None
return getattr(self._running_plugins[pid], method)(*args, **kwargs)
def get_plugin_ids(self) -> List[str]:
"""
获取所有插件ID
"""
return list(self._plugins.keys())
def get_plugin_apps(self) -> List[dict]:
"""
获取所有插件信息

View File

@@ -1,3 +1,5 @@
from typing import Tuple, Optional, Generator
from sqlalchemy import create_engine, QueuePool
from sqlalchemy.orm import sessionmaker, Session, scoped_session
@@ -9,20 +11,20 @@ Engine = create_engine(f"sqlite:///{settings.CONFIG_PATH}/user.db",
echo=False,
poolclass=QueuePool,
pool_size=1024,
pool_recycle=600,
pool_recycle=3600,
pool_timeout=180,
max_overflow=0,
max_overflow=10,
connect_args={"timeout": 60})
# 会话工厂
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=Engine)
SessionFactory = sessionmaker(bind=Engine)
# 多线程全局使用的数据库会话
ScopedSession = scoped_session(SessionFactory)
def get_db():
def get_db() -> Generator:
"""
获取数据库会话
获取数据库会话用于WEB请求
:return: Session
"""
db = None
@@ -34,11 +36,110 @@ def get_db():
db.close()
def get_args_db(args: tuple, kwargs: dict) -> Optional[Session]:
"""
从参数中获取数据库Session对象
"""
db = None
if args:
for arg in args:
if isinstance(arg, Session):
db = arg
break
if kwargs:
for key, value in kwargs.items():
if isinstance(value, Session):
db = value
break
return db
def update_args_db(args: tuple, kwargs: dict, db: Session) -> Tuple[tuple, dict]:
"""
更新参数中的数据库Session对象关键字传参时更新db的值否则更新第1或第2个参数
"""
if kwargs and 'db' in kwargs:
kwargs['db'] = db
elif args:
if args[0] is None:
args = (db, *args[1:])
else:
args = (args[0], db, *args[2:])
return args, kwargs
def db_update(func):
"""
数据库更新类操作装饰器第一个参数必须是数据库会话或存在db参数
"""
def wrapper(*args, **kwargs):
# 是否关闭数据库会话
_close_db = False
# 从参数中获取数据库会话
db = get_args_db(args, kwargs)
if not db:
# 如果没有获取到数据库会话,创建一个
db = ScopedSession()
# 标记需要关闭数据库会话
_close_db = True
# 更新参数中的数据库会话
args, kwargs = update_args_db(args, kwargs, db)
try:
# 执行函数
result = func(*args, **kwargs)
# 提交事务
db.commit()
except Exception as err:
# 回滚事务
db.rollback()
raise err
finally:
# 关闭数据库会话
if _close_db:
db.close()
return result
return wrapper
def db_query(func):
"""
数据库查询操作装饰器第一个参数必须是数据库会话或存在db参数
注意db.query列表数据时需要转换为list返回
"""
def wrapper(*args, **kwargs):
# 是否关闭数据库会话
_close_db = False
# 从参数中获取数据库会话
db = get_args_db(args, kwargs)
if not db:
# 如果没有获取到数据库会话,创建一个
db = ScopedSession()
# 标记需要关闭数据库会话
_close_db = True
# 更新参数中的数据库会话
args, kwargs = update_args_db(args, kwargs, db)
try:
# 执行函数
result = func(*args, **kwargs)
except Exception as err:
raise err
finally:
# 关闭数据库会话
if _close_db:
db.close()
return result
return wrapper
class DbOper:
"""
数据库操作基类
"""
_db: Session = None
def __init__(self, db: Session = None):
if db:
self._db = db
else:
self._db = ScopedSession()
self._db = db

View File

@@ -24,12 +24,11 @@ class DownloadHistoryOper(DbOper):
"""
return DownloadHistory.get_by_hash(self._db, download_hash)
def add(self, **kwargs) -> DownloadHistory:
def add(self, **kwargs):
"""
新增下载历史
"""
downloadhistory = DownloadHistory(**kwargs)
return downloadhistory.create(self._db)
DownloadHistory(**kwargs).create(self._db)
def add_files(self, file_items: List[dict]):
"""
@@ -109,10 +108,10 @@ class DownloadHistoryOper(DbOper):
episode=episode,
tmdbid=tmdbid)
def list_by_user_date(self, date: str, userid: str = None) -> List[DownloadHistory]:
def list_by_user_date(self, date: str, username: str = None) -> List[DownloadHistory]:
"""
查询某用户某时间之的下载历史
查询某用户某时间之的下载历史
"""
return DownloadHistory.list_by_user_date(db=self._db,
date=date,
userid=userid)
username=username)

View File

@@ -22,16 +22,15 @@ def init_db():
# 全量建表
Base.metadata.create_all(bind=Engine)
# 初始化超级管理员
db = SessionFactory()
user = User.get_by_name(db=db, name=settings.SUPERUSER)
if not user:
user = User(
name=settings.SUPERUSER,
hashed_password=get_password_hash(settings.SUPERUSER_PASSWORD),
is_superuser=True,
)
user.create(db)
db.close()
with SessionFactory() as db:
user = User.get_by_name(db=db, name=settings.SUPERUSER)
if not user:
user = User(
name=settings.SUPERUSER,
hashed_password=get_password_hash(settings.SUPERUSER_PASSWORD),
is_superuser=True,
)
user.create(db)
def update_db():
@@ -46,4 +45,4 @@ def update_db():
alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}")
upgrade(alembic_cfg, 'head')
except Exception as e:
logger.error(f'数据库更新失败:{e}')
logger.error(f'数据库更新失败:{str(e)}')

View File

@@ -1,49 +1,48 @@
from typing import Any, Self, List
from sqlalchemy import inspect
from sqlalchemy.orm import as_declarative, declared_attr, Session
from app.db import db_update, db_query
@as_declarative()
class Base:
id: Any
__name__: str
@staticmethod
def commit(db: Session):
try:
db.commit()
except Exception as err:
db.rollback()
raise err
def create(self, db: Session) -> Self:
@db_update
def create(self, db: Session):
db.add(self)
self.commit(db)
return self
@classmethod
@db_query
def get(cls, db: Session, rid: int) -> Self:
return db.query(cls).filter(cls.id == rid).first()
@db_update
def update(self, db: Session, payload: dict):
payload = {k: v for k, v in payload.items() if v is not None}
for key, value in payload.items():
setattr(self, key, value)
Base.commit(db)
if inspect(self).detached:
db.add(self)
@classmethod
@db_update
def delete(cls, db: Session, rid):
db.query(cls).filter(cls.id == rid).delete()
Base.commit(db)
@classmethod
@db_update
def truncate(cls, db: Session):
db.query(cls).delete()
Base.commit(db)
@classmethod
@db_query
def list(cls, db: Session) -> List[Self]:
return db.query(cls).all()
result = db.query(cls).all()
return list(result)
def to_dict(self):
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns}

View File

@@ -1,7 +1,8 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db.models import Base
from app.db import db_query
from app.db.models import Base, db_update
class DownloadHistory(Base):
@@ -37,6 +38,8 @@ class DownloadHistory(Base):
torrent_site = Column(String)
# 下载用户
userid = Column(String)
# 下载用户名/插件名
username = Column(String)
# 下载渠道
channel = Column(String)
# 创建时间
@@ -45,69 +48,80 @@ class DownloadHistory(Base):
note = Column(String)
@staticmethod
@db_query
def get_by_hash(db: Session, download_hash: str):
return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).first()
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30):
return db.query(DownloadHistory).offset((page - 1) * count).limit(count).all()
result = db.query(DownloadHistory).offset((page - 1) * count).limit(count).all()
return list(result)
@staticmethod
@db_query
def get_by_path(db: Session, path: str):
return db.query(DownloadHistory).filter(DownloadHistory.path == path).first()
@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):
"""
据tmdbid、season、season_episode查询转移记录
"""
result = None
if tmdbid and not season and not episode:
return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).order_by(
DownloadHistory.id.desc()).all()
if tmdbid and season and not episode:
return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season).order_by(
DownloadHistory.id.desc()).all()
if tmdbid and season and episode:
return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
DownloadHistory.id.desc()).all()
# 电视剧所有季集|电影
if not season and not episode:
return db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year).order_by(
DownloadHistory.id.desc()).all()
# 电视剧某季
if season and not episode:
return db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season).order_by(
DownloadHistory.id.desc()).all()
# 电视剧某季某集
if season and episode:
return db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.type == mtype,
DownloadHistory.title == title,
DownloadHistory.year == year,
DownloadHistory.seasons == season,
DownloadHistory.episodes == episode).order_by(
DownloadHistory.id.desc()).all()
if result:
return list(result)
@staticmethod
def list_by_user_date(db: Session, date: str, userid: str = None):
@db_query
def list_by_user_date(db: Session, date: str, username: str = None):
"""
查询某用户某时间之后的下载历史
"""
if userid:
return db.query(DownloadHistory).filter(DownloadHistory.date < date,
DownloadHistory.userid == userid).order_by(
if username:
result = db.query(DownloadHistory).filter(DownloadHistory.date < date,
DownloadHistory.username == username).order_by(
DownloadHistory.id.desc()).all()
else:
return db.query(DownloadHistory).filter(DownloadHistory.date < date).order_by(
result = db.query(DownloadHistory).filter(DownloadHistory.date < date).order_by(
DownloadHistory.id.desc()).all()
return list(result)
class DownloadFiles(Base):
@@ -131,23 +145,29 @@ class DownloadFiles(Base):
state = Column(Integer, nullable=False, default=1)
@staticmethod
@db_query
def get_by_hash(db: Session, download_hash: str, state: int = None):
if state:
return db.query(DownloadFiles).filter(DownloadFiles.download_hash == download_hash,
DownloadFiles.state == state).all()
result = db.query(DownloadFiles).filter(DownloadFiles.download_hash == download_hash,
DownloadFiles.state == state).all()
else:
return db.query(DownloadFiles).filter(DownloadFiles.download_hash == download_hash).all()
result = db.query(DownloadFiles).filter(DownloadFiles.download_hash == download_hash).all()
return list(result)
@staticmethod
@db_query
def get_by_fullpath(db: Session, fullpath: str):
return db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath).order_by(
DownloadFiles.id.desc()).first()
@staticmethod
@db_query
def get_by_savepath(db: Session, savepath: str):
return db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all()
result = db.query(DownloadFiles).filter(DownloadFiles.savepath == savepath).all()
return list(result)
@staticmethod
@db_update
def delete_by_fullpath(db: Session, fullpath: str):
db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath,
DownloadFiles.state == 1).update(
@@ -155,4 +175,3 @@ class DownloadFiles(Base):
"state": 0
}
)
Base.commit(db)

View File

@@ -3,7 +3,8 @@ from datetime import datetime
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db.models import Base
from app.db import db_query
from app.db.models import Base, db_update
class MediaServerItem(Base):
@@ -41,20 +42,23 @@ class MediaServerItem(Base):
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
@staticmethod
@db_query
def get_by_itemid(db: Session, item_id: str):
return db.query(MediaServerItem).filter(MediaServerItem.item_id == item_id).first()
@staticmethod
@db_update
def empty(db: Session, server: str):
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
Base.commit(db)
@staticmethod
@db_query
def exist_by_tmdbid(db: Session, tmdbid: int, mtype: str):
return db.query(MediaServerItem).filter(MediaServerItem.tmdbid == tmdbid,
MediaServerItem.item_type == mtype).first()
@staticmethod
@db_query
def exists_by_title(db: Session, title: str, mtype: str, year: str):
return db.query(MediaServerItem).filter(MediaServerItem.title == title,
MediaServerItem.item_type == mtype,

View File

@@ -1,7 +1,8 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db.models import Base
from app.db import db_query
from app.db.models import Base, db_update
class PluginData(Base):
@@ -14,18 +15,23 @@ class PluginData(Base):
value = Column(String)
@staticmethod
@db_query
def get_plugin_data(db: Session, plugin_id: str):
return db.query(PluginData).filter(PluginData.plugin_id == plugin_id).all()
result = db.query(PluginData).filter(PluginData.plugin_id == plugin_id).all()
return list(result)
@staticmethod
@db_query
def get_plugin_data_by_key(db: Session, plugin_id: str, key: str):
return db.query(PluginData).filter(PluginData.plugin_id == plugin_id, PluginData.key == key).first()
@staticmethod
@db_update
def del_plugin_data_by_key(db: Session, plugin_id: str, key: str):
db.query(PluginData).filter(PluginData.plugin_id == plugin_id, PluginData.key == key).delete()
Base.commit(db)
@staticmethod
@db_query
def get_plugin_data_by_plugin_id(db: Session, plugin_id: str):
return db.query(PluginData).filter(PluginData.plugin_id == plugin_id).all()
result = db.query(PluginData).filter(PluginData.plugin_id == plugin_id).all()
return list(result)

View File

@@ -3,7 +3,8 @@ from datetime import datetime
from sqlalchemy import Boolean, Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db.models import Base
from app.db import db_query
from app.db.models import Base, db_update
class Site(Base):
@@ -47,18 +48,23 @@ class Site(Base):
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
@staticmethod
@db_query
def get_by_domain(db: Session, domain: str):
return db.query(Site).filter(Site.domain == domain).first()
@staticmethod
@db_query
def get_actives(db: Session):
return db.query(Site).filter(Site.is_active == 1).all()
result = db.query(Site).filter(Site.is_active == 1).all()
return list(result)
@staticmethod
@db_query
def list_order_by_pri(db: Session):
return db.query(Site).order_by(Site.pri).all()
result = db.query(Site).order_by(Site.pri).all()
return list(result)
@staticmethod
@db_update
def reset(db: Session):
db.query(Site).delete()
Base.commit(db)

View File

@@ -1,6 +1,7 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_query
from app.db.models import Base
@@ -19,5 +20,6 @@ class SiteIcon(Base):
base64 = Column(String)
@staticmethod
@db_query
def get_by_domain(db: Session, domain: str):
return db.query(SiteIcon).filter(SiteIcon.domain == domain).first()

View File

@@ -1,6 +1,7 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_update, db_query
from app.db.models import Base
@@ -37,6 +38,12 @@ class Subscribe(Base):
include = Column(String)
# 排除
exclude = Column(String)
# 质量
quality = Column(String)
# 分辨率
resolution = Column(String)
# 特效
effect = Column(String)
# 总集数
total_episode = Column(Integer)
# 开始集数
@@ -61,6 +68,7 @@ class Subscribe(Base):
current_priority = Column(Integer)
@staticmethod
@db_query
def exists(db: Session, tmdbid: int, season: int = None):
if season:
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
@@ -68,30 +76,39 @@ class Subscribe(Base):
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).first()
@staticmethod
@db_query
def get_by_state(db: Session, state: str):
return db.query(Subscribe).filter(Subscribe.state == state).all()
result = db.query(Subscribe).filter(Subscribe.state == state).all()
return list(result)
@staticmethod
@db_query
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
if season:
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
Subscribe.season == season).all()
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).all()
result = db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
Subscribe.season == season).all()
else:
result = db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).all()
return list(result)
@staticmethod
@db_query
def get_by_title(db: Session, title: str):
return db.query(Subscribe).filter(Subscribe.name == title).first()
@staticmethod
@db_query
def get_by_doubanid(db: Session, doubanid: str):
return db.query(Subscribe).filter(Subscribe.doubanid == doubanid).first()
@db_update
def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int):
subscrbies = self.get_by_tmdbid(db, tmdbid, season)
for subscrbie in subscrbies:
subscrbie.delete(db, subscrbie.id)
return True
@db_update
def delete_by_doubanid(self, db: Session, doubanid: str):
subscribe = self.get_by_doubanid(db, doubanid)
if subscribe:

View File

@@ -1,6 +1,7 @@
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_update, db_query
from app.db.models import Base
@@ -15,9 +16,11 @@ class SystemConfig(Base):
value = Column(String, nullable=True)
@staticmethod
@db_query
def get_by_key(db: Session, key: str):
return db.query(SystemConfig).filter(SystemConfig.key == key).first()
@db_update
def delete_by_key(self, db: Session, key: str):
systemconfig = self.get_by_key(db, key)
if systemconfig:

View File

@@ -3,7 +3,8 @@ import time
from sqlalchemy import Column, Integer, String, Sequence, Boolean, func
from sqlalchemy.orm import Session
from app.db.models import Base
from app.db import db_query
from app.db.models import Base, db_update
class TransferHistory(Base):
@@ -47,29 +48,38 @@ class TransferHistory(Base):
files = Column(String)
@staticmethod
@db_query
def list_by_title(db: Session, title: str, page: int = 1, count: int = 30):
return db.query(TransferHistory).filter(TransferHistory.title.like(f'%{title}%')).order_by(
result = db.query(TransferHistory).filter(TransferHistory.title.like(f'%{title}%')).order_by(
TransferHistory.date.desc()).offset((page - 1) * count).limit(
count).all()
return list(result)
@staticmethod
@db_query
def list_by_page(db: Session, page: int = 1, count: int = 30):
return db.query(TransferHistory).order_by(TransferHistory.date.desc()).offset((page - 1) * count).limit(
result = db.query(TransferHistory).order_by(TransferHistory.date.desc()).offset((page - 1) * count).limit(
count).all()
return list(result)
@staticmethod
@db_query
def get_by_hash(db: Session, download_hash: str):
return db.query(TransferHistory).filter(TransferHistory.download_hash == download_hash).first()
@staticmethod
@db_query
def get_by_src(db: Session, src: str):
return db.query(TransferHistory).filter(TransferHistory.src == src).first()
@staticmethod
@db_query
def list_by_hash(db: Session, download_hash: str):
return db.query(TransferHistory).filter(TransferHistory.download_hash == download_hash).all()
result = db.query(TransferHistory).filter(TransferHistory.download_hash == download_hash).all()
return list(result)
@staticmethod
@db_query
def statistic(db: Session, days: int = 7):
"""
统计最近days天的下载历史数量按日期分组返回每日数量
@@ -78,74 +88,82 @@ class TransferHistory(Base):
TransferHistory.id.label('id')).filter(
TransferHistory.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * days))).subquery()
return db.query(sub_query.c.date, func.count(sub_query.c.id)).group_by(sub_query.c.date).all()
result = db.query(sub_query.c.date, func.count(sub_query.c.id)).group_by(sub_query.c.date).all()
return list(result)
@staticmethod
@db_query
def count(db: Session):
return db.query(func.count(TransferHistory.id)).first()[0]
@staticmethod
@db_query
def count_by_title(db: Session, title: str):
return db.query(func.count(TransferHistory.id)).filter(TransferHistory.title.like(f'%{title}%')).first()[0]
@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):
"""
据tmdbid、season、season_episode查询转移记录
tmdbid + mtype 或 title + year 必输
"""
result = None
# TMDBID + 类型
if tmdbid and mtype:
# 电视剧某季某集
if season and episode:
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.seasons == season,
TransferHistory.episodes == episode,
TransferHistory.dest == dest).all()
result = db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.seasons == season,
TransferHistory.episodes == episode,
TransferHistory.dest == dest).all()
# 电视剧某季
elif season:
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.seasons == season).all()
result = db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.seasons == season).all()
else:
if dest:
# 电影
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.dest == dest).all()
result = db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype,
TransferHistory.dest == dest).all()
else:
# 电视剧所有季集
return db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype).all()
result = db.query(TransferHistory).filter(TransferHistory.tmdbid == tmdbid,
TransferHistory.type == mtype).all()
# 标题 + 年份
elif title and year:
# 电视剧某季某集
if season and episode:
return db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.seasons == season,
TransferHistory.episodes == episode,
TransferHistory.dest == dest).all()
result = db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.seasons == season,
TransferHistory.episodes == episode,
TransferHistory.dest == dest).all()
# 电视剧某季
elif season:
return db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.seasons == season).all()
result = db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.seasons == season).all()
else:
if dest:
# 电影
return db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.dest == dest).all()
result = db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year,
TransferHistory.dest == dest).all()
else:
# 电视剧所有季集
return db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year).all()
result = db.query(TransferHistory).filter(TransferHistory.title == title,
TransferHistory.year == year).all()
if result:
return list(result)
return []
@staticmethod
@db_query
def get_by_type_tmdbid(db: Session, mtype: str = None, tmdbid: int = None):
"""
据tmdbid、type查询转移记录
@@ -154,10 +172,10 @@ class TransferHistory(Base):
TransferHistory.type == mtype).first()
@staticmethod
@db_update
def update_download_hash(db: Session, historyid: int = None, download_hash: str = None):
db.query(TransferHistory).filter(TransferHistory.id == historyid).update(
{
"download_hash": download_hash
}
)
Base.commit(db)

View File

@@ -2,6 +2,7 @@ from sqlalchemy import Boolean, Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.core.security import verify_password
from app.db import db_update, db_query
from app.db.models import Base
@@ -25,6 +26,7 @@ class User(Base):
avatar = Column(String)
@staticmethod
@db_query
def authenticate(db: Session, name: str, password: str):
user = db.query(User).filter(User.name == name).first()
if not user:
@@ -34,9 +36,11 @@ class User(Base):
return user
@staticmethod
@db_query
def get_by_name(db: Session, name: str):
return db.query(User).filter(User.name == name).first()
@db_update
def delete_by_name(self, db: Session, name: str):
user = self.get_by_name(db, name)
if user:

View File

@@ -11,7 +11,7 @@ class PluginDataOper(DbOper):
插件数据管理
"""
def save(self, plugin_id: str, key: str, value: Any) -> PluginData:
def save(self, plugin_id: str, key: str, value: Any):
"""
保存插件数据
:param plugin_id: 插件id
@@ -25,10 +25,8 @@ class PluginDataOper(DbOper):
plugin.update(self._db, {
"value": value
})
return plugin
else:
plugin = PluginData(plugin_id=plugin_id, key=key, value=value)
return plugin.create(self._db)
PluginData(plugin_id=plugin_id, key=key, value=value).create(self._db)
def get_data(self, plugin_id: str, key: str) -> Any:
"""

View File

@@ -31,6 +31,12 @@ class SiteOper(DbOper):
"""
return Site.list(self._db)
def list_order_by_pri(self) -> List[Site]:
"""
获取站点列表
"""
return Site.list_order_by_pri(self._db)
def list_active(self) -> List[Site]:
"""
按状态获取站点列表

View File

@@ -30,6 +30,8 @@ class SubscribeOper(DbOper):
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
**kwargs)
subscribe.create(self._db)
# 查询订阅
subscribe = Subscribe.exists(self._db, tmdbid=mediainfo.tmdb_id, season=kwargs.get('season'))
return subscribe.id, "新增订阅成功"
else:
return subscribe.id, "订阅已存在"

View File

@@ -1,7 +1,7 @@
import json
from typing import Any, Union
from app.db import DbOper, SessionFactory
from app.db import DbOper
from app.db.models.systemconfig import SystemConfig
from app.schemas.types import SystemConfigKey
from app.utils.object import ObjectUtils
@@ -16,8 +16,7 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
"""
加载配置到内存
"""
self._db = SessionFactory()
super().__init__(self._db)
super().__init__()
for item in SystemConfig.list(self._db):
if ObjectUtils.is_obj(item.value):
self.__SYSTEMCONF[item.key] = json.loads(item.value)

View File

@@ -43,14 +43,14 @@ class TransferHistoryOper(DbOper):
"""
return TransferHistory.list_by_hash(self._db, download_hash)
def add(self, **kwargs) -> TransferHistory:
def add(self, **kwargs):
"""
新增转移历史
"""
kwargs.update({
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
})
return TransferHistory(**kwargs).create(self._db)
TransferHistory(**kwargs).create(self._db)
def statistic(self, days: int = 7) -> List[Any]:
"""
@@ -103,7 +103,8 @@ class TransferHistoryOper(DbOper):
kwargs.update({
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
})
return TransferHistory(**kwargs).create(self._db)
TransferHistory(**kwargs).create(self._db)
return TransferHistory.get_by_src(self._db, kwargs.get("src"))
def update_download_hash(self, historyid, download_hash):
"""
@@ -119,7 +120,7 @@ class TransferHistoryOper(DbOper):
"""
self.add_force(
src=str(src_path),
dest=str(transferinfo.target_path),
dest=str(transferinfo.target_path or ''),
mode=mode,
type=mediainfo.type.value,
category=mediainfo.category,
@@ -145,7 +146,7 @@ class TransferHistoryOper(DbOper):
if mediainfo and transferinfo:
his = self.add_force(
src=str(src_path),
dest=str(transferinfo.target_path),
dest=str(transferinfo.target_path or ''),
mode=mode,
type=mediainfo.type.value,
category=mediainfo.category,

View File

@@ -49,11 +49,11 @@ class PlaywrightHelper:
# 回调函数
return callback(page)
except Exception as e:
logger.error(f"网页操作失败: {e}")
logger.error(f"网页操作失败: {str(e)}")
finally:
browser.close()
except Exception as e:
logger.error(f"网页操作失败: {e}")
logger.error(f"网页操作失败: {str(e)}")
return None
def get_page_source(self, url: str,
@@ -85,12 +85,12 @@ class PlaywrightHelper:
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
source = page.content()
except Exception as e:
logger.error(f"获取网页源码失败: {e}")
logger.error(f"获取网页源码失败: {str(e)}")
source = None
finally:
browser.close()
except Exception as e:
logger.error(f"获取网页源码失败: {e}")
logger.error(f"获取网页源码失败: {str(e)}")
return source

View File

@@ -162,8 +162,8 @@ class CookieHelper:
page.click(submit_xpath)
page.wait_for_load_state("networkidle", timeout=30 * 1000)
except Exception as e:
logger.error(f"仿真登录失败:{e}")
return None, None, f"仿真登录失败:{e}"
logger.error(f"仿真登录失败:{str(e)}")
return None, None, f"仿真登录失败:{str(e)}"
# 登录后的源码
html_text = page.content()
if not html_text:

View File

@@ -15,7 +15,7 @@ class DisplayHelper(metaclass=Singleton):
self._display = Display(visible=False, size=(1024, 768))
self._display.start()
except Exception as err:
logger.error(f"DisplayHelper init error: {err}")
logger.error(f"DisplayHelper init error: {str(err)}")
def stop(self):
if self._display:

Binary file not shown.

31
app/helper/thread.py Normal file
View File

@@ -0,0 +1,31 @@
from concurrent.futures import ThreadPoolExecutor
from app.utils.singleton import Singleton
class ThreadHelper(metaclass=Singleton):
"""
线程池管理
"""
def __init__(self, max_workers=50):
self.pool = ThreadPoolExecutor(max_workers=max_workers)
def submit(self, func, *args, **kwargs):
"""
提交任务
:param func: 函数
:param args: 参数
:param kwargs: 参数
:return: future
"""
return self.pool.submit(func, *args, **kwargs)
def shutdown(self):
"""
关闭线程池
:return:
"""
self.pool.shutdown()
def __del__(self):
self.shutdown()

View File

@@ -95,7 +95,7 @@ class TorrentHelper:
logger.warn(f"触发了站点首次种子下载,且无法自动跳过:{url}")
break
except Exception as err:
logger.warn(f"触发了站点首次种子下载,尝试自动跳过时出现错误:{err},链接:{url}")
logger.warn(f"触发了站点首次种子下载,尝试自动跳过时出现错误:{str(err)},链接:{url}")
if not skip_flag:
return None, None, "", [], "种子数据有误请确认链接是否正确如为PT站点则需手工在站点下载一次种子"
# 种子内容
@@ -113,7 +113,7 @@ class TorrentHelper:
# 成功拿到种子数据
return file_path, req.content, folder_name, file_list, ""
except Exception as err:
logger.error(f"种子文件解析失败:{err}")
logger.error(f"种子文件解析失败:{str(err)}")
# 种子数据仍然错误
return None, None, "", [], "种子数据有误,请确认链接是否正确"
# 返回失败
@@ -157,10 +157,10 @@ class TorrentHelper:
file_list.append(str(file_path.relative_to(root_path)))
else:
file_list.append(fileinfo.name)
logger.info(f"解析种子:{torrent_path.name} => 目录:{folder_name},文件清单:{file_list}")
logger.debug(f"解析种子:{torrent_path.name} => 目录:{folder_name},文件清单:{file_list}")
return folder_name, file_list
except Exception as err:
logger.error(f"种子文件解析失败:{err}")
logger.error(f"种子文件解析失败:{str(err)}")
return "", []
@staticmethod

View File

@@ -1,28 +1,12 @@
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
import click
from app.core.config import settings
# logger
logger = logging.getLogger()
if settings.DEBUG:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
# 创建终端输出Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# 创建文件输出Handler
file_handler = RotatingFileHandler(filename=settings.LOG_PATH / 'moviepilot.log',
mode='w',
maxBytes=5 * 1024 * 1024,
backupCount=3,
encoding='utf-8')
file_handler.setLevel(logging.INFO)
# 日志级别颜色
level_name_colors = {
logging.DEBUG: lambda level_name: click.style(str(level_name), fg="cyan"),
logging.INFO: lambda level_name: click.style(str(level_name), fg="green"),
@@ -34,20 +18,40 @@ level_name_colors = {
}
# 定义日志输出格式
class CustomFormatter(logging.Formatter):
"""
定义日志输出格式
"""
def format(self, record):
seperator = " " * (8 - len(record.levelname))
record.leveltext = level_name_colors[record.levelno](record.levelname + ":") + seperator
if record.filename == "__init__.py":
record.filename = Path(record.pathname).parent.name
return super().format(record)
# DEBUG
logger = logging.getLogger()
if settings.DEBUG:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
# 终端日志
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
console_formatter = CustomFormatter("%(leveltext)s%(filename)s - %(message)s")
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# 文件日志
file_handler = RotatingFileHandler(filename=settings.LOG_PATH / 'moviepilot.log',
mode='w',
maxBytes=5 * 1024 * 1024,
backupCount=3,
encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_formater = CustomFormatter("%(levelname)s%(asctime)s - %(filename)s - %(message)s")
file_handler.setFormatter(file_formater)
logger.addHandler(file_handler)

View File

@@ -1,18 +1,30 @@
import multiprocessing
import os
import sys
import threading
import uvicorn as uvicorn
from PIL import Image
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from uvicorn import Config
from app.command import Command
from app.utils.system import SystemUtils
# 禁用输出
if SystemUtils.is_frozen():
sys.stdout = open(os.devnull, 'w')
sys.stderr = open(os.devnull, 'w')
from app.core.config import settings
from app.core.module import ModuleManager
from app.core.plugin import PluginManager
from app.db.init import init_db, update_db
from app.helper.thread import ThreadHelper
from app.helper.display import DisplayHelper
from app.helper.sites import SitesHelper
from app.scheduler import Scheduler
from app.command import Command
# App
App = FastAPI(title=settings.PROJECT_NAME,
@@ -44,6 +56,89 @@ def init_routers():
App.include_router(arr_router, prefix="/api/v3")
def start_frontend():
"""
启动前端服务
"""
if not SystemUtils.is_frozen():
return
# 临时Nginx目录
nginx_path = settings.ROOT_PATH / 'nginx'
if not nginx_path.exists():
return
# 配置目录下的Nginx目录
run_nginx_dir = settings.CONFIG_PATH.with_name('nginx')
if not run_nginx_dir.exists():
# 移动到配置目录
SystemUtils.move(nginx_path, run_nginx_dir)
# 启动Nginx
import subprocess
if SystemUtils.is_windows():
subprocess.Popen("start nginx.exe",
cwd=run_nginx_dir,
shell=True)
else:
subprocess.Popen("nohup ./nginx &",
cwd=run_nginx_dir,
shell=True)
def stop_frontend():
"""
停止前端服务
"""
if not SystemUtils.is_frozen():
return
import subprocess
if SystemUtils.is_windows():
subprocess.Popen(f"taskkill /f /im nginx.exe", shell=True)
else:
subprocess.Popen(f"killall nginx", shell=True)
def start_tray():
"""
启动托盘图标
"""
if not SystemUtils.is_frozen():
return
def open_web():
"""
调用浏览器打开前端页面
"""
import webbrowser
webbrowser.open(f"http://localhost:{settings.NGINX_PORT}")
def quit_app():
"""
退出程序
"""
TrayIcon.stop()
Server.should_exit = True
import pystray
# 托盘图标
TrayIcon = pystray.Icon(
settings.PROJECT_NAME,
icon=Image.open(settings.ROOT_PATH / 'app.ico'),
menu=pystray.Menu(
pystray.MenuItem(
'打开',
open_web,
),
pystray.MenuItem(
'退出',
quit_app,
)
)
)
# 启动托盘图标
threading.Thread(target=TrayIcon.run, daemon=True).start()
@App.on_event("shutdown")
def shutdown_server():
"""
@@ -59,6 +154,10 @@ def shutdown_server():
DisplayHelper().stop()
# 停止定时服务
Scheduler().stop()
# 停止线程池
ThreadHelper().shutdown()
# 停止前端服务
stop_frontend()
@App.on_event("startup")
@@ -66,7 +165,7 @@ def start_module():
"""
启动模块
"""
# 虚显示
# 虚显示
DisplayHelper()
# 站点管理
SitesHelper()
@@ -80,12 +179,16 @@ def start_module():
Command()
# 初始化路由
init_routers()
# 启动前端服务
start_frontend()
if __name__ == '__main__':
# 启动托盘
start_tray()
# 初始化数据库
init_db()
# 更新数据库
update_db()
# 启动服务
# 启动API服务
Server.run()

View File

@@ -1,4 +1,4 @@
from datetime import datetime
import re
from pathlib import Path
from typing import List, Optional, Tuple, Union
@@ -369,6 +369,16 @@ class DoubanModule(_ModuleBase):
return []
return infos.get("subject_collection_items")
def tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
"""
获取豆瓣动画剧
"""
infos = self.doubanapi.tv_animation(start=(page - 1) * count,
count=count)
if not infos:
return []
return infos.get("subject_collection_items")
def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
"""
搜索媒体信息
@@ -396,17 +406,30 @@ class DoubanModule(_ModuleBase):
return ret_medias
@retry(Exception, 5, 3, 3, logger=logger)
def match_doubaninfo(self, name: str, mtype: str = None,
year: str = None, season: int = None) -> dict:
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> dict:
"""
搜索和匹配豆瓣信息
:param name: 名称
:param imdbid: IMDB ID
:param mtype: 类型 电影/电视剧
:param year: 年份
:param season: 季号
"""
result = self.doubanapi.search(f"{name} {year or ''}".strip(),
ts=datetime.strftime(datetime.now(), '%Y%m%d%H%M%S'))
if imdbid:
# 优先使用IMDBID查询
logger.info(f"开始使用IMDBID {imdbid} 查询豆瓣信息 ...")
result = self.doubanapi.imdbid(imdbid)
if result:
doubanid = result.get("id")
if doubanid and not str(doubanid).isdigit():
doubanid = re.search(r"\d+", doubanid).group(0)
result["id"] = doubanid
logger.info(f"{imdbid} 查询到豆瓣信息:{result.get('title')}")
return result
# 搜索
logger.info(f"开始使用名称 {name} 匹配豆瓣信息 ...")
result = self.doubanapi.search(f"{name} {year or ''}".strip())
if not result:
logger.warn(f"未找到 {name} 的豆瓣信息")
return {}
@@ -433,6 +456,7 @@ class DoubanModule(_ModuleBase):
if meta.name == name \
and ((not season and not meta.begin_season) or meta.begin_season == season) \
and (not year or item.get('year') == year):
logger.info(f"{name} 匹配到豆瓣信息:{item.get('id')} {item.get('title')}")
return item
return {}
@@ -446,11 +470,12 @@ class DoubanModule(_ModuleBase):
return []
return infos.get("subject_collection_items")
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
"""
刮削元数据
:param path: 媒体文件路径
:param mediainfo: 识别的媒体信息
:param transfer_type: 传输类型
:return: 成功或失败
"""
if settings.SCRAP_SOURCE != "douban":
@@ -463,16 +488,21 @@ class DoubanModule(_ModuleBase):
return
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type.value,
year=mediainfo.year,
season=meta.begin_season)
if not doubaninfo:
logger.warn(f"未找到 {mediainfo.title} 的豆瓣信息")
return
# 查询豆瓣详情
doubaninfo = self.douban_info(doubaninfo.get("id"))
# 刮削路径
scrape_path = path / path.name
self.scraper.gen_scraper_files(meta=meta,
mediainfo=MediaInfo(douban_info=doubaninfo),
file_path=scrape_path)
file_path=scrape_path,
transfer_type=transfer_type)
else:
# 目录下的所有文件
for file in SystemUtils.list_files(path, settings.RMT_MEDIAEXT):
@@ -485,16 +515,20 @@ class DoubanModule(_ModuleBase):
continue
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type.value,
year=mediainfo.year,
season=meta.begin_season)
if not doubaninfo:
logger.warn(f"未找到 {mediainfo.title} 的豆瓣信息")
break
# 查询豆瓣详情
doubaninfo = self.douban_info(doubaninfo.get("id"))
# 刮削
self.scraper.gen_scraper_files(meta=meta,
mediainfo=MediaInfo(douban_info=doubaninfo),
file_path=file)
file_path=file,
transfer_type=transfer_type)
except Exception as e:
logger.error(f"刮削文件 {file} 失败,原因:{e}")
logger.error(f"刮削文件 {file} 失败,原因:{str(e)}")
logger.info(f"{path} 刮削完成")

View File

@@ -18,28 +18,29 @@ class DoubanApi(metaclass=Singleton):
_urls = {
# 搜索类
# sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映
# q=search_word&start=0&count=20&sort=U
# q=search_word&start: int = 0&count: int = 20&sort=U
# 聚合搜索
"search": "/search/weixin",
"search_agg": "/search",
"imdbid": "/movie/imdb/%s",
# 电影探索
# sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间
# tags='日本,动画,2022'&start=0&count=20&sort=U
# 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=0&count=20
# 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=0&count=20
# start: int = 0&count: int = 20
# 正在上映
"movie_showing": "/subject_collection/movie_showing/items",
# 热门电影
@@ -145,7 +146,9 @@ class DoubanApi(metaclass=Singleton):
"api-client/1 com.douban.frodo/7.3.0(207) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1"]
_api_secret_key = "bf7dddc7c9cfe6f7"
_api_key = "0dad551ec0f84ed02907ff5c42e8ec70"
_api_key2 = "0ab215a8b1977939201640fa14c66bab"
_base_url = "https://frodo.douban.com/api/v2"
_api_url = "https://api.douban.com/v2"
_session = None
def __init__(self):
@@ -153,6 +156,9 @@ class DoubanApi(metaclass=Singleton):
@classmethod
def __sign(cls, url: str, ts: int, method='GET') -> str:
"""
签名
"""
url_path = parse.urlparse(url).path
raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)])
return base64.b64encode(
@@ -164,7 +170,10 @@ class DoubanApi(metaclass=Singleton):
).decode()
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
def __invoke(self, url, **kwargs):
def __invoke(self, url: str, **kwargs) -> dict:
"""
GET请求
"""
req_url = self._base_url + url
params = {'apiKey': self._api_key}
@@ -189,119 +198,224 @@ class DoubanApi(metaclass=Singleton):
return resp.json()
return resp.json() if resp else {}
def search(self, keyword, start=0, count=20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
def __post(self, url: str, **kwargs) -> dict:
"""
POST请求
esponse = requests.post(
url="https://api.douban.com/v2/movie/imdb/tt29139455",
headers={
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": "bid=J9zb1zA5sJc",
},
data={
"apikey": "0ab215a8b1977939201640fa14c66bab",
},
)
"""
req_url = self._api_url + url
params = {'apikey': self._api_key2}
if kwargs:
params.update(kwargs)
if '_ts' in params:
params.pop('_ts')
resp = RequestUtils(
ua=settings.USER_AGENT,
session=self._session,
).post_res(url=req_url, data=params)
if resp.status_code == 400 and "rate_limit" in resp.text:
return resp.json()
return resp.json() if resp else {}
def search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')) -> dict:
"""
关键字搜索
"""
return self.__invoke(self._urls["search"], q=keyword,
start=start, count=count, _ts=ts)
def movie_search(self, keyword, start=0, count=20,
def imdbid(self, imdbid: str,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
IMDBID搜索
"""
return self.__post(self._urls["imdbid"] % imdbid, _ts=ts)
def movie_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影搜索
"""
return self.__invoke(self._urls["movie_search"], q=keyword,
start=start, count=count, _ts=ts)
def tv_search(self, keyword, start=0, count=20,
def tv_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视搜索
"""
return self.__invoke(self._urls["tv_search"], q=keyword,
start=start, count=count, _ts=ts)
def book_search(self, keyword, start=0, count=20,
def book_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
书籍搜索
"""
return self.__invoke(self._urls["book_search"], q=keyword,
start=start, count=count, _ts=ts)
def group_search(self, keyword, start=0, count=20,
def group_search(self, keyword: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
小组搜索
"""
return self.__invoke(self._urls["group_search"], q=keyword,
start=start, count=count, _ts=ts)
def movie_showing(self, start=0, count=20,
def movie_showing(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
正在热映
"""
return self.__invoke(self._urls["movie_showing"],
start=start, count=count, _ts=ts)
def movie_soon(self, start=0, count=20,
def movie_soon(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
即将上映
"""
return self.__invoke(self._urls["movie_soon"],
start=start, count=count, _ts=ts)
def movie_hot_gaia(self, start=0, count=20,
def movie_hot_gaia(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门电影
"""
return self.__invoke(self._urls["movie_hot_gaia"],
start=start, count=count, _ts=ts)
def tv_hot(self, start=0, count=20,
def tv_hot(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
热门剧集
"""
return self.__invoke(self._urls["tv_hot"],
start=start, count=count, _ts=ts)
def tv_animation(self, start=0, count=20,
def tv_animation(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
动画
"""
return self.__invoke(self._urls["tv_animation"],
start=start, count=count, _ts=ts)
def tv_variety_show(self, start=0, count=20,
def tv_variety_show(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺
"""
return self.__invoke(self._urls["tv_variety_show"],
start=start, count=count, _ts=ts)
def tv_rank_list(self, start=0, count=20,
def tv_rank_list(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧排行榜
"""
return self.__invoke(self._urls["tv_rank_list"],
start=start, count=count, _ts=ts)
def show_hot(self, start=0, count=20,
def show_hot(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
综艺热门
"""
return self.__invoke(self._urls["show_hot"],
start=start, count=count, _ts=ts)
def movie_detail(self, subject_id):
def movie_detail(self, subject_id: str):
"""
电影详情
"""
return self.__invoke(self._urls["movie_detail"] + subject_id)
def movie_celebrities(self, subject_id):
def movie_celebrities(self, subject_id: str):
"""
电影演职员
"""
return self.__invoke(self._urls["movie_celebrities"] % subject_id)
def tv_detail(self, subject_id):
def tv_detail(self, subject_id: str):
"""
电视剧详情
"""
return self.__invoke(self._urls["tv_detail"] + subject_id)
def tv_celebrities(self, subject_id):
def tv_celebrities(self, subject_id: str):
"""
电视剧演职员
"""
return self.__invoke(self._urls["tv_celebrities"] % subject_id)
def book_detail(self, subject_id):
def book_detail(self, subject_id: str):
"""
书籍详情
"""
return self.__invoke(self._urls["book_detail"] + subject_id)
def movie_top250(self, start=0, count=20,
def movie_top250(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影TOP250
"""
return self.__invoke(self._urls["movie_top250"],
start=start, count=count, _ts=ts)
def movie_recommend(self, tags='', sort='R', start=0, count=20,
def movie_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影探索
"""
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_recommend(self, tags='', sort='R', start=0, count=20,
def tv_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧探索
"""
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort,
start=start, count=count, _ts=ts)
def tv_chinese_best_weekly(self, start=0, count=20,
def tv_chinese_best_weekly(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
华语口碑周榜
"""
return self.__invoke(self._urls["tv_chinese_best_weekly"],
start=start, count=count, _ts=ts)
def tv_global_best_weekly(self, start=0, count=20,
def tv_global_best_weekly(self, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
全球口碑周榜
"""
return self.__invoke(self._urls["tv_global_best_weekly"],
start=start, count=count, _ts=ts)
def doulist_detail(self, subject_id):
def doulist_detail(self, subject_id: str):
"""
豆列详情
:param subject_id: 豆列id
"""
return self.__invoke(self._urls["doulist"] + subject_id)
def doulist_items(self, subject_id, start=0, count=20,
def doulist_items(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
豆列列表

View File

@@ -1,25 +1,34 @@
import time
from pathlib import Path
from typing import Union
from xml.dom import minidom
from app.core.config import settings
from app.core.context import MediaInfo
from app.core.meta import MetaBase
from app.log import logger
from app.schemas.types import MediaType
from app.utils.dom import DomUtils
from app.utils.http import RequestUtils
from app.utils.system import SystemUtils
class DoubanScraper:
def gen_scraper_files(self, meta: MetaBase, mediainfo: MediaInfo, file_path: Path):
_transfer_type = settings.TRANSFER_TYPE
def gen_scraper_files(self, meta: MetaBase, mediainfo: MediaInfo,
file_path: Path, transfer_type: str):
"""
生成刮削文件
:param meta: 元数据
:param mediainfo: 媒体信息
:param file_path: 文件路径或者目录路径
:param transfer_type: 转输类型
"""
self._transfer_type = transfer_type
try:
# 电影
if mediainfo.type == MediaType.MOVIE:
@@ -48,7 +57,7 @@ class DoubanScraper:
season=meta.begin_season,
season_path=file_path.parent)
except Exception as e:
logger.error(f"{file_path} 刮削失败:{e}")
logger.error(f"{file_path} 刮削失败:{str(e)}")
@staticmethod
def __gen_common_nfo(mediainfo: MediaInfo, doc, root):
@@ -154,31 +163,55 @@ class DoubanScraper:
# 保存
self.__save_nfo(doc, season_path.joinpath("season.nfo"))
@staticmethod
def __save_image(url: str, file_path: Path):
def __save_image(self, url: str, file_path: Path):
"""
下载图片并保存
"""
if file_path.exists():
return
if not url:
return
try:
# 没有后缀时处理URL转化为jpg格式
if not file_path.suffix:
url = url.replace("/format/webp", "/format/jpg")
file_path.with_suffix(".jpg")
logger.info(f"正在下载{file_path.stem}图片:{url} ...")
r = RequestUtils().get_res(url=url)
if r:
file_path.write_bytes(r.content)
if self._transfer_type in ['rclone_move', 'rclone_copy']:
self.__save_remove_file(file_path, r.content)
else:
file_path.write_bytes(r.content)
logger.info(f"图片已保存:{file_path}")
else:
logger.info(f"{file_path.stem}图片下载失败,请检查网络连通性")
except Exception as err:
logger.error(f"{file_path.stem}图片下载失败:{err}")
logger.error(f"{file_path.stem}图片下载失败:{str(err)}")
@staticmethod
def __save_nfo(doc, file_path: Path):
def __save_nfo(self, doc, file_path: Path):
"""
保存NFO
"""
if file_path.exists():
return
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
file_path.write_bytes(xml_str)
if self._transfer_type in ['rclone_move', 'rclone_copy']:
self.__save_remove_file(file_path, xml_str)
else:
file_path.write_bytes(xml_str)
logger.info(f"NFO文件已保存{file_path}")
def __save_remove_file(self, out_file: Path, content: Union[str, bytes]):
"""
保存文件到远端
"""
temp_file = settings.TEMP_PATH / str(out_file)[1:]
temp_file_dir = temp_file.parent
if not temp_file_dir.exists():
temp_file_dir.mkdir(parents=True, exist_ok=True)
temp_file.write_bytes(content)
if self._transfer_type == 'rclone_move':
SystemUtils.rclone_move(temp_file, out_file)
elif self._transfer_type == 'rclone_copy':
SystemUtils.rclone_copy(temp_file, out_file)

View File

@@ -26,7 +26,7 @@ class EmbyModule(_ModuleBase):
定时任务每10分钟调用一次
"""
# 定时重连
if not self.emby.is_inactive():
if self.emby.is_inactive():
self.emby.reconnect()
def user_authenticate(self, name: str, password: str) -> Optional[str]:

View File

@@ -23,7 +23,7 @@ class Emby(metaclass=Singleton):
if not self._host.startswith("http"):
self._host = "http://" + self._host
self._apikey = settings.EMBY_API_KEY
self.user = self.get_user()
self.user = self.get_user(settings.SUPERUSER)
self.folders = self.get_emby_folders()
def is_inactive(self) -> bool:
@@ -800,7 +800,7 @@ class Emby(metaclass=Singleton):
eventType = message.get('Event')
if not eventType:
return None
logger.info(f"接收到emby webhook{message}")
logger.debug(f"接收到emby webhook{message}")
eventItem = schemas.WebhookEventInfo(event=eventType, channel="emby")
if message.get('Item'):
if message.get('Item', {}).get('Type') == 'Episode':

View File

@@ -382,5 +382,5 @@ class FanartModule(_ModuleBase):
if ret:
return ret.json()
except Exception as err:
logger.error(f"获取{queryid}的Fanart图片失败{err}")
logger.error(f"获取{queryid}的Fanart图片失败{str(err)}")
return None

View File

@@ -13,6 +13,7 @@ from app.log import logger
from app.modules import _ModuleBase
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode
from app.schemas.types import MediaType
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
lock = Lock()
@@ -45,13 +46,16 @@ class FileTransferModule(_ModuleBase):
# 获取目标路径
if not target:
target = self.get_target_path(in_path=path)
else:
elif not target.exists() or target.is_file():
# 目的路径不存在或者是文件时,找对应的媒体库目录
target = self.get_library_path(target)
if not target:
logger.error("未找到媒体库目录,无法转移文件")
return TransferInfo(success=False,
path=path,
message="未找到媒体库目录")
else:
logger.info(f"获取转移目标路径:{target}")
# 转移
return self.transfer_media(in_path=path,
in_meta=meta,
@@ -80,6 +84,12 @@ class FileTransferModule(_ModuleBase):
elif transfer_type == 'move':
# 移动
retcode, retmsg = SystemUtils.move(file_item, target_file)
elif transfer_type == 'rclone_move':
# Rclone 移动
retcode, retmsg = SystemUtils.rclone_move(file_item, target_file)
elif transfer_type == 'rclone_copy':
# Rclone 复制
retcode, retmsg = SystemUtils.rclone_copy(file_item, target_file)
else:
# 复制
retcode, retmsg = SystemUtils.copy(file_item, target_file)
@@ -376,10 +386,12 @@ class FileTransferModule(_ModuleBase):
path=in_path,
message=f"{in_path} 路径不存在")
if not target_dir.exists():
return TransferInfo(success=False,
path=in_path,
message=f"{target_dir} 目标路径不存在")
if transfer_type not in ['rclone_copy', 'rclone_move']:
# 检查目标路径
if not target_dir.exists():
return TransferInfo(success=False,
path=in_path,
message=f"{target_dir} 目标路径不存在")
# 媒体库目的目录
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
@@ -395,6 +407,8 @@ class FileTransferModule(_ModuleBase):
bluray_flag = SystemUtils.is_bluray_dir(in_path)
if bluray_flag:
logger.info(f"{in_path} 是蓝光原盘文件夹")
# 原文件大小
file_size = in_path.stat().st_size
# 目的路径
new_path = self.get_rename_path(
path=target_dir,
@@ -419,7 +433,7 @@ class FileTransferModule(_ModuleBase):
return TransferInfo(success=True,
path=in_path,
target_path=new_path,
total_size=new_path.stat().st_size,
total_size=file_size,
is_bluray=bluray_flag)
else:
# 转移单个文件
@@ -457,10 +471,27 @@ class FileTransferModule(_ModuleBase):
# 判断是否要覆盖
overflag = False
if new_file.exists():
if new_file.stat().st_size < in_path.stat().st_size:
logger.info(f"目标文件已存在,但文件大小更小,将覆盖:{new_file}")
overflag = True
# 目标文件已存在
logger.info(f"目标文件已存在,转移覆盖模式:{settings.OVERWRITE_MODE}")
match settings.OVERWRITE_MODE:
case 'always':
overflag = True
case 'size':
if new_file.stat().st_size < in_path.stat().st_size:
logger.info(f"目标文件文件大小更小,将被覆盖:{new_file}")
overflag = True
case 'never':
pass
case _:
pass
if not overflag:
return TransferInfo(success=False,
message=f"目标文件已存在,转移覆盖模式:{settings.OVERWRITE_MODE}",
path=in_path,
target_path=new_file,
fail_list=[str(in_path)])
# 原文件大小
file_size = in_path.stat().st_size
# 转移文件
retcode = self.__transfer_file(file_item=in_path,
new_file=new_file,
@@ -479,7 +510,7 @@ class FileTransferModule(_ModuleBase):
path=in_path,
target_path=new_file,
file_count=1,
total_size=new_file.stat().st_size,
total_size=file_size,
is_bluray=False,
file_list=[str(in_path)],
file_list_new=[str(new_file)])
@@ -565,7 +596,7 @@ class FileTransferModule(_ModuleBase):
@staticmethod
def get_library_path(path: Path):
"""
根据目录查询其所在的媒体库目录,查询不到的返回输入目录
根据文件路径查询其所在的媒体库目录,查询不到的返回输入目录
"""
if not path:
return None
@@ -578,7 +609,7 @@ class FileTransferModule(_ModuleBase):
if path.is_relative_to(libpath):
return libpath
except Exception as e:
logger.debug(f"计算媒体库路径时出错:{e}")
logger.debug(f"计算媒体库路径时出错:{str(e)}")
continue
return path
@@ -601,12 +632,13 @@ class FileTransferModule(_ModuleBase):
if in_path:
for path in dest_paths:
try:
relative = in_path.relative_to(path).as_posix()
# 计算in_path和path的公共字符串长度
relative = StringUtils.find_common_prefix(str(in_path), str(path))
if len(relative) > max_length:
max_length = len(relative)
target_path = path
except Exception as e:
logger.debug(f"计算目标路径时出错:{e}")
logger.debug(f"计算目标路径时出错:{str(e)}")
continue
if target_path:
return target_path

View File

@@ -70,16 +70,26 @@ class FilterModule(_ModuleBase):
"include": [r'[Hx].?264|AVC'],
"exclude": []
},
# 杜比
# 杜比视界
"DOLBY": {
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
"exclude": []
},
# 杜比全景声
"ATMOS": {
"include": [r"Dolby[\s.+]+Atmos|Atmos|杜比全景[声聲]"],
"exclude": []
},
# HDR
"HDR": {
"include": [r"[\s.]+HDR[\s.]+|HDR10|HDR10\+"],
"exclude": []
},
# SDR
"SDR": {
"include": [r"[\s.]+SDR[\s.]+"],
"exclude": []
},
# 重编码
"REMUX": {
"include": [r'REMUX'],
@@ -98,7 +108,22 @@ class FilterModule(_ModuleBase):
"CNVOI": {
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
"exclude": []
}
},
# 粤语配音
"HKVOI": {
"include": [r'粤语配音|粤语'],
"exclude": []
},
# 60FPS
"60FPS": {
"include": [r'60fps'],
"exclude": []
},
# 3D
"3D": {
"include": [r'3D'],
"exclude": []
},
}
def init_module(self) -> None:

View File

@@ -92,7 +92,7 @@ class IndexerModule(_ModuleBase):
if result_array:
break
except Exception as err:
logger.error(f"{site.get('name')} 搜索出错:{err}")
logger.error(f"{site.get('name')} 搜索出错:{str(err)}")
# 索引花费的时间
seconds = round((datetime.now() - start_time).seconds, 1)

View File

@@ -254,7 +254,7 @@ class TorrentSpider:
# 解码为字符串
page_source = raw_data.decode(encoding)
except Exception as e:
logger.debug(f"chardet解码失败{e}")
logger.debug(f"chardet解码失败{str(e)}")
# 探测utf-8解码
if re.search(r"charset=\"?utf-8\"?", ret.text, re.IGNORECASE):
ret.encoding = "utf-8"
@@ -661,4 +661,4 @@ class TorrentSpider:
return self.torrents_info_array
except Exception as err:
self.is_error = True
logger.warn(f"错误:{self.indexername} {err}")
logger.warn(f"错误:{self.indexername} {str(err)}")

View File

@@ -23,7 +23,7 @@ class JellyfinModule(_ModuleBase):
定时任务每10分钟调用一次
"""
# 定时重连
if not self.jellyfin.is_inactive():
if self.jellyfin.is_inactive():
self.jellyfin.reconnect()
def stop(self):

View File

@@ -21,7 +21,7 @@ class Jellyfin(metaclass=Singleton):
if not self._host.startswith("http"):
self._host = "http://" + self._host
self._apikey = settings.JELLYFIN_API_KEY
self.user = self.get_user()
self.user = self.get_user(settings.SUPERUSER)
self.serverid = self.get_server_id()
def is_inactive(self) -> bool:
@@ -452,7 +452,7 @@ class Jellyfin(metaclass=Singleton):
return None
if not message:
return None
logger.info(f"接收到jellyfin webhook{message}")
logger.debug(f"接收到jellyfin webhook{message}")
eventType = message.get('NotificationType')
if not eventType:
return None

View File

@@ -26,7 +26,7 @@ class PlexModule(_ModuleBase):
定时任务每10分钟调用一次
"""
# 定时重连
if not self.plex.is_inactive():
if self.plex.is_inactive():
self.plex.reconnect()
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:

View File

@@ -284,7 +284,7 @@ class Plex(metaclass=Singleton):
if is_subpath(path, Path(location)):
return lib.key, str(path)
except Exception as err:
logger.error(f"查找媒体库出错:{err}")
logger.error(f"查找媒体库出错:{str(err)}")
return "", ""
def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]:
@@ -313,7 +313,7 @@ class Plex(metaclass=Singleton):
path=path,
)
except Exception as err:
logger.error(f"获取项目详情出错:{err}")
logger.error(f"获取项目详情出错:{str(err)}")
return None
@staticmethod
@@ -372,7 +372,7 @@ class Plex(metaclass=Singleton):
path=path,
)
except Exception as err:
logger.error(f"获取媒体库列表出错:{err}")
logger.error(f"获取媒体库列表出错:{str(err)}")
yield None
def get_webhook_message(self, form: any) -> Optional[schemas.WebhookEventInfo]:
@@ -492,7 +492,7 @@ class Plex(metaclass=Singleton):
eventType = message.get('event')
if not eventType:
return None
logger.info(f"接收到plex webhook{message}")
logger.debug(f"接收到plex webhook{message}")
eventItem = schemas.WebhookEventInfo(event=eventType, channel="plex")
if message.get('Metadata'):
if message.get('Metadata', {}).get('type') == 'episode':

View File

@@ -101,9 +101,15 @@ class QbittorrentModule(_ModuleBase):
# 选择文件
self.qbittorrent.set_files(torrent_hash=torrent_hash, file_ids=file_ids, priority=0)
# 开始任务
self.qbittorrent.start_torrents(torrent_hash)
if settings.QB_FORCE_RESUME:
# 强制继续
self.qbittorrent.torrents_set_force_start(torrent_hash)
else:
self.qbittorrent.start_torrents(torrent_hash)
return torrent_hash, f"添加下载成功,已选择集数:{sucess_epidised}"
else:
if settings.QB_FORCE_RESUME:
self.qbittorrent.torrents_set_force_start(torrent_hash)
return torrent_hash, "添加下载成功"
def list_torrents(self, status: TorrentStatus = None,
@@ -123,7 +129,7 @@ class QbittorrentModule(_ModuleBase):
if content_path:
torrent_path = Path(content_path)
else:
torrent_path = Path(settings.DOWNLOAD_PATH) / torrent.get('name')
torrent_path = settings.SAVE_PATH / torrent.get('name')
ret_torrents.append(TransferTorrent(
title=torrent.get('name'),
path=torrent_path,
@@ -142,7 +148,7 @@ class QbittorrentModule(_ModuleBase):
if content_path:
torrent_path = Path(content_path)
else:
torrent_path = Path(settings.DOWNLOAD_PATH) / torrent.get('name')
torrent_path = settings.SAVE_PATH / torrent.get('name')
ret_torrents.append(TransferTorrent(
title=torrent.get('name'),
path=torrent_path,
@@ -165,6 +171,9 @@ class QbittorrentModule(_ModuleBase):
state="paused" if torrent.get('state') == "paused" else "downloading",
dlspeed=StringUtils.str_filesize(torrent.get('dlspeed')),
upspeed=StringUtils.str_filesize(torrent.get('upspeed')),
left_time=StringUtils.str_secends(
(torrent.get('total_size') - torrent.get('completed')) / torrent.get('dlspeed')) if torrent.get(
'dlspeed') > 0 else ''
))
else:
return None
@@ -211,7 +220,7 @@ class QbittorrentModule(_ModuleBase):
:param hashs: 种子Hash
:return: bool
"""
return self.qbittorrent.start_torrents(ids=hashs)
return self.qbittorrent.stop_torrents(ids=hashs)
def torrent_files(self, tid: str) -> Optional[TorrentFilesList]:
"""

View File

@@ -57,10 +57,10 @@ class Qbittorrent(metaclass=Singleton):
try:
qbt.auth_log_in()
except qbittorrentapi.LoginFailed as e:
logger.error(f"qbittorrent 登录失败:{e}")
logger.error(f"qbittorrent 登录失败:{str(e)}")
return qbt
except Exception as err:
logger.error(f"qbittorrent 连接出错:{err}")
logger.error(f"qbittorrent 连接出错:{str(err)}")
return None
def get_torrents(self, ids: Union[str, list] = None,
@@ -86,7 +86,7 @@ class Qbittorrent(metaclass=Singleton):
return results, False
return torrents or [], False
except Exception as err:
logger.error(f"获取种子列表出错:{err}")
logger.error(f"获取种子列表出错:{str(err)}")
return [], True
def get_completed_torrents(self, ids: Union[str, list] = None,
@@ -126,7 +126,7 @@ class Qbittorrent(metaclass=Singleton):
self.qbc.torrents_delete_tags(torrent_hashes=ids, tags=tag)
return True
except Exception as err:
logger.error(f"移除种子Tag出错{err}")
logger.error(f"移除种子Tag出错{str(err)}")
return False
def set_torrents_tag(self, ids: Union[str, list], tags: list):
@@ -139,7 +139,7 @@ class Qbittorrent(metaclass=Singleton):
# 打标签
self.qbc.torrents_add_tags(tags=tags, torrent_hashes=ids)
except Exception as err:
logger.error(f"设置种子Tag出错{err}")
logger.error(f"设置种子Tag出错{str(err)}")
def torrents_set_force_start(self, ids: Union[str, list]):
"""
@@ -150,7 +150,7 @@ class Qbittorrent(metaclass=Singleton):
try:
self.qbc.torrents_set_force_start(enable=True, torrent_hashes=ids)
except Exception as err:
logger.error(f"设置强制作种出错:{err}")
logger.error(f"设置强制作种出错:{str(err)}")
def __get_last_add_torrentid_by_tag(self, tags: Union[str, list],
status: Union[str, list] = None) -> Optional[str]:
@@ -161,7 +161,7 @@ class Qbittorrent(metaclass=Singleton):
try:
torrents, _ = self.get_torrents(status=status, tags=tags)
except Exception as err:
logger.error(f"获取种子列表出错:{err}")
logger.error(f"获取种子列表出错:{str(err)}")
return None
if torrents:
return torrents[0].get("hash")
@@ -243,13 +243,13 @@ class Qbittorrent(metaclass=Singleton):
is_paused=is_paused,
tags=tags,
use_auto_torrent_management=is_auto,
is_sequential_download=True,
is_sequential_download=settings.QB_SEQUENTIAL,
cookie=cookie,
category=category,
**kwargs)
return True if qbc_ret and str(qbc_ret).find("Ok") != -1 else False
except Exception as err:
logger.error(f"添加种子出错:{err}")
logger.error(f"添加种子出错:{str(err)}")
return False
def start_torrents(self, ids: Union[str, list]) -> bool:
@@ -262,7 +262,7 @@ class Qbittorrent(metaclass=Singleton):
self.qbc.torrents_resume(torrent_hashes=ids)
return True
except Exception as err:
logger.error(f"启动种子出错:{err}")
logger.error(f"启动种子出错:{str(err)}")
return False
def stop_torrents(self, ids: Union[str, list]) -> bool:
@@ -275,7 +275,7 @@ class Qbittorrent(metaclass=Singleton):
self.qbc.torrents_pause(torrent_hashes=ids)
return True
except Exception as err:
logger.error(f"暂停种子出错:{err}")
logger.error(f"暂停种子出错:{str(err)}")
return False
def delete_torrents(self, delete_file: bool, ids: Union[str, list]) -> bool:
@@ -290,7 +290,7 @@ class Qbittorrent(metaclass=Singleton):
self.qbc.torrents_delete(delete_files=delete_file, torrent_hashes=ids)
return True
except Exception as err:
logger.error(f"删除种子出错:{err}")
logger.error(f"删除种子出错:{str(err)}")
return False
def get_files(self, tid: str) -> Optional[TorrentFilesList]:
@@ -302,7 +302,7 @@ class Qbittorrent(metaclass=Singleton):
try:
return self.qbc.torrents_files(torrent_hash=tid)
except Exception as err:
logger.error(f"获取种子文件列表出错:{err}")
logger.error(f"获取种子文件列表出错:{str(err)}")
return None
def set_files(self, **kwargs) -> bool:
@@ -319,7 +319,7 @@ class Qbittorrent(metaclass=Singleton):
priority=kwargs.get("priority"))
return True
except Exception as err:
logger.error(f"设置种子文件状态出错:{err}")
logger.error(f"设置种子文件状态出错:{str(err)}")
return False
def transfer_info(self) -> Optional[TransferInfoDictionary]:
@@ -331,7 +331,7 @@ class Qbittorrent(metaclass=Singleton):
try:
return self.qbc.transfer_info()
except Exception as err:
logger.error(f"获取传输信息出错:{err}")
logger.error(f"获取传输信息出错:{str(err)}")
return None
def set_speed_limit(self, download_limit: float = None, upload_limit: float = None) -> bool:
@@ -349,7 +349,7 @@ class Qbittorrent(metaclass=Singleton):
self.qbc.transfer.download_limit = int(download_limit)
return True
except Exception as err:
logger.error(f"设置速度限制出错:{err}")
logger.error(f"设置速度限制出错:{str(err)}")
return False
def recheck_torrents(self, ids: Union[str, list]):
@@ -361,7 +361,7 @@ class Qbittorrent(metaclass=Singleton):
try:
return self.qbc.torrents_recheck(torrent_hashes=ids)
except Exception as err:
logger.error(f"重新校验种子出错:{err}")
logger.error(f"重新校验种子出错:{str(err)}")
return False
def add_trackers(self, ids: Union[str, list], trackers: list):
@@ -373,5 +373,5 @@ class Qbittorrent(metaclass=Singleton):
try:
return self.qbc.torrents_add_trackers(torrent_hashes=ids, urls=trackers)
except Exception as err:
logger.error(f"添加tracker出错{err}")
logger.error(f"添加tracker出错{str(err)}")
return False

View File

@@ -151,7 +151,7 @@ class SlackModule(_ModuleBase):
try:
msg_json: dict = json.loads(body)
except Exception as err:
logger.debug(f"解析Slack消息失败{err}")
logger.debug(f"解析Slack消息失败{str(err)}")
return None
if msg_json:
if msg_json.get("type") == "message":

View File

@@ -34,7 +34,7 @@ class Slack:
ssl_check_enabled=False,
url_verification_enabled=False)
except Exception as err:
logger.error(f"Slack初始化失败: {err}")
logger.error(f"Slack初始化失败: {str(err)}")
return
self._client = slack_app.client
@@ -335,5 +335,5 @@ class Slack:
conversation_id = channel.get("id")
break
except Exception as e:
logger.error(f"查找Slack公共频道失败: {e}")
logger.error(f"查找Slack公共频道失败: {str(e)}")
return conversation_id

View File

@@ -122,7 +122,7 @@ class SubtitleModule(_ModuleBase):
shutil.rmtree(zip_path)
zip_file.unlink()
except Exception as err:
logger.error(f"删除临时文件失败:{err}")
logger.error(f"删除临时文件失败:{str(err)}")
else:
sub_file = settings.TEMP_PATH / file_name
# 保存

View File

@@ -50,7 +50,7 @@ class SynologyChatModule(_ModuleBase):
return CommingMessage(channel=MessageChannel.SynologyChat,
userid=user_id, username=user_name, text=text)
except Exception as err:
logger.debug(f"解析SynologyChat消息失败{err}")
logger.debug(f"解析SynologyChat消息失败{str(err)}")
return None
@checkMessage(MessageChannel.SynologyChat)

View File

@@ -63,7 +63,7 @@ class TelegramModule(_ModuleBase):
try:
message: dict = json.loads(body)
except Exception as err:
logger.debug(f"解析Telegram消息失败{err}")
logger.debug(f"解析Telegram消息失败{str(err)}")
return None
if message:
text = message.get("text")

View File

@@ -54,7 +54,7 @@ class Telegram(metaclass=Singleton):
try:
_bot.infinity_polling(long_polling_timeout=30, logger_level=None)
except Exception as err:
logger.error(f"Telegram消息接收服务异常{err}")
logger.error(f"Telegram消息接收服务异常{str(err)}")
# 启动线程来运行 infinity_polling
self._polling_thread = threading.Thread(target=run_polling)

View File

@@ -63,7 +63,10 @@ class TheMovieDbModule(_ModuleBase):
# 直接查询详情
info = self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
elif meta:
logger.info(f"正在识别 {meta.name} ...")
if meta.begin_season:
logger.info(f"正在识别 {meta.name}{meta.begin_season}季 ...")
else:
logger.info(f"正在识别 {meta.name} ...")
if meta.type == MediaType.UNKNOWN and not meta.year:
info = self.tmdb.match_multi(meta.name)
else:
@@ -184,11 +187,12 @@ class TheMovieDbModule(_ModuleBase):
return [MediaInfo(tmdb_info=info) for info in results]
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
def scrape_metadata(self, path: Path, mediainfo: MediaInfo, transfer_type: str) -> None:
"""
刮削元数据
:param path: 媒体文件路径
:param mediainfo: 识别的媒体信息
:param transfer_type: 转移类型
:return: 成功或失败
"""
if settings.SCRAP_SOURCE != "themoviedb":
@@ -199,12 +203,14 @@ class TheMovieDbModule(_ModuleBase):
logger.info(f"开始刮削蓝光原盘:{path} ...")
scrape_path = path / path.name
self.scraper.gen_scraper_files(mediainfo=mediainfo,
file_path=scrape_path)
file_path=scrape_path,
transfer_type=transfer_type)
elif path.is_file():
# 单个文件
logger.info(f"开始刮削媒体库文件:{path} ...")
self.scraper.gen_scraper_files(mediainfo=mediainfo,
file_path=path)
file_path=path,
transfer_type=transfer_type)
else:
# 目录下的所有文件
logger.info(f"开始刮削目录:{path} ...")
@@ -212,7 +218,8 @@ class TheMovieDbModule(_ModuleBase):
if not file:
continue
self.scraper.gen_scraper_files(mediainfo=mediainfo,
file_path=file)
file_path=file,
transfer_type=transfer_type)
logger.info(f"{path} 刮削完成")
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str, with_original_language: str,
@@ -280,6 +287,8 @@ class TheMovieDbModule(_ModuleBase):
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
if not mediainfo.tmdb_id:
return mediainfo
if mediainfo.logo_path \
and mediainfo.poster_path \
and mediainfo.backdrop_path:

View File

@@ -32,7 +32,7 @@ class CategoryHelper(metaclass=Singleton):
logger.warn(f"二级分类策略配置文件格式出现严重错误!请检查:{str(e)}")
self._categorys = {}
except Exception as err:
logger.warn(f"二级分类策略配置文件加载出错:{err}")
logger.warn(f"二级分类策略配置文件加载出错:{str(err)}")
if self._categorys:
self._movie_categorys = self._categorys.get('movie')

View File

@@ -1,5 +1,6 @@
import time
from pathlib import Path
from typing import Union
from xml.dom import minidom
from requests import RequestException
@@ -12,21 +13,26 @@ from app.schemas.types import MediaType
from app.utils.common import retry
from app.utils.dom import DomUtils
from app.utils.http import RequestUtils
from app.utils.system import SystemUtils
class TmdbScraper:
tmdb = None
_transfer_type = settings.TRANSFER_TYPE
def __init__(self, tmdb):
self.tmdb = tmdb
def gen_scraper_files(self, mediainfo: MediaInfo, file_path: Path):
def gen_scraper_files(self, mediainfo: MediaInfo, file_path: Path, transfer_type: str):
"""
生成刮削文件包括NFO和图片传入路径为文件路径
:param mediainfo: 媒体信息
:param file_path: 文件路径或者目录路径
:param transfer_type: 传输类型
"""
self._transfer_type = transfer_type
def __get_episode_detail(_seasoninfo: dict, _episode: int):
"""
根据季信息获取集的信息
@@ -119,7 +125,7 @@ class TmdbScraper:
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episode_image}",
file_path.with_suffix(Path(episode_image).suffix))
except Exception as e:
logger.error(f"{file_path} 刮削失败:{e}")
logger.error(f"{file_path} 刮削失败:{str(e)}")
@staticmethod
def __gen_common_nfo(mediainfo: MediaInfo, doc, root):
@@ -328,9 +334,8 @@ class TmdbScraper:
# 保存文件
self.__save_nfo(doc, file_path.with_suffix(".nfo"))
@staticmethod
@retry(RequestException, logger=logger)
def __save_image(url: str, file_path: Path):
def __save_image(self, url: str, file_path: Path):
"""
下载图片并保存
"""
@@ -340,22 +345,41 @@ class TmdbScraper:
logger.info(f"正在下载{file_path.stem}图片:{url} ...")
r = RequestUtils().get_res(url=url, raise_exception=True)
if r:
file_path.write_bytes(r.content)
if self._transfer_type in ['rclone_move', 'rclone_copy']:
self.__save_remove_file(file_path, r.content)
else:
file_path.write_bytes(r.content)
logger.info(f"图片已保存:{file_path}")
else:
logger.info(f"{file_path.stem}图片下载失败,请检查网络连通性")
except RequestException as err:
raise err
except Exception as err:
logger.error(f"{file_path.stem}图片下载失败:{err}")
logger.error(f"{file_path.stem}图片下载失败:{str(err)}")
@staticmethod
def __save_nfo(doc, file_path: Path):
def __save_nfo(self, doc, file_path: Path):
"""
保存NFO
"""
if file_path.exists():
return
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
file_path.write_bytes(xml_str)
if self._transfer_type in ['rclone_move', 'rclone_copy']:
self.__save_remove_file(file_path, xml_str)
else:
file_path.write_bytes(xml_str)
logger.info(f"NFO文件已保存{file_path}")
def __save_remove_file(self, out_file: Path, content: Union[str, bytes]):
"""
保存文件到远端
"""
temp_file = settings.TEMP_PATH / str(out_file)[1:]
temp_file_dir = temp_file.parent
if not temp_file_dir.exists():
temp_file_dir.mkdir(parents=True, exist_ok=True)
temp_file.write_bytes(content)
if self._transfer_type == 'rclone_move':
SystemUtils.rclone_move(temp_file, out_file)
elif self._transfer_type == 'rclone_copy':
SystemUtils.rclone_copy(temp_file, out_file)

View File

@@ -144,7 +144,8 @@ class TmdbCache(metaclass=Singleton):
"backdrop_path": info.get("backdrop_path"),
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
}
else:
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': 0}
def save(self, force: bool = False) -> None:

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