Compare commits

...

135 Commits

Author SHA1 Message Date
jxxghp
f0fbad889d - 偿试修复可执行文件打包插件数据表缺失问题 2023-12-05 20:57:53 +08:00
jxxghp
1323cd5dc6 fix 插件下载Bug 2023-12-04 17:27:33 +08:00
jxxghp
2c43d8e145 fix 插件下载Bug 2023-12-04 16:58:39 +08:00
jxxghp
0214beb679 - 修复二进制打包插件表缺失的问题 2023-12-04 11:33:16 +08:00
jxxghp
7d73cdef33 Merge remote-tracking branch 'origin/main' 2023-12-04 11:03:00 +08:00
jxxghp
fcfab2c750 feat 命名增加豆瓣ID/识别英文名称 2023-12-04 11:02:53 +08:00
jxxghp
e048be17a5 Merge pull request #1195 from WithdewHua/fix-mediaserver 2023-12-02 12:47:20 +08:00
WithdewHua
024f1de4f1 fix: 清空 MediaServerItem 表 2023-12-02 12:31:38 +08:00
jxxghp
d2c9f7a778 更新 README.md 2023-12-01 18:39:40 +08:00
jxxghp
1784d2ec61 v1.4.8 2023-12-01 10:51:49 +08:00
jxxghp
8f4a213f55 fix QB&TR 判重 2023-12-01 08:12:06 +08:00
jxxghp
6a8c684af0 feat 兼容TR下载任务重复添加 2023-11-30 15:58:44 +08:00
jxxghp
aefba83319 fix 2023-11-30 13:35:26 +08:00
jxxghp
e411f4062a feat 兼容QB下载任务重复添加 2023-11-30 13:14:55 +08:00
jxxghp
3c6802860d fix #1186 2023-11-30 11:19:43 +08:00
jxxghp
3daad5ea90 Merge pull request #1186 from honue/main 2023-11-30 11:08:00 +08:00
jxxghp
6835c38c24 Merge remote-tracking branch 'origin/main' 2023-11-30 08:16:30 +08:00
honue
32be9b71d5 Merge remote-tracking branch 'origin/main' 2023-11-30 07:42:19 +08:00
honue
84bc0e0fb4 fix 2023-11-30 07:41:58 +08:00
Summer⛱
8105dc9c82 Merge branch 'jxxghp:main' into main 2023-11-30 00:42:23 +08:00
honue
840c968454 tr增加修改tracker方法 2023-11-30 00:39:57 +08:00
honue
8c5f19a0f4 fix 2023-11-30 00:39:35 +08:00
jxxghp
bf813aa906 Merge pull request #1181 from honue/main 2023-11-29 13:29:40 +08:00
honue
e5ac7f10d4 update downloader init 2023-11-29 13:23:28 +08:00
jxxghp
d049e04fa2 v1.4.7 2023-11-29 13:13:58 +08:00
jxxghp
ef45b08ee5 Merge pull request #1180 from honue/main 2023-11-29 13:01:18 +08:00
honue
d77644ab16 fix singleton.py 2023-11-29 12:46:49 +08:00
jxxghp
1a90b4a3cb v1.4.7 2023-11-29 12:39:37 +08:00
jxxghp
9e5d401a85 remove plugin_color 2023-11-29 12:07:54 +08:00
jxxghp
6da6dc2b8c Merge pull request #1178 from cikezhu/main 2023-11-29 08:43:26 +08:00
叮叮当
2ca7021df0 fix item_id 2023-11-29 08:35:44 +08:00
jxxghp
1682cdad37 fix README.md 2023-11-28 14:55:05 +08:00
jxxghp
ec5e898feb fix app.env 2023-11-27 08:36:45 +08:00
jxxghp
ccf6fb1b36 fix 插件重置 2023-11-27 08:30:13 +08:00
jxxghp
83c8d619d0 fix README.md 2023-11-27 08:17:23 +08:00
jxxghp
dd5b8219a1 更新 plugin.py 2023-11-27 07:09:38 +08:00
jxxghp
34fd927972 更新 README.md 2023-11-26 22:50:43 +08:00
jxxghp
6db5ad2697 feat:插件重置 2023-11-26 21:40:43 +08:00
jxxghp
0be1f27970 - 修复了种子失效时订阅一直偿试下载的问题
- 新增配置中心插件,可快速调整配置
2023-11-26 20:16:44 +08:00
jxxghp
54603798fc fix bug 2023-11-24 15:26:54 +08:00
jxxghp
2f1e947323 fix #1144 2023-11-24 15:24:38 +08:00
jxxghp
ef2cfe1c1d fix #1141 修复错误种子链接一直偿试下载的问题 2023-11-24 14:32:56 +08:00
jxxghp
f8edf79c59 fix api doc 2023-11-24 14:00:52 +08:00
jxxghp
dec80b6567 fix 2023-11-23 17:25:54 +08:00
jxxghp
4dac9237ef fix README.md 2023-11-23 17:22:13 +08:00
jxxghp
12f5f373b3 fix frozen 2023-11-23 10:59:33 +08:00
jxxghp
76472770bf v1.4.5 2023-11-22 15:35:12 +08:00
jxxghp
f5baf77c3c Merge pull request #1155 from thsrite/main 2023-11-22 13:42:33 +08:00
thsrite
126276c727 fix #1154 2023-11-22 13:39:39 +08:00
jxxghp
5d6ba83fc5 Merge pull request #1153 from thsrite/main 2023-11-22 09:23:19 +08:00
thsrite
047c3b596d add 插件方法 2023-11-22 09:21:51 +08:00
jxxghp
2240ff08a1 fix 插件事件依赖 2023-11-22 07:25:00 +08:00
jxxghp
4d6bf56fa0 fix bug 2023-11-21 21:44:50 +08:00
jxxghp
10ad9a5601 fix bug 2023-11-21 21:20:02 +08:00
jxxghp
fb39500428 feat 优化插件仓库URL格式 2023-11-21 21:10:34 +08:00
jxxghp
615a52cef9 更新 build.yml 2023-11-20 21:53:38 +08:00
jxxghp
791be0583a fix build 2023-11-20 20:14:07 +08:00
jxxghp
a324731061 - 优化豆瓣详情展示 2023-11-20 20:06:25 +08:00
jxxghp
539d9cf537 - 优化豆瓣详情展示 2023-11-20 20:01:04 +08:00
jxxghp
699312ff28 - 优化豆瓣详情展示 2023-11-20 19:56:52 +08:00
jxxghp
b92b0ec149 fix build 2023-11-20 19:54:41 +08:00
jxxghp
51536062f1 fix 2023-11-20 16:43:49 +08:00
jxxghp
4c230b4c1e feat 清理缓存时清理种子缓存 2023-11-20 10:56:28 +08:00
jxxghp
a7752ceb17 fix bug 2023-11-19 09:48:06 +08:00
jxxghp
ed59a90d78 fix update timeout 2023-11-19 09:45:13 +08:00
jxxghp
11d65e7527 fix douban scraper 2023-11-19 09:05:58 +08:00
jxxghp
b6ac5f0f84 feat 完善豆瓣详情API 2023-11-18 22:37:23 +08:00
jxxghp
c336e62885 Merge remote-tracking branch 'origin/main' 2023-11-18 21:54:10 +08:00
jxxghp
b868cdb25e feat 完善豆瓣详情API 2023-11-18 21:53:58 +08:00
jxxghp
04339539d1 fix bug 2023-11-17 14:25:00 +08:00
jxxghp
2d146880ec fix 2023-11-17 13:36:23 +08:00
jxxghp
6eec4ef7f4 fix build 2023-11-17 12:27:43 +08:00
jxxghp
9841f3dd18 fix build 2023-11-17 11:43:01 +08:00
jxxghp
03e0118fb7 add linux frozen build 2023-11-17 11:39:03 +08:00
jxxghp
c7c222b357 v1.4.3 2023-11-17 11:12:52 +08:00
jxxghp
2d8e45cd1b feat TG消息重试 2023-11-17 10:41:14 +08:00
jxxghp
e2bd5cc245 更新 resource.py 2023-11-16 21:10:21 +08:00
jxxghp
47ddfec30e feat 资源包自动更新 2023-11-16 20:57:41 +08:00
jxxghp
9344b2a324 add AGSVPT 2023-11-16 17:27:58 +08:00
jxxghp
8496fcccc5 Merge pull request #1133 from honue/main 2023-11-16 14:59:50 +08:00
honue
c6fa3b9d25 fix torrent_cache ttl 2023-11-16 13:34:23 +08:00
jxxghp
ce13987748 - 修复Windows打包 2023-11-15 08:20:57 +08:00
jxxghp
5221fc4f6a fix #1125 2023-11-15 08:08:46 +08:00
jxxghp
d4dc388d3f fix search 2023-11-14 17:54:41 +08:00
jxxghp
42966c2537 fix #1122 2023-11-14 17:35:31 +08:00
jxxghp
7921dcd86b Merge pull request #1122 from thsrite/main 2023-11-14 17:30:52 +08:00
thsrite
4c69bb6c48 fix 2023-11-14 16:22:04 +08:00
thsrite
0aad809c82 feat 增加GITHUB_TOKEN,提高API限流阈值 2023-11-14 16:12:02 +08:00
thsrite
f514a5a416 fix 793a7460 2023-11-14 14:17:14 +08:00
thsrite
793a7460c6 fix 动漫电影 剧场版 2023-11-14 14:10:48 +08:00
jxxghp
6dcc979fd5 fix #1072
fix #1118
2023-11-14 08:19:57 +08:00
jxxghp
5a07732712 fix 豆瓣已订阅显示 2023-11-13 20:25:57 +08:00
jxxghp
61d71b32ff fix 豆瓣已订阅显示 2023-11-13 20:22:51 +08:00
jxxghp
ba62ca3d18 - 文件整理支持仅保留最新版本文件(转移覆盖模式设为:latest
- 重启时会识别用户已安装过但本地不存在的插件并自动下载安装,避免升级重置后第三方插件消失的问题
2023-11-13 12:11:43 +08:00
jxxghp
612271bf0c fix 缺失状态识判 2023-11-13 12:00:53 +08:00
jxxghp
3b99fb5c96 fix #1109 2023-11-12 19:10:46 +08:00
jxxghp
bb61f8197c fix #1110 2023-11-12 19:01:25 +08:00
jxxghp
b54f04a35b fix #1110 2023-11-12 18:51:24 +08:00
jxxghp
d47639bada fix #1110 2023-11-12 18:51:08 +08:00
jxxghp
ae9bab2981 Merge pull request #1110 from thsrite/update
fix 重启或重置后三方插件丢失问题
2023-11-12 18:44:45 +08:00
thsrite
2116b094ad fix 三方插件丢失问题 2023-11-11 23:40:39 -06:00
jxxghp
288883a13b feat 文件整理支持仅保留最新版本 2023-11-12 08:03:15 +08:00
jxxghp
07c988abae fix transfer message 2023-11-12 07:21:57 +08:00
jxxghp
fd4a3b5671 fix bug 2023-11-11 15:23:10 +08:00
jxxghp
71adfad94d fix bug 2023-11-11 14:57:38 +08:00
jxxghp
7faaaf3dcd fix bug 2023-11-11 14:14:09 +08:00
jxxghp
25e7db5ac9 fix seerr api 2023-11-11 12:20:27 +08:00
jxxghp
07bd5f1926 fix seerr api 2023-11-11 12:16:45 +08:00
jxxghp
9439d02351 fix TMDB缓存None的问题 2023-11-11 10:37:33 +08:00
jxxghp
cbea7ccdf6 fix TMDB缓存None的问题 2023-11-11 10:34:49 +08:00
jxxghp
93661dfde4 v1.4.1 2023-11-10 22:23:13 +08:00
jxxghp
b98f5351cf fix #1098 2023-11-10 21:56:38 +08:00
jxxghp
83a7261fcd fix #1101 2023-11-10 21:54:45 +08:00
jxxghp
daa2b7a8cd feat 部分API支持api token访问 2023-11-10 21:40:24 +08:00
jxxghp
d245fedb3f fix api token 2023-11-10 20:51:09 +08:00
jxxghp
b0fee2cb3c add token verify 2023-11-10 17:31:19 +08:00
jxxghp
9a102056d8 fix douban rank 2023-11-10 12:23:03 +08:00
jxxghp
3905463940 fix douban mode 2023-11-09 23:44:08 +08:00
jxxghp
746fde592d fix douban mode 2023-11-09 23:22:38 +08:00
jxxghp
3e5f5554da fix bug 2023-11-09 21:47:22 +08:00
jxxghp
01fb6e8772 fix bug 2023-11-09 21:15:36 +08:00
jxxghp
b7448232e6 fix bug 2023-11-09 19:56:56 +08:00
jxxghp
05f1a24199 feat 支持豆瓣做为识别源 2023-11-09 17:32:26 +08:00
jxxghp
4072799c13 fix #1064 2023-11-06 11:29:43 +08:00
jxxghp
9744032f93 Merge remote-tracking branch 'origin/main' 2023-11-06 08:14:13 +08:00
jxxghp
eb9a92d76d v1.4.0 2023-11-06 08:06:57 +08:00
jxxghp
89a4932823 fix plugin get_state 2023-11-05 21:20:27 +08:00
jxxghp
cef06a8894 fix message event 2023-11-05 08:41:45 +08:00
jxxghp
c741edffb0 fix 2023-11-04 07:52:48 +08:00
jxxghp
e7c543fcb9 feat 第三方插件支持依赖 2023-11-04 07:31:14 +08:00
jxxghp
2a61720b0a Merge pull request #1052 from DDS-Derek/main 2023-11-03 19:00:06 +08:00
DDSRem
73484647ba feat: optimize restart update 2023-11-03 18:33:53 +08:00
jxxghp
c9d461f8c8 更新 update 2023-11-03 12:46:29 +08:00
jxxghp
9bdc056359 fix time 2023-11-03 12:29:38 +08:00
jxxghp
6a8a1e799d fix 同步用户提权Bug 2023-11-03 12:19:05 +08:00
86 changed files with 2480 additions and 1029 deletions

View File

@@ -110,7 +110,7 @@ jobs:
- name: Pyinstaller
run: |
pyinstaller windows.spec
pyinstaller frozen.spec
shell: pwsh
- name: Upload Windows File
@@ -119,10 +119,54 @@ jobs:
name: windows
path: dist/MoviePilot.exe
Linux-build-amd64:
runs-on: ubuntu-latest
name: Build Linux Amd64
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
- name: Prepare Frontend
run: |
wget https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip
unzip main.zip
mv MoviePilot-Plugins-main/plugins/* app/plugins/
rm main.zip
rm -rf MoviePilot-Plugins-main
wget https://github.com/jxxghp/MoviePilot-Resources/archive/refs/heads/main.zip
unzip main.zip
mv MoviePilot-Resources-main/resources/* app/helper/
rm main.zip
rm -rf MoviePilot-Resources-main
- name: Pyinstaller
run: |
pyinstaller frozen.spec
mv dist/MoviePilot dist/MoviePilot_Amd64
- name: Upload Linux File
uses: actions/upload-artifact@v3
with:
name: linux-amd64
path: dist/MoviePilot_Amd64
Create-release:
permissions: write-all
runs-on: ubuntu-latest
needs: [ Windows-build, Docker-build ]
needs: [ Windows-build, Docker-build, Linux-build-amd64]
steps:
- uses: actions/checkout@v2
@@ -139,7 +183,8 @@ jobs:
shell: bash
run: |
mkdir releases
mv ./windows/MoviePilot.exe ./releases/MoviePilot_v${{ env.app_version }}.exe
mv ./windows/MoviePilot.exe ./releases/MoviePilot_Win_v${{ env.app_version }}.exe
mv ./linux-amd64/MoviePilot_Amd64 ./releases/MoviePilot_Amd64_v${{ env.app_version }}
- name: Create Release
id: create_release

View File

@@ -11,8 +11,7 @@ ENV LANG="C.UTF-8" \
PORT=3001 \
NGINX_PORT=3000 \
PROXY_HOST="" \
MOVIEPILOT_AUTO_UPDATE=true \
MOVIEPILOT_AUTO_UPDATE_DEV=false \
MOVIEPILOT_AUTO_UPDATE=release \
AUTH_SITE="iyuu" \
IYUU_SIGN=""
WORKDIR "/app"
@@ -78,12 +77,11 @@ RUN cp -f /app/nginx.conf /etc/nginx/nginx.template.conf \
&& FRONTEND_VERSION=$(curl -sL "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest" | jq -r .tag_name) \
&& curl -sL "https://github.com/jxxghp/MoviePilot-Frontend/releases/download/${FRONTEND_VERSION}/dist.zip" | busybox unzip -d / - \
&& mv /dist /public \
&& curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d / - \
&& mv -f /MoviePilot-Plugins-main/plugins/* /app/app/plugins/ \
&& rm -rf /MoviePilot-Plugins-main \
&& curl -sL "https://github.com/jxxghp/MoviePilot-Resources/archive/refs/heads/main.zip" | busybox unzip -d / - \
&& mv -f /MoviePilot-Resources-main/resources/* /app/app/helper/ \
&& rm -rf /MoviePilot-Resources-main
&& curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d /tmp - \
&& mv -f /tmp/MoviePilot-Plugins-main/plugins/* /app/app/plugins/ \
&& curl -sL "https://github.com/jxxghp/MoviePilot-Resources/archive/refs/heads/main.zip" | busybox unzip -d /tmp - \
&& mv -f /tmp/MoviePilot-Resources-main/resources/* /app/app/helper/ \
&& rm -rf /tmp/*
EXPOSE 3000
VOLUME [ "/config" ]
ENTRYPOINT [ "/entrypoint" ]

208
README.md
View File

@@ -7,7 +7,7 @@
发布频道https://t.me/moviepilot_channel
## 主要特性
- 前后端分离基于FastApi + Vue3前端项目地址[MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)
- 前后端分离基于FastApi + Vue3前端项目地址[MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend)APIhttp://localhost:3001/docs
- 聚焦核心需求,简化功能和设置,部分设置项可直接使用默认值。
- 重新设计了用户界面,更加美观易用。
@@ -39,11 +39,13 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
docker pull jxxghp/moviepilot:latest
```
- Windows
下载 [MoviePilot.exe](https://github.com/jxxghp/MoviePilot/releases),双击运行后自动生成配置文件目录
下载 [MoviePilot.exe](https://github.com/jxxghp/MoviePilot/releases),双击运行后自动生成配置文件目录访问http://localhost:3000
- 群晖套件
添加套件源https://spk7.imnks.com/
- 本地运行
@@ -51,46 +53,73 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
2) 将工程 [MoviePilot-Resources](https://github.com/jxxghp/MoviePilot-Resources) resources目录下的所有文件复制到`app/helper`目录
3) 执行命令:`pip install -r requirements.txt` 安装依赖
4) 执行命令:`python app/main.py` 启动服务
5) 根据前端项目 [MoviePilot-Frontend](https://github.com/jxxghp/MoviePilot-Frontend) 说明,启动前端服务
## 配置
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
- 在Docker环境变量部分或Windows系统环境变量中进行参数配置如未自动显示配置项则需要手动增加对应环境变量。
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 配置文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,**部分参数如路径映射、站点认证、权限端口、时区等必须通过环境变量进行配置**。
> ❗号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
### 1. **基础设置**
### 1. **环境变量**
- **❗NGINX_PORT** WEB服务端口默认`3000`可自行修改不能与API服务端口冲突(仅支持环境变量配置)
- **❗PORT** API服务端口默认`3001`可自行修改不能与WEB服务端口冲突(仅支持环境变量配置)
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
- **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** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
- **❗SUPERUSER_PASSWORD** 超级管理员初始密码,默认`password`,建议修改为复杂密码
- **❗NGINX_PORT** WEB服务端口默认`3000`可自行修改不能与API服务端口冲突
- **❗PORT** API服务端口默认`3001`可自行修改不能与WEB服务端口冲突
- **PUID**:运行程序用户的`uid`,默认`0`
- **PGID**:运行程序用户的`gid`,默认`0`
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`
- **PROXY_HOST** 网络代理访问themoviedb或者重启更新需要使用代理访问格式为`http(s)://ip:port`、`socks5://user:pass@host:port`
- **MOVIEPILOT_AUTO_UPDATE** 重启时自动更新,`true`/`release`/`dev`/`false`,默认`release`需要能正常连接Github **注意:如果出现网络问题可以配置`PROXY_HOST`**
- **AUTO_UPDATE_RESOURCE**:启动时自动检测和更新资源包(站点索引及认证等),`true`/`false`,默认`true`需要能正常连接Github仅支持Docker
- **❗AUTH_SITE** 认证站点(认证通过后才能使用站点相关功能),支持配置多个认证站点,使用`,`分隔,如:`iyuu,hhclub`,会依次执行认证操作,直到有一个站点认证成功。
配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数,认证资源`v1.0.2`支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`/`ptvicomo`/`agsvpt`
| 站点 | 参数 |
|:------------:|:-----------------------------------------------------:|
| iyuu | `IYUU_SIGN`IYUU登录令牌 |
| hhclub | `HHCLUB_USERNAME`:用户名<br/>`HHCLUB_PASSKEY`:密钥 |
| audiences | `AUDIENCES_UID`用户ID<br/>`AUDIENCES_PASSKEY`:密钥 |
| hddolby | `HDDOLBY_ID`用户ID<br/>`HDDOLBY_PASSKEY`:密钥 |
| zmpt | `ZMPT_UID`用户ID<br/>`ZMPT_PASSKEY`:密钥 |
| freefarm | `FREEFARM_UID`用户ID<br/>`FREEFARM_PASSKEY`:密钥 |
| hdfans | `HDFANS_UID`用户ID<br/>`HDFANS_PASSKEY`:密钥 |
| wintersakura | `WINTERSAKURA_UID`用户ID<br/>`WINTERSAKURA_PASSKEY`:密钥 |
| leaves | `LEAVES_UID`用户ID<br/>`LEAVES_PASSKEY`:密钥 |
| 1ptba | `1PTBA_UID`用户ID<br/>`1PTBA_PASSKEY`:密钥 |
| icc2022 | `ICC2022_UID`用户ID<br/>`ICC2022_PASSKEY`:密钥 |
| ptlsp | `PTLSP_UID`用户ID<br/>`PTLSP_PASSKEY`:密钥 |
| xingtan | `XINGTAN_UID`用户ID<br/>`XINGTAN_PASSKEY`:密钥 |
| ptvicomo | `PTVICOMO_UID`用户ID<br/>`PTVICOMO_PASSKEY`:密钥 |
| agsvpt | `AGSVPT_UID`用户ID<br/>`AGSVPT_PASSKEY`:密钥 |
### 2. **app.env配置文件**
下载 [app.env 模板](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env)修改后放配置文件目录下app.env 的所有配置项也可以通过环境变量进行配置。
- **❗SUPERUSER** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面,**注意:启动一次后再次修改该值不会生效,除非删除数据库文件!**
- **❗SUPERUSER_PASSWORD** 超级管理员初始密码,默认`password`,建议修改为复杂密码,**注意:启动一次后再次修改该值不会生效,除非删除数据库文件!**
- **❗API_TOKEN** API密钥默认`moviepilot`在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
- **TMDB_API_DOMAIN** TMDB API地址,默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
- **BIG_MEMORY_MODE** 大内存模式,默认`false`,开启后会增加缓存数量,占用更多的内存,但响应速度会更快
- **GITHUB_TOKEN** Github token提高自动更新、插件安装等请求Github Api的限流阈值格式ghp_****
---
- **TMDB_API_DOMAIN** TMDB API地址默认`api.themoviedb.org`,也可配置为`api.tmdb.org`、`tmdb.movie-pilot.org` 或其它中转代理服务地址,能连通即可
- **TMDB_IMAGE_DOMAIN** TMDB图片地址默认`image.tmdb.org`可配置为其它中转代理以加速TMDB图片显示`static-mdb.v.geilijiasu.com`
- **WALLPAPER** 登录首页电影海报,`tmdb`/`bing`,默认`tmdb`
- **RECOGNIZE_SOURCE** 媒体信息识别来源,`themoviedb`/`douban`,默认`themoviedb`,使用`douban`时不支持二级分类
---
- **SCRAP_METADATA** 刮削入库的媒体文件,`true`/`false`,默认`true`
- **SCRAP_SOURCE** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
- **SCRAP_FOLLOW_TMDB** 新增已入库媒体是否跟随TMDB信息变化`true`/`false`,默认`true`
- **SCRAP_FOLLOW_TMDB** 新增已入库媒体是否跟随TMDB信息变化`true`/`false`,默认`true`,为`false`时即使TMDB信息变化了也会仍然按历史记录中已入库的信息进行刮削
---
- **❗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) 自动在媒体库目录下建立二级目录分类
- **❗TRANSFER_TYPE** 整理转移方式,支持`link`/`copy`/`move`/`softlink`/`rclone_copy`/`rclone_move` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响rclone需要自行映射rclone配置目录到容器中或在容器内完成rclone配置节点名称必须为`MP`**
- **OVERWRITE_MODE** 转移覆盖模式,默认为`size`,支持`nerver`/`size`/`always`/`latest`,分别表示`不覆盖同名文件`/`同名文件根据文件大小覆盖(大覆盖小)`/`总是覆盖同名文件`/`仅保留最新版本,删除旧版本文件(包括非同名文件)`
---
- **❗COOKIECLOUD_HOST** CookieCloud服务器地址格式`http(s)://ip:port`,不配置默认使用内建服务器`https://movie-pilot.org/cookiecloud`
- **❗COOKIECLOUD_KEY** CookieCloud用户KEY
@@ -101,11 +130,9 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- **SUBSCRIBE_MODE** 订阅模式,`rss`/`spider`,默认`spider``rss`模式通过定时刷新RSS来匹配订阅RSS地址会自动获取也可手动维护对站点压力小同时可设置订阅刷新周期24小时运行但订阅和下载通知不能过滤和显示免费推荐使用rss模式。
- **SUBSCRIBE_RSS_INTERVAL** RSS订阅模式刷新时间间隔分钟默认`30`分钟不能小于5分钟。
- **SUBSCRIBE_SEARCH** 订阅搜索,`true`/`false`,默认`false`开启后会每隔24小时对所有订阅进行全量搜索以补齐缺失剧集一般情况下正常订阅即可订阅搜索只做为兜底会增加站点压力不建议开启
- **SEARCH_SOURCE** 媒体信息搜索来源,`themoviedb`/`douban`,默认`themoviedb`
- **AUTO_DOWNLOAD_USER** 远程交互搜索时自动择优下载的用户ID多个用户使用,分割,未设置需要选择资源或者回复`0`
- **AUTO_DOWNLOAD_USER** 远程交互搜索时自动择优下载的用户ID消息通知渠道的用户ID多个用户使用,分割,未设置需要选择资源或者回复`0`
---
- **OCR_HOST** OCR识别服务器地址格式`http(s)://ip:port`用于识别站点验证码实现自动登录获取Cookie等不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
- **PLUGIN_MARKET** 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/,默认为官方插件仓库:`https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/`。
---
- **❗MESSAGER** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
@@ -136,7 +163,6 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- **SYNOLOGYCHAT_WEBHOOK** 在Synology Chat中创建机器人获取机器人`传入URL`
- **SYNOLOGYCHAT_TOKEN** SynologyChat机器人`令牌`
---
- **❗DOWNLOAD_PATH** 下载保存目录,**注意:需要将`moviepilot`及`下载器`的映射路径保持一致**,否则会导致下载文件无法转移
- **DOWNLOAD_MOVIE_PATH** 电影下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
@@ -144,8 +170,7 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- **DOWNLOAD_ANIME_PATH** 动漫下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
- **DOWNLOAD_CATEGORY** 下载二级分类开关,`true`/`false`,默认`false`,开启后会根据配置 [category.yaml](https://github.com/jxxghp/MoviePilot/raw/main/config/category.yaml) 自动在下载目录下建立二级目录分类
- **DOWNLOAD_SUBTITLE** 下载站点字幕,`true`/`false`,默认`true`
- **DOWNLOADER_MONITOR** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
- **TORRENT_TAG** 下载器种子标签,默认为`MOVIEPILOT`设置后只有MoviePilot添加的下载才会处理留空所有下载器中的任务均会处理
---
- **❗DOWNLOADER** 下载器,支持`qbittorrent`/`transmission`QB版本号要求>= 4.3.9TR版本号要求>= 3.0,同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`qbittorrent`
- `qbittorrent`设置项:
@@ -162,7 +187,8 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- **TR_HOST** transmission地址格式`ip:port`https需要添加`https://`前缀
- **TR_USER** transmission用户名
- **TR_PASSWORD** transmission密码
- **DOWNLOADER_MONITOR** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
- **TORRENT_TAG** 下载器种子标签,默认为`MOVIEPILOT`设置后只有MoviePilot添加的下载才会处理留空所有下载器中的任务均会处理
---
- **❗MEDIASERVER** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
@@ -180,87 +206,55 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- **PLEX_HOST** Plex服务器地址格式`ip:port`https需要添加`https://`前缀
- **PLEX_TOKEN** Plex网页Url中的`X-Plex-Token`通过浏览器F12->网络从请求URL中获取
- **MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步
- **MEDIASERVER_SYNC_BLACKLIST:** 媒体服务器同步黑名单,多个媒体库名称使用,分割
---
- **MOVIE_RENAME_FORMAT** 电影重命名格式基于jinjia2语法
`MOVIE_RENAME_FORMAT`支持的配置项:
### 2. **用户认证**
> `title` TMDB/豆瓣中的标题
> `original_title` TMDB/豆瓣中的原语种标题
> `name` 从文件名中识别的名称(同时存在中英文时,优先使用中文)
> `en_name`:从文件名中识别的英文名称(可能为空)
> `original_name` 原文件名(包括文件外缀)
> `year` 年份
> `resourceType`:资源类型
> `effect`:特效
> `edition` 版本(资源类型+特效)
> `videoFormat` 分辨率
> `releaseGroup` 制作组/字幕组
> `customization` 自定义占位符
> `videoCodec` 视频编码
> `audioCodec` 音频编码
> `tmdbid` TMDB ID非TMDB识别源时为空
> `imdbid` IMDB ID可能为空
> `doubanid`豆瓣ID非豆瓣识别源时为空
> `part`:段/节
> `fileExt`:文件扩展名
> `customization`:自定义占位符
`MOVIE_RENAME_FORMAT`默认配置格式:
```
{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}
```
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过环境变量配置**
`AUTH_SITE`支持配置多个认证站点,使用`,`分隔,如:`iyuu,hhclub`,会依次执行认证操作,直到有一个站点认证成功。
- **❗AUTH_SITE** 认证站点,认证资源`v1.0.1`支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`/`ptvicomo`
| 站点 | 参数 |
|:------------:|:-----------------------------------------------------:|
| iyuu | `IYUU_SIGN`IYUU登录令牌 |
| hhclub | `HHCLUB_USERNAME`:用户名<br/>`HHCLUB_PASSKEY`:密钥 |
| audiences | `AUDIENCES_UID`用户ID<br/>`AUDIENCES_PASSKEY`:密钥 |
| hddolby | `HDDOLBY_ID`用户ID<br/>`HDDOLBY_PASSKEY`:密钥 |
| zmpt | `ZMPT_UID`用户ID<br/>`ZMPT_PASSKEY`:密钥 |
| freefarm | `FREEFARM_UID`用户ID<br/>`FREEFARM_PASSKEY`:密钥 |
| hdfans | `HDFANS_UID`用户ID<br/>`HDFANS_PASSKEY`:密钥 |
| wintersakura | `WINTERSAKURA_UID`用户ID<br/>`WINTERSAKURA_PASSKEY`:密钥 |
| leaves | `LEAVES_UID`用户ID<br/>`LEAVES_PASSKEY`:密钥 |
| 1ptba | `1PTBA_UID`用户ID<br/>`1PTBA_PASSKEY`:密钥 |
| icc2022 | `ICC2022_UID`用户ID<br/>`ICC2022_PASSKEY`:密钥 |
| ptlsp | `PTLSP_UID`用户ID<br/>`PTLSP_PASSKEY`:密钥 |
| xingtan | `XINGTAN_UID`用户ID<br/>`XINGTAN_PASSKEY`:密钥 |
| ptvicomo | `PTVICOMO_UID`用户ID<br/>`PTVICOMO_PASSKEY`:密钥 |
### 2. **进阶配置**
- **BIG_MEMORY_MODE** 大内存模式,默认为`false`,开启后会占用更多的内存,但响应速度会更快
- **MOVIE_RENAME_FORMAT** 电影重命名格式
`MOVIE_RENAME_FORMAT`支持的配置项:
> `title` 标题
> `original_name` 原文件名
> `original_title` 原语种标题
> `name` 识别名称
> `year` 年份
> `resourceType`:资源类型
> `effect`:特效
> `edition` 版本(资源类型+特效)
> `videoFormat` 分辨率
> `releaseGroup` 制作组/字幕组
> `customization` 自定义占位符
> `videoCodec` 视频编码
> `audioCodec` 音频编码
> `tmdbid` TMDBID
> `imdbid` IMDBID
> `part`:段/节
> `fileExt`:文件扩展名
> `tmdbid`TMDB ID
> `imdbid`IMDB ID
> `customization`:自定义占位符
`MOVIE_RENAME_FORMAT`默认配置格式:
```
{{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}
```
- **TV_RENAME_FORMAT** 电视剧重命名格式
`TV_RENAME_FORMAT`额外支持的配置项:
> `season` 季号
> `episode` 集号
> `season_episode` 季集 SxxExx
> `episode_title` 集标题
`TV_RENAME_FORMAT`默认配置格式:
```
{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}
```
- **TV_RENAME_FORMAT** 电视剧重命名格式基于jinjia2语法
`TV_RENAME_FORMAT`额外支持的配置项:
> `season` 季号
> `episode` 集号
> `season_episode` 季集 SxxExx
> `episode_title` 集标题
`TV_RENAME_FORMAT`默认配置格式:
```
{{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}
```
### 3. **优先级规则**
@@ -268,6 +262,10 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- 符合任一层级规则的资源将被标识选中,匹配成功的层级做为该资源的优先级,排越前面优先级超高
- 不符合过滤规则所有层级规则的资源将不会被选中
### 4. **插件扩展**
- **PLUGIN_MARKET** 插件市场仓库地址仅支持Github仓库`main`分支,多个地址使用`,`分隔,默认为官方插件仓库:`https://github.com/jxxghp/MoviePilot-Plugins` ,通过查看[MoviePilot-Plugins](https://github.com/jxxghp/MoviePilot-Plugins)项目的fork或者查看频道置顶了解更多第三方插件仓库。
## 使用

View File

@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from app import schemas
from app.chain.dashboard import DashboardChain
from app.core.config import settings
from app.core.security import verify_token
from app.core.security import verify_token, verify_uri_token
from app.db import get_db
from app.db.models.transferhistory import TransferHistory
from app.scheduler import Scheduler
@@ -34,6 +34,14 @@ def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return schemas.Statistic()
@router.get("/statistic2", summary="媒体数量统计API_TOKEN", response_model=schemas.Statistic)
def statistic2(_: str = Depends(verify_uri_token)) -> Any:
"""
查询媒体数量统计信息 API_TOKEN认证?token=xxx
"""
return statistic()
@router.get("/storage", summary="存储空间", response_model=schemas.Storage)
def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -46,6 +54,14 @@ def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
)
@router.get("/storage2", summary="存储空间API_TOKEN", response_model=schemas.Storage)
def storage2(_: str = Depends(verify_uri_token)) -> Any:
"""
查询存储空间信息 API_TOKEN认证?token=xxx
"""
return storage()
@router.get("/processes", summary="进程信息", response_model=List[schemas.ProcessInfo])
def processes(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -73,6 +89,14 @@ def downloader(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return schemas.DownloaderInfo()
@router.get("/downloader2", summary="下载器信息API_TOKEN", response_model=schemas.DownloaderInfo)
def downloader2(_: str = Depends(verify_uri_token)) -> Any:
"""
查询下载器信息 API_TOKEN认证?token=xxx
"""
return downloader()
@router.get("/schedule", summary="后台服务", response_model=List[schemas.ScheduleInfo])
def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
@@ -81,6 +105,14 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return Scheduler().list()
@router.get("/schedule2", summary="后台服务API_TOKEN", response_model=List[schemas.ScheduleInfo])
def schedule2(_: str = Depends(verify_uri_token)) -> Any:
"""
查询下载器信息 API_TOKEN认证?token=xxx
"""
return schedule()
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
def transfer(days: int = 7, db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@@ -99,9 +131,25 @@ def cpu(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
return SystemUtils.cpu_usage()
@router.get("/cpu2", summary="获取当前CPU使用率API_TOKEN", response_model=int)
def cpu2(_: str = Depends(verify_uri_token)) -> Any:
"""
获取当前CPU使用率 API_TOKEN认证?token=xxx
"""
return cpu()
@router.get("/memory", summary="获取当前内存使用量和使用率", response_model=List[int])
def memory(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取当前内存使用率
"""
return SystemUtils.memory_usage()
@router.get("/memory2", summary="获取当前内存使用量和使用率API_TOKEN", response_model=List[int])
def memory2(_: str = Depends(verify_uri_token)) -> Any:
"""
获取当前内存使用率 API_TOKEN认证?token=xxx
"""
return memory()

View File

@@ -28,20 +28,6 @@ def douban_img(imgurl: str) -> Any:
return None
@router.get("/recognize/{doubanid}", summary="豆瓣ID识别", response_model=schemas.Context)
def recognize_doubanid(doubanid: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID识别媒体信息
"""
# 识别媒体信息
context = DoubanChain().recognize_by_doubanid(doubanid=doubanid)
if context:
return context.to_dict()
else:
return schemas.Context()
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
def movie_showing(page: int = 1,
count: int = 30,
@@ -141,6 +127,69 @@ def tv_animation(page: int = 1,
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
def movie_hot(page: int = 1,
count: int = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门电影
"""
movies = DoubanChain().movie_hot(page=page, count=count)
return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
@router.get("/tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
def tv_hot(page: int = 1,
count: int = 30,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
热门电视剧
"""
tvs = DoubanChain().tv_hot(page=page, count=count)
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
@router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.DoubanPerson])
def douban_credits(doubanid: str,
type_name: str,
page: int = 1,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID查询演员阵容type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
doubaninfos = DoubanChain().movie_credits(doubanid=doubanid, page=page)
elif mediatype == MediaType.TV:
doubaninfos = DoubanChain().tv_credits(doubanid=doubanid, page=page)
else:
return []
if not doubaninfos:
return []
else:
return [schemas.DoubanPerson(**doubaninfo) for doubaninfo in doubaninfos]
@router.get("/recommend/{doubanid}/{type_name}", summary="豆瓣推荐电影/电视剧", response_model=List[schemas.MediaInfo])
def douban_recommend(doubanid: str,
type_name: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据豆瓣ID查询推荐电影/电视剧type_name: 电影/电视剧
"""
mediatype = MediaType(type_name)
if mediatype == MediaType.MOVIE:
doubaninfos = DoubanChain().movie_recommend(doubanid=doubanid)
elif mediatype == MediaType.TV:
doubaninfos = DoubanChain().tv_recommend(doubanid=doubanid)
else:
return []
if not doubaninfos:
return []
else:
return [MediaInfo(douban_info=doubaninfo).to_dict() for doubaninfo in doubaninfos]
@router.get("/{doubanid}", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
def douban_info(doubanid: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:

View File

@@ -3,14 +3,13 @@ from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
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.models.user import User
from app.db.userauth import get_current_active_user
from app.schemas import NotExistMediaInfo, MediaType
router = APIRouter()
@@ -61,30 +60,31 @@ def exists(media_in: schemas.MediaInfo,
查询缺失媒体信息
"""
# 媒体信息
mediainfo = MediaInfo()
meta = MetaInfo(title=media_in.title)
if media_in.tmdb_id:
mediainfo.from_dict(media_in.dict())
elif media_in.douban_id:
context = DoubanChain().recognize_by_doubanid(doubanid=media_in.douban_id)
if context:
mediainfo = context.media_info
meta = context.meta_info
mtype = MediaType(media_in.type) if media_in.type else None
if mtype:
meta.type = mtype
if media_in.season:
meta.begin_season = media_in.season
meta.type = MediaType.TV
if media_in.year:
meta.year = media_in.year
if media_in.tmdb_id or media_in.douban_id:
mediainfo = MediaChain().recognize_media(meta=meta, mtype=mtype,
tmdbid=media_in.tmdb_id, doubanid=media_in.douban_id)
else:
context = MediaChain().recognize_by_title(title=f"{media_in.title} {media_in.year}")
if context:
mediainfo = context.media_info
meta = context.meta_info
mediainfo = MediaChain().recognize_by_meta(metainfo=meta)
# 查询缺失信息
if not mediainfo or not mediainfo.tmdb_id:
if not mediainfo:
raise HTTPException(status_code=404, detail="媒体信息不存在")
mediakey = mediainfo.tmdb_id or mediainfo.douban_id
exist_flag, no_exists = DownloadChain().get_no_exists_info(meta=meta, mediainfo=mediainfo)
if mediainfo.type == MediaType.MOVIE:
# 电影已存在时返回空列表,存在时返回空对像列表
return [] if exist_flag else [NotExistMediaInfo()]
elif no_exists and no_exists.get(mediainfo.tmdb_id):
elif no_exists and no_exists.get(mediakey):
# 电视剧返回缺失的剧集
return list(no_exists.get(mediainfo.tmdb_id).values())
return list(no_exists.get(mediakey).values())
return []
@@ -104,7 +104,7 @@ def stop_downloading(
hashString: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
控制下载任务
暂停下载任务
"""
ret = DownloadChain().set_downloading(hashString, "stop")
return schemas.Response(success=True if ret else False)
@@ -115,7 +115,7 @@ def remove_downloading(
hashString: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
控制下载任务
删除下载任务
"""
ret = DownloadChain().remove_downloading(hashString)
return schemas.Response(success=True if ret else False)

View File

@@ -37,18 +37,23 @@ async def login_access_token(
logger.warn("登录用户本地不匹配,尝试辅助认证 ...")
token = UserChain().user_authenticate(form_data.username, form_data.password)
if not token:
logger.warn(f"用户 {form_data.username} 登录失败!")
raise HTTPException(status_code=401, detail="用户名或密码不正确")
else:
logger.info(f"辅助认证成功,用户信息: {token}")
logger.info(f"用户 {form_data.username} 辅助认证成功,用户信息: {token}")
# 加入用户信息表
user = User.get_by_name(db=db, name=form_data.username)
if not user:
logger.info(f"用户不存在,创建用户: {form_data.username}")
logger.info(f"用户不存在,创建普通用户: {form_data.username}")
user = User(name=form_data.username, is_active=True,
is_superuser=False, hashed_password=get_password_hash(token))
user.create(db)
else:
# 普通用户权限
user.is_superuser = False
elif not user.is_active:
raise HTTPException(status_code=403, detail="用户未启用")
logger.info(f"用户 {user.name} 登录成功!")
return schemas.Token(
access_token=security.create_access_token(
user.id,

View File

@@ -4,12 +4,11 @@ from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas
from app.chain.douban import DoubanChain
from app.chain.media import MediaChain
from app.chain.tmdb import TmdbChain
from app.core.context import MediaInfo
from app.core.config import settings
from app.core.context import Context
from app.core.metainfo import MetaInfo
from app.core.security import verify_token
from app.core.security import verify_token, verify_uri_token
from app.db import get_db
from app.db.mediaserver_oper import MediaServerOper
from app.schemas import MediaType
@@ -25,15 +24,27 @@ def recognize(title: str,
根据标题、副标题识别媒体信息
"""
# 识别媒体信息
context = MediaChain().recognize_by_title(title=title, subtitle=subtitle)
if context:
return context.to_dict()
metainfo = MetaInfo(title, subtitle)
mediainfo = MediaChain().recognize_by_meta(metainfo)
if mediainfo:
return Context(meta_info=metainfo, media_info=mediainfo).to_dict()
return schemas.Context()
@router.get("/recognize2", summary="识别种子媒体信息API_TOKEN", response_model=schemas.Context)
def recognize2(title: str,
subtitle: str = None,
_: str = Depends(verify_uri_token)) -> Any:
"""
根据标题、副标题识别媒体信息 API_TOKEN认证?token=xxx
"""
# 识别媒体信息
return recognize(title, subtitle)
@router.get("/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context)
def recognize(path: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def recognize_file(path: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据文件路径识别媒体信息
"""
@@ -44,6 +55,16 @@ def recognize(path: str,
return schemas.Context()
@router.get("/recognize_file2", summary="识别文件媒体信息API_TOKEN", response_model=schemas.Context)
def recognize_file2(path: str,
_: str = Depends(verify_uri_token)) -> Any:
"""
根据文件路径识别媒体信息 API_TOKEN认证?token=xxx
"""
# 识别媒体信息
return recognize_file(path)
@router.get("/search", summary="搜索媒体信息", response_model=List[schemas.MediaInfo])
def search_by_title(title: str,
page: int = 1,
@@ -81,26 +102,35 @@ def exists(title: str = None,
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
def tmdb_info(mediaid: str, type_name: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def media_info(mediaid: str, type_name: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧
"""
mtype = MediaType(type_name)
tmdbid, doubanid = None, None
if mediaid.startswith("tmdb:"):
result = TmdbChain().tmdb_info(int(mediaid[5:]), mtype)
return MediaInfo(tmdb_info=result).to_dict()
tmdbid = int(mediaid[5:])
elif mediaid.startswith("douban:"):
# 查询豆瓣信息
doubaninfo = DoubanChain().douban_info(doubanid=mediaid[7:])
if not doubaninfo:
return schemas.MediaInfo()
result = DoubanChain().recognize_by_doubaninfo(doubaninfo)
if result:
# TMDB
return result.media_info.to_dict()
else:
# 豆瓣
return MediaInfo(douban_info=doubaninfo).to_dict()
else:
doubanid = mediaid[7:]
if not tmdbid and not doubanid:
return schemas.MediaInfo()
if settings.RECOGNIZE_SOURCE == "themoviedb":
if not tmdbid and doubanid:
tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
if tmdbinfo:
tmdbid = tmdbinfo.get("id")
else:
return schemas.MediaInfo()
else:
if not doubanid and tmdbid:
doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype)
if doubaninfo:
doubanid = doubaninfo.get("id")
else:
return schemas.MediaInfo()
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype)
if mediainfo:
MediaChain().obtain_images(mediainfo)
return mediainfo.to_dict()
return schemas.MediaInfo()

View File

@@ -62,7 +62,7 @@ def install_plugin(plugin_id: str,
state, msg = PluginHelper().install(pid=plugin_id, repo_url=repo_url)
if not state:
# 安装失败
return schemas.Response(success=False, msg=msg)
return schemas.Response(success=False, message=msg)
# 安装插件
if plugin_id not in install_plugins:
install_plugins.append(plugin_id)
@@ -89,11 +89,23 @@ def plugin_form(plugin_id: str,
@router.get("/page/{plugin_id}", summary="获取插件数据页面")
def plugin_page(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
"""
根据插件ID获取插件配置信息
根据插件ID获取插件数据页面
"""
return PluginManager().get_plugin_page(plugin_id)
@router.get("/reset/{plugin_id}", summary="重置插件配置", response_model=schemas.Response)
def reset_plugin(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
"""
根据插件ID重置插件配置
"""
# 删除配置
PluginManager().delete_plugin_config(plugin_id)
# 重新生效插件
PluginManager().reload_plugin(plugin_id, {})
return schemas.Response(success=True)
@router.get("/{plugin_id}", summary="获取插件配置")
def plugin_config(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token)) -> dict:
"""
@@ -106,7 +118,7 @@ def plugin_config(plugin_id: str, _: schemas.TokenPayload = Depends(verify_token
def set_plugin_config(plugin_id: str, conf: dict,
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据插件ID获取插件配置信息
更新插件配置
"""
# 保存配置
PluginManager().save_plugin_config(plugin_id, conf)

View File

@@ -3,8 +3,9 @@ from typing import List, Any
from fastapi import APIRouter, Depends
from app import schemas
from app.chain.douban import DoubanChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.core.config import settings
from app.core.security import verify_token
from app.schemas.types import MediaType
@@ -21,27 +22,36 @@ async def search_latest(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@router.get("/media/{mediaid}", summary="精确搜索资源", response_model=List[schemas.Context])
def search_by_tmdbid(mediaid: str,
mtype: str = None,
area: str = "title",
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
def search_by_id(mediaid: str,
mtype: str = None,
area: str = "title",
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID/豆瓣ID精确搜索站点资源 tmdb:/douban:/
"""
torrents = []
if mtype:
mtype = MediaType(mtype)
if mediaid.startswith("tmdb:"):
tmdbid = int(mediaid.replace("tmdb:", ""))
if mtype:
mtype = MediaType(mtype)
torrents = SearchChain().search_by_tmdbid(tmdbid=tmdbid, mtype=mtype, area=area)
if settings.RECOGNIZE_SOURCE == "douban":
# 通过TMDBID识别豆瓣ID
doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype)
if doubaninfo:
torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"),
mtype=mtype, area=area)
else:
torrents = SearchChain().search_by_id(tmdbid=tmdbid, mtype=mtype, area=area)
elif mediaid.startswith("douban:"):
doubanid = mediaid.replace("douban:", "")
# 识别豆瓣信息
context = DoubanChain().recognize_by_doubanid(doubanid)
if not context or not context.media_info or not context.media_info.tmdb_id:
return []
torrents = SearchChain().search_by_tmdbid(tmdbid=context.media_info.tmdb_id,
mtype=context.media_info.type,
area=area)
if settings.RECOGNIZE_SOURCE == "themoviedb":
# 通过豆瓣ID识别TMDBID
tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
if tmdbinfo:
torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"),
mtype=mtype, area=area)
else:
torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=mtype, area=area)
else:
return []
return [torrent.to_dict() for torrent in torrents]

View File

@@ -7,7 +7,8 @@ from sqlalchemy.orm import Session
from app import schemas
from app.chain.subscribe import SubscribeChain
from app.core.config import settings
from app.core.security import verify_token
from app.core.metainfo import MetaInfo
from app.core.security import verify_token, verify_uri_token
from app.db import get_db
from app.db.models.subscribe import Subscribe
from app.db.models.user import User
@@ -27,7 +28,7 @@ def start_subscribe_add(title: str, year: str,
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
@router.get("/", summary="查询所有订阅", response_model=List[schemas.Subscribe])
def read_subscribes(
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
@@ -41,6 +42,14 @@ def read_subscribes(
return subscribes
@router.get("/list", summary="查询所有订阅API_TOKEN", response_model=List[schemas.Subscribe])
def list_subscribes(_: str = Depends(verify_uri_token)) -> Any:
"""
查询所有订阅 API_TOKEN认证?token=xxx
"""
return read_subscribes()
@router.post("/", summary="新增订阅", response_model=schemas.Response)
def create_subscribe(
*,
@@ -55,6 +64,11 @@ def create_subscribe(
mtype = MediaType(subscribe_in.type)
else:
mtype = None
# 豆瓣标理
if subscribe_in.doubanid:
meta = MetaInfo(subscribe_in.name)
subscribe_in.name = meta.name
subscribe_in.season = meta.begin_season
# 标题转换
if subscribe_in.name:
title = subscribe_in.name
@@ -108,23 +122,30 @@ def update_subscribe(
def subscribe_mediaid(
mediaid: str,
season: int = None,
title: str = None,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
根据TMDBID或豆瓣ID查询订阅 tmdb:/douban:
"""
result = None
if mediaid.startswith("tmdb:"):
tmdbid = mediaid[5:]
if not tmdbid or not str(tmdbid).isdigit():
return Subscribe()
result = Subscribe.exists(db, int(tmdbid), season)
result = Subscribe.exists(db, tmdbid=int(tmdbid), season=season)
elif mediaid.startswith("douban:"):
doubanid = mediaid[7:]
if not doubanid:
return Subscribe()
result = Subscribe.get_by_doubanid(db, doubanid)
else:
result = None
if not result and title:
meta = MetaInfo(title)
if season:
meta.begin_season = season
result = Subscribe.get_by_title(db, title=meta.name, season=meta.begin_season)
if result and result.sites:
result.sites = json.loads(result.sites)
@@ -243,7 +264,7 @@ def delete_subscribe(
async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
authorization: str = Header(None)) -> Any:
"""
Jellyseerr/Overseerr订阅
Jellyseerr/Overseerr网络勾子通知订阅
"""
if not authorization or authorization != settings.API_TOKEN:
raise HTTPException(

View File

@@ -1,10 +1,10 @@
import json
import time
from datetime import datetime
from typing import Union
from typing import Union, Any
import tailer
from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, HTTPException, Depends, Response
from fastapi.responses import StreamingResponse
from app import schemas
@@ -24,6 +24,19 @@ from version import APP_VERSION
router = APIRouter()
@router.get("/img/{imgurl:path}", summary="图片代理")
def get_img(imgurl: str) -> Any:
"""
通过图片代理(使用代理服务器)
"""
if not imgurl:
return None
response = RequestUtils(ua=settings.USER_AGENT, proxies=settings.PROXY).get_res(url=imgurl)
if response:
return Response(content=response.content, media_type="image/jpeg")
return None
@router.get("/env", summary="查询系统环境变量", response_model=schemas.Response)
def get_env_setting(_: schemas.TokenPayload = Depends(verify_token)):
"""
@@ -163,7 +176,8 @@ def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
"""
查询Github所有Release版本
"""
version_res = RequestUtils().get_res(f"https://api.github.com/repos/jxxghp/MoviePilot/releases")
version_res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS).get_res(
f"https://api.github.com/repos/jxxghp/MoviePilot/releases")
if version_res:
ver_json = version_res.json()
if ver_json:

View File

@@ -1,10 +1,11 @@
from typing import Any
from fastapi import APIRouter, BackgroundTasks, Request
from fastapi import APIRouter, BackgroundTasks, Request, Depends
from app import schemas
from app.chain.webhook import WebhookChain
from app.core.config import settings
from app.core.security import verify_uri_token
router = APIRouter()
@@ -18,13 +19,12 @@ def start_webhook_chain(body: Any, form: Any, args: Any):
@router.post("/", summary="Webhook消息响应", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request,
request: Request,
_: str = Depends(verify_uri_token)
) -> Any:
"""
Webhook响应
"""
if token != settings.API_TOKEN:
return schemas.Response(success=False, message="token认证不通过")
body = await request.body()
form = await request.form()
args = request.query_params
@@ -34,12 +34,10 @@ async def webhook_message(background_tasks: BackgroundTasks,
@router.get("/", summary="Webhook消息响应", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request) -> Any:
request: Request, _: str = Depends(verify_uri_token)) -> 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, None, None, args)
return schemas.Response(success=True)

View File

@@ -8,6 +8,7 @@ from app.chain.media import MediaChain
from app.chain.subscribe import SubscribeChain
from app.core.config import settings
from app.core.metainfo import MetaInfo
from app.core.security import verify_uri_apikey
from app.db import get_db
from app.db.models.subscribe import Subscribe
from app.schemas import RadarrMovie, SonarrSeries
@@ -18,15 +19,10 @@ arr_router = APIRouter(tags=['servarr'])
@arr_router.get("/system/status", summary="系统状态")
def arr_system_status(apikey: str) -> Any:
def arr_system_status(_: str = Depends(verify_uri_apikey)) -> Any:
"""
模拟Radarr、Sonarr系统状态
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
return {
"appName": "MoviePilot",
"instanceName": "moviepilot",
@@ -77,15 +73,10 @@ def arr_system_status(apikey: str) -> Any:
@arr_router.get("/qualityProfile", summary="质量配置")
def arr_qualityProfile(apikey: str) -> Any:
def arr_qualityProfile(_: str = Depends(verify_uri_apikey)) -> Any:
"""
模拟Radarr、Sonarr质量配置
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
return [
{
"id": 1,
@@ -123,15 +114,10 @@ def arr_qualityProfile(apikey: str) -> Any:
@arr_router.get("/rootfolder", summary="根目录")
def arr_rootfolder(apikey: str) -> Any:
def arr_rootfolder(_: str = Depends(verify_uri_apikey)) -> Any:
"""
模拟Radarr、Sonarr根目录
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
return [
{
"id": 1,
@@ -144,15 +130,10 @@ def arr_rootfolder(apikey: str) -> Any:
@arr_router.get("/tag", summary="标签")
def arr_tag(apikey: str) -> Any:
def arr_tag(_: str = Depends(verify_uri_apikey)) -> Any:
"""
模拟Radarr、Sonarr标签
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
return [
{
"id": 1,
@@ -162,15 +143,10 @@ def arr_tag(apikey: str) -> Any:
@arr_router.get("/languageprofile", summary="语言")
def arr_languageprofile(apikey: str) -> Any:
def arr_languageprofile(_: str = Depends(verify_uri_apikey)) -> Any:
"""
模拟Radarr、Sonarr语言
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
return [{
"id": 1,
"name": "默认",
@@ -193,7 +169,7 @@ def arr_languageprofile(apikey: str) -> Any:
@arr_router.get("/movie", summary="所有订阅电影", response_model=List[schemas.RadarrMovie])
def arr_movies(apikey: str, db: Session = Depends(get_db)) -> Any:
def arr_movies(_: str = Depends(verify_uri_apikey), db: Session = Depends(get_db)) -> Any:
"""
查询Rardar电影
"""
@@ -262,11 +238,6 @@ def arr_movies(apikey: str, db: Session = Depends(get_db)) -> Any:
}
]
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
# 查询所有电影订阅
result = []
subscribes = Subscribe.list(db)
@@ -289,16 +260,11 @@ def arr_movies(apikey: str, db: Session = Depends(get_db)) -> Any:
@arr_router.get("/movie/lookup", summary="查询电影", response_model=List[schemas.RadarrMovie])
def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> Any:
def arr_movie_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
查询Rardar电影 term: `tmdb:${id}`
存在和不存在均不能返回错误
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
tmdbid = term.replace("tmdb:", "")
# 查询媒体信息
mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
@@ -340,15 +306,10 @@ def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> A
@arr_router.get("/movie/{mid}", summary="电影订阅详情", response_model=schemas.RadarrMovie)
def arr_movie(apikey: str, mid: int, db: Session = Depends(get_db)) -> Any:
def arr_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
查询Rardar电影订阅
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
subscribe = Subscribe.get(db, mid)
if subscribe:
return RadarrMovie(
@@ -371,18 +332,13 @@ def arr_movie(apikey: str, mid: int, db: Session = Depends(get_db)) -> Any:
@arr_router.post("/movie", summary="新增电影订阅")
def arr_add_movie(apikey: str,
movie: RadarrMovie,
def arr_add_movie(movie: RadarrMovie,
db: Session = Depends(get_db),
_: str = Depends(verify_uri_apikey)
) -> Any:
"""
新增Rardar电影订阅
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
# 检查订阅是否已存在
subscribe = Subscribe.get_by_tmdbid(db, movie.tmdbId)
if subscribe:
@@ -407,15 +363,10 @@ def arr_add_movie(apikey: str,
@arr_router.delete("/movie/{mid}", summary="删除电影订阅", response_model=schemas.Response)
def arr_remove_movie(apikey: str, mid: int, db: Session = Depends(get_db)) -> Any:
def arr_remove_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
删除Rardar电影订阅
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
subscribe = Subscribe.get(db, mid)
if subscribe:
subscribe.delete(db, mid)
@@ -428,7 +379,7 @@ def arr_remove_movie(apikey: str, mid: int, db: Session = Depends(get_db)) -> An
@arr_router.get("/series", summary="所有剧集", response_model=List[schemas.SonarrSeries])
def arr_series(apikey: str, db: Session = Depends(get_db)) -> Any:
def arr_series(_: str = Depends(verify_uri_apikey), db: Session = Depends(get_db)) -> Any:
"""
查询Sonarr剧集
"""
@@ -534,11 +485,6 @@ def arr_series(apikey: str, db: Session = Depends(get_db)) -> Any:
}
]
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
# 查询所有电视剧订阅
result = []
subscribes = Subscribe.list(db)
@@ -569,16 +515,10 @@ def arr_series(apikey: str, db: Session = Depends(get_db)) -> Any:
@arr_router.get("/series/lookup", summary="查询剧集")
def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> Any:
def arr_series_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
查询Sonarr剧集 term: `tvdb:${id}` title
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
# 获取TVDBID
if not term.startswith("tvdb:"):
mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
@@ -664,15 +604,10 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
@arr_router.get("/series/{tid}", summary="剧集详情")
def arr_serie(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
def arr_serie(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
查询Sonarr剧集
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
subscribe = Subscribe.get(db, tid)
if subscribe:
return SonarrSeries(
@@ -703,16 +638,12 @@ def arr_serie(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
@arr_router.post("/series", summary="新增剧集订阅")
def arr_add_series(apikey: str, tv: schemas.SonarrSeries,
db: Session = Depends(get_db)) -> Any:
def arr_add_series(tv: schemas.SonarrSeries,
db: Session = Depends(get_db),
_: str = Depends(verify_uri_apikey)) -> Any:
"""
新增Sonarr剧集订阅
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
# 检查订阅是否存在
left_seasons = []
for season in tv.seasons:
@@ -751,15 +682,10 @@ def arr_add_series(apikey: str, tv: schemas.SonarrSeries,
@arr_router.delete("/series/{tid}", summary="删除剧集订阅")
def arr_remove_series(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
def arr_remove_series(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_uri_apikey)) -> Any:
"""
删除Sonarr剧集订阅
"""
if not apikey or apikey != settings.API_TOKEN:
raise HTTPException(
status_code=403,
detail="认证失败!",
)
subscribe = Subscribe.get(db, tid)
if subscribe:
subscribe.delete(db, tid)

View File

@@ -107,28 +107,34 @@ class ChainBase(metaclass=ABCMeta):
# 中止继续执行
break
except Exception as err:
logger.error(f"运行模块 {method} 出错:{module.__class__.__name__} - {str(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,
mtype: MediaType = None,
tmdbid: int = None) -> Optional[MediaInfo]:
tmdbid: int = None,
doubanid: str = None) -> Optional[MediaInfo]:
"""
识别媒体信息
:param meta: 识别的元数据
:param mtype: 识别的媒体类型与tmdbid配套
:param tmdbid: tmdbid
:param doubanid: 豆瓣ID
:return: 识别的媒体信息,包括剧集信息
"""
# 识别用名中含指定信息情形
if not mtype and meta and meta.type in [MediaType.TV, MediaType.MOVIE]:
mtype = meta.type
if not tmdbid and hasattr(meta, "tmdbid"):
# 识别用名中含指定信息情形
tmdbid = meta.tmdbid
if not mtype and meta.type in [MediaType.TV, MediaType.MOVIE]:
mtype = meta.type
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
if not doubanid and hasattr(meta, "doubanid"):
doubanid = meta.doubanid
return self.run_module("recognize_media", meta=meta, mtype=mtype,
tmdbid=tmdbid, doubanid=doubanid)
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> Optional[dict]:
mtype: MediaType = None, year: str = None, season: int = None) -> Optional[dict]:
"""
搜索和匹配豆瓣信息
:param name: 标题
@@ -140,6 +146,18 @@ class ChainBase(metaclass=ABCMeta):
return self.run_module("match_doubaninfo", name=name, imdbid=imdbid,
mtype=mtype, year=year, season=season)
def match_tmdbinfo(self, name: str, mtype: MediaType = None,
year: str = None, season: int = None) -> Optional[dict]:
"""
搜索和匹配TMDB信息
:param name: 标题
:param mtype: 类型
:param year: 年份
:param season: 季
"""
return self.run_module("match_tmdbinfo", name=name,
mtype=mtype, year=year, season=season)
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
补充抓取媒体信息图片
@@ -164,13 +182,14 @@ class ChainBase(metaclass=ABCMeta):
image_prefix=image_prefix, image_type=image_type,
season=season, episode=episode)
def douban_info(self, doubanid: str) -> Optional[dict]:
def douban_info(self, doubanid: str, mtype: MediaType = None) -> Optional[dict]:
"""
获取豆瓣信息
:param doubanid: 豆瓣ID
:param mtype: 媒体类型
:return: 豆瓣信息
"""
return self.run_module("douban_info", doubanid=doubanid)
return self.run_module("douban_info", doubanid=doubanid, mtype=mtype)
def tvdb_info(self, tvdbid: int) -> Optional[dict]:
"""
@@ -372,6 +391,7 @@ class ChainBase(metaclass=ABCMeta):
self.eventmanager.send_event(etype=EventType.NoticeMessage,
data={
"channel": message.channel,
"type": message.mtype,
"title": message.title,
"text": message.text,
"image": message.image,

View File

@@ -1,10 +1,7 @@
from typing import Optional, List
from app.chain import ChainBase
from app.core.context import Context
from app.core.context import MediaInfo
from app.core.metainfo import MetaInfo
from app.log import logger
from app.core.config import settings
from app.schemas import MediaType
from app.utils.singleton import Singleton
@@ -14,53 +11,7 @@ class DoubanChain(ChainBase, metaclass=Singleton):
豆瓣处理链,单例运行
"""
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
"""
根据豆瓣ID识别媒体信息
"""
logger.info(f'开始识别媒体信息豆瓣ID{doubanid} ...')
# 查询豆瓣信息
doubaninfo = self.douban_info(doubanid=doubanid)
if not doubaninfo:
logger.warn(f'未查询到豆瓣信息豆瓣ID{doubanid}')
return None
return self.recognize_by_doubaninfo(doubaninfo)
def recognize_by_doubaninfo(self, doubaninfo: dict) -> Optional[Context]:
"""
根据豆瓣信息识别媒体信息
"""
# 优先使用原标题匹配
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 = self.recognize_media(meta=meta, mtype=meta.type)
if not mediainfo:
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)
def movie_top250(self, page: int = 1, count: int = 30) -> List[dict]:
def movie_top250(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取豆瓣电影TOP250
:param page: 页码
@@ -68,19 +19,19 @@ class DoubanChain(ChainBase, metaclass=Singleton):
"""
return self.run_module("movie_top250", page=page, count=count)
def movie_showing(self, page: int = 1, count: int = 30) -> List[dict]:
def movie_showing(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取正在上映的电影
"""
return self.run_module("movie_showing", page=page, count=count)
def tv_weekly_chinese(self, page: int = 1, count: int = 30) -> List[dict]:
def tv_weekly_chinese(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取本周中国剧集榜
"""
return self.run_module("tv_weekly_chinese", page=page, count=count)
def tv_weekly_global(self, page: int = 1, count: int = 30) -> List[dict]:
def tv_weekly_global(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取本周全球剧集榜
"""
@@ -100,8 +51,54 @@ class DoubanChain(ChainBase, metaclass=Singleton):
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
page=page, count=count)
def tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
def tv_animation(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取动画剧集
"""
return self.run_module("tv_animation", page=page, count=count)
def movie_hot(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取热门电影
"""
if settings.RECOGNIZE_SOURCE != "douban":
return None
return self.run_module("movie_hot", page=page, count=count)
def tv_hot(self, page: int = 1, count: int = 30) -> Optional[List[dict]]:
"""
获取热门剧集
"""
if settings.RECOGNIZE_SOURCE != "douban":
return None
return self.run_module("tv_hot", page=page, count=count)
def movie_credits(self, doubanid: str, page: int = 1) -> List[dict]:
"""
根据TMDBID查询电影演职人员
:param doubanid: 豆瓣ID
:param page: 页码
"""
return self.run_module("douban_movie_credits", doubanid=doubanid, page=page)
def tv_credits(self, doubanid: str, page: int = 1) -> List[dict]:
"""
根据TMDBID查询电视剧演职人员
:param doubanid: 豆瓣ID
:param page: 页码
"""
return self.run_module("douban_tv_credits", doubanid=doubanid, page=page)
def movie_recommend(self, doubanid: str) -> List[dict]:
"""
根据豆瓣ID查询推荐电影
:param doubanid: 豆瓣ID
"""
return self.run_module("douban_movie_recommend", doubanid=doubanid)
def tv_recommend(self, doubanid: str) -> List[dict]:
"""
根据豆瓣ID查询推荐电视剧
:param doubanid: 豆瓣ID
"""
return self.run_module("douban_tv_recommend", doubanid=doubanid)

View File

@@ -192,7 +192,7 @@ class DownloadChain(ChainBase):
channel=channel,
userid=userid)
if not content:
return
return None
else:
content = torrent_file
# 获取种子文件的文件夹名和文件清单
@@ -283,6 +283,10 @@ class DownloadChain(ChainBase):
if not file_meta.begin_episode \
or file_meta.begin_episode not in episodes:
continue
# 只处理视频格式
if not Path(file).suffix \
or Path(file).suffix not in settings.RMT_MEDIAEXT:
continue
files_to_add.append({
"download_hash": _hash,
"downloader": settings.DOWNLOADER,
@@ -321,7 +325,7 @@ class DownloadChain(ChainBase):
def batch_download(self,
contexts: List[Context],
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]] = None,
save_path: str = None,
channel: MessageChannel = None,
userid: str = None,
@@ -334,34 +338,34 @@ class DownloadChain(ChainBase):
:param channel: 通知渠道
:param userid: 用户ID
:param username: 调用下载的用户名/插件名
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id/douban_id] = {season: NotExistMediaInfo}
"""
# 已下载的项目
downloaded_list: List[Context] = []
def __update_seasons(_tmdbid: int, _need: list, _current: list) -> list:
def __update_seasons(_mid: Union[int, str], _need: list, _current: list) -> list:
"""
更新need_tvs季数返回剩余季数
:param _tmdbid: TMDBID
:param _mid: TMDBID
:param _need: 需要下载的季数
:param _current: 已经下载的季数
"""
# 剩余季数
need = list(set(_need).difference(set(_current)))
# 清除已下载的季信息
seas = copy.deepcopy(no_exists.get(_tmdbid))
seas = copy.deepcopy(no_exists.get(_mid))
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)
no_exists[_mid].pop(_sea)
if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
no_exists.pop(_mid)
break
return need
def __update_episodes(_tmdbid: int, _sea: int, _need: list, _current: set) -> list:
def __update_episodes(_mid: Union[int, str], _sea: int, _need: list, _current: set) -> list:
"""
更新need_tvs集数返回剩余集数
:param _tmdbid: TMDBID
:param _mid: TMDBID
:param _sea: 季数
:param _need: 需要下载的集数
:param _current: 已经下载的集数
@@ -369,26 +373,26 @@ class DownloadChain(ChainBase):
# 剩余集数
need = list(set(_need).difference(set(_current)))
if need:
not_exist = no_exists[_tmdbid][_sea]
no_exists[_tmdbid][_sea] = NotExistMediaInfo(
not_exist = no_exists[_mid][_sea]
no_exists[_mid][_sea] = NotExistMediaInfo(
season=not_exist.season,
episodes=need,
total_episode=not_exist.total_episode,
start_episode=not_exist.start_episode
)
else:
no_exists[_tmdbid].pop(_sea)
if not no_exists.get(_tmdbid) and no_exists.get(_tmdbid) is not None:
no_exists.pop(_tmdbid)
no_exists[_mid].pop(_sea)
if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
no_exists.pop(_mid)
return need
def __get_season_episodes(tmdbid: int, season: int) -> int:
def __get_season_episodes(_mid: Union[int, str], season: int) -> int:
"""
获取需要的季的集数
"""
if not no_exists.get(tmdbid):
if not no_exists.get(_mid):
return 9999
no_exist = no_exists.get(tmdbid)
no_exist = no_exists.get(_mid)
if not no_exist.get(season):
return 9999
return no_exist[season].total_episode
@@ -408,17 +412,17 @@ class DownloadChain(ChainBase):
if no_exists:
# 先把整季缺失的拿出来,看是否刚好有所有季都满足的种子 {tmdbid: [seasons]}
need_seasons: Dict[int, list] = {}
for need_tmdbid, need_tv in no_exists.items():
for need_mid, need_tv in no_exists.items():
for tv in need_tv.values():
if not tv:
continue
# 季列表为空的,代表全季缺失
if not tv.episodes:
if not need_seasons.get(need_tmdbid):
need_seasons[need_tmdbid] = []
need_seasons[need_tmdbid].append(tv.season or 1)
if not need_seasons.get(need_mid):
need_seasons[need_mid] = []
need_seasons[need_mid].append(tv.season or 1)
# 查找整季包含的种子,只处理整季没集的种子或者是集数超过季的种子
for need_tmdbid, need_season in need_seasons.items():
for need_mid, need_season in need_seasons.items():
# 循环种子
for context in contexts:
# 媒体信息
@@ -436,7 +440,7 @@ class DownloadChain(ChainBase):
if meta.episode_list:
continue
# 匹配TMDBID
if need_tmdbid == media.tmdb_id:
if need_mid == media.tmdb_id or need_mid == media.douban_id:
# 种子季是需要季或者子集
if set(torrent_season).issubset(set(need_season)):
if len(torrent_season) == 1:
@@ -456,7 +460,7 @@ class DownloadChain(ChainBase):
end_ep = max(torrent_episodes)
meta.set_episodes(begin=begin_ep, end=end_ep)
# 需要总集数
need_total = __get_season_episodes(need_tmdbid, torrent_season[0])
need_total = __get_season_episodes(need_mid, torrent_season[0])
if len(torrent_episodes) < need_total:
logger.info(
f"{meta.org_string} 解析文件集数发现不是完整合集")
@@ -480,19 +484,19 @@ class DownloadChain(ChainBase):
# 下载成功
downloaded_list.append(context)
# 更新仍需季集
need_season = __update_seasons(_tmdbid=need_tmdbid,
need_season = __update_seasons(_mid=need_mid,
_need=need_season,
_current=torrent_season)
# 电视剧季内的集匹配
if no_exists:
# TMDBID列表
need_tv_list = list(no_exists)
for need_tmdbid in need_tv_list:
for need_mid in need_tv_list:
# dict[season, [NotExistMediaInfo]]
need_tv = no_exists.get(need_tmdbid)
need_tv = no_exists.get(need_mid)
if not need_tv:
continue
need_tv_copy = copy.deepcopy(no_exists.get(need_tmdbid))
need_tv_copy = copy.deepcopy(no_exists.get(need_mid))
# 循环每一季
for sea, tv in need_tv_copy.items():
# 当前需要季
@@ -516,7 +520,7 @@ class DownloadChain(ChainBase):
if media.type != MediaType.TV:
continue
# 匹配TMDB
if media.tmdb_id == need_tmdbid:
if media.tmdb_id == need_mid or media.douban_id == need_mid:
# 不重复添加
if context in downloaded_list:
continue
@@ -539,7 +543,7 @@ class DownloadChain(ChainBase):
# 下载成功
downloaded_list.append(context)
# 更新仍需集数
need_episodes = __update_episodes(_tmdbid=need_tmdbid,
need_episodes = __update_episodes(_mid=need_mid,
_need=need_episodes,
_sea=need_season,
_current=torrent_episodes)
@@ -548,9 +552,9 @@ class DownloadChain(ChainBase):
if no_exists:
# TMDBID列表
no_exists_list = list(no_exists)
for need_tmdbid in no_exists_list:
for need_mid in no_exists_list:
# dict[season, [NotExistMediaInfo]]
need_tv = no_exists.get(need_tmdbid)
need_tv = no_exists.get(need_mid)
if not need_tv:
continue
# 需要季列表
@@ -584,7 +588,7 @@ class DownloadChain(ChainBase):
if not need_episodes:
break
# 选中一个单季整季的或单季包括需要的所有集的
if media.tmdb_id == need_tmdbid \
if (media.tmdb_id == need_mid or media.douban_id == need_mid) \
and (not meta.episode_list
or set(meta.episode_list).intersection(set(need_episodes))) \
and len(meta.season_list) == 1 \
@@ -624,7 +628,7 @@ class DownloadChain(ChainBase):
end_ep = max(torrent_episodes)
meta.set_episodes(begin=begin_ep, end=end_ep)
# 更新仍需集数
need_episodes = __update_episodes(_tmdbid=need_tmdbid,
need_episodes = __update_episodes(_mid=need_mid,
_need=need_episodes,
_sea=need_season,
_current=selected_episodes)
@@ -656,8 +660,9 @@ class DownloadChain(ChainBase):
"start_episode": int
]}
"""
if not no_exists.get(mediainfo.tmdb_id):
no_exists[mediainfo.tmdb_id] = {
mediakey = mediainfo.tmdb_id or mediainfo.douban_id
if not no_exists.get(mediakey):
no_exists[mediakey] = {
_season: NotExistMediaInfo(
season=_season,
episodes=_episodes,
@@ -666,7 +671,7 @@ class DownloadChain(ChainBase):
)
}
else:
no_exists[mediainfo.tmdb_id][_season] = NotExistMediaInfo(
no_exists[mediakey][_season] = NotExistMediaInfo(
season=_season,
episodes=_episodes,
total_episode=_total,
@@ -682,6 +687,7 @@ class DownloadChain(ChainBase):
if mediainfo.type == MediaType.MOVIE:
# 电影
itemid = self.mediaserver.get_item_id(mtype=mediainfo.type.value,
title=mediainfo.title,
tmdbid=mediainfo.tmdb_id)
exists_movies: Optional[ExistMediaInfo] = self.media_exists(mediainfo=mediainfo, itemid=itemid)
if exists_movies:
@@ -692,7 +698,8 @@ class DownloadChain(ChainBase):
if not mediainfo.seasons:
# 补充媒体信息
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
return False, {}
@@ -701,6 +708,7 @@ class DownloadChain(ChainBase):
return False, {}
# 电视剧
itemid = self.mediaserver.get_item_id(mtype=mediainfo.type.value,
title=mediainfo.title,
tmdbid=mediainfo.tmdb_id,
season=mediainfo.season)
# 媒体库已存在的剧集
@@ -711,7 +719,7 @@ class DownloadChain(ChainBase):
if not episodes:
continue
# 全季不存在
if meta.season_list \
if meta.sea \
and season not in meta.season_list:
continue
# 总集数
@@ -722,7 +730,7 @@ class DownloadChain(ChainBase):
else:
# 存在一些,检查每季缺失的季集
for season, episodes in mediainfo.seasons.items():
if meta.begin_season \
if meta.sea \
and season not in meta.season_list:
continue
if not episodes:

View File

@@ -14,7 +14,6 @@ from app.schemas.types import EventType, MediaType
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
recognize_lock = Lock()
@@ -27,13 +26,11 @@ class MediaChain(ChainBase, metaclass=Singleton):
# 临时识别结果 {title, name, year, season, episode}
recognize_temp: Optional[dict] = None
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
def recognize_by_meta(self, metainfo: MetaBase) -> Optional[MediaInfo]:
"""
根据主副标题识别媒体信息
"""
logger.info(f'开始识别媒体信息,标题:{title},副标题:{subtitle} ...')
# 识别元数据
metainfo = MetaInfo(title, subtitle)
title = metainfo.title
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
@@ -43,13 +40,13 @@ class MediaChain(ChainBase, metaclass=Singleton):
mediainfo = self.recognize_help(title=title, org_meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta_info=metainfo)
return None
# 识别成功
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 返回上下文
return Context(meta_info=metainfo, media_info=mediainfo)
return mediainfo
def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[MediaInfo]:
"""
@@ -69,7 +66,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
}
)
# 每0.5秒循环一次等待结果直到10秒后超时
for i in range(10):
for i in range(20):
if self.recognize_temp is not None:
break
time.sleep(0.5)
@@ -170,8 +167,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
# 识别
meta = MetaInfo(content)
if not meta.name:
logger.warn(f'{title} 未识别到元数据!')
return meta, []
meta.cn_name = content
# 合并信息
if mtype:
meta.type = mtype
@@ -190,3 +186,78 @@ class MediaChain(ChainBase, metaclass=Singleton):
logger.info(f"{content} 搜索到 {len(medias)} 条相关媒体信息")
# 识别的元数据,媒体信息列表
return meta, medias
def get_tmdbinfo_by_doubanid(self, doubanid: str, mtype: MediaType = None) -> Optional[dict]:
"""
根据豆瓣ID获取TMDB信息
"""
tmdbinfo = None
doubaninfo = self.douban_info(doubanid=doubanid, mtype=mtype)
if doubaninfo:
# 优先使用原标题匹配
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
# 使用原标题识别TMDB媒体信息
tmdbinfo = self.match_tmdbinfo(
name=meta.name,
year=meta.year,
mtype=mtype or meta.type,
season=meta.begin_season
)
if not tmdbinfo:
if season_meta and season_meta.name != meta.name:
# 使用主标题识别媒体信息
tmdbinfo = self.match_tmdbinfo(
name=season_meta.name,
year=meta.year,
mtype=mtype or meta.type,
season=meta.begin_season
)
return tmdbinfo
def get_doubaninfo_by_tmdbid(self, tmdbid: int,
mtype: MediaType = None, season: int = None) -> Optional[dict]:
"""
根据TMDBID获取豆瓣信息
"""
tmdbinfo = self.tmdb_info(tmdbid=tmdbid, mtype=mtype)
if tmdbinfo:
# 名称
name = tmdbinfo.get("title") or tmdbinfo.get("name")
# 年份
year = None
if tmdbinfo.get('release_date'):
year = tmdbinfo['release_date'][:4]
elif tmdbinfo.get('seasons') and season:
for seainfo in tmdbinfo['seasons']:
# 季
season_number = seainfo.get("season_number")
if not season_number:
continue
air_date = seainfo.get("air_date")
if air_date and season_number == season:
year = air_date[:4]
break
# IMDBID
imdbid = tmdbinfo.get("external_ids", {}).get("imdb_id")
return self.match_doubaninfo(
name=name,
year=year,
mtype=mtype,
imdbid=imdbid
)
return None

View File

@@ -52,7 +52,7 @@ class MediaServerChain(ChainBase):
# 汇总统计
total_count = 0
# 清空登记薄
self.dboper.empty(server=settings.MEDIASERVER)
self.dboper.empty()
# 同步黑名单
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []

View File

@@ -1,15 +1,22 @@
import copy
from typing import Any
import json
import re
from typing import Any, Optional, Dict
from app.chain.download import *
from app.chain import ChainBase
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.subscribe import SubscribeChain
from app.core.context import MediaInfo
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.core.event import EventManager
from app.core.meta import MetaBase
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas import Notification
from app.schemas.types import EventType, MessageChannel
from app.schemas.types import EventType, MessageChannel, MediaType
from app.utils.string import StringUtils
# 当前页面
_current_page: int = 0
@@ -34,7 +41,6 @@ class MessageChain(ChainBase):
self.subscribechain = SubscribeChain()
self.searchchain = SearchChain()
self.medtachain = MediaChain()
self.torrent = TorrentHelper()
self.eventmanager = EventManager()
self.torrenthelper = TorrentHelper()
@@ -110,9 +116,10 @@ class MessageChain(ChainBase):
# 发送缺失的媒体信息
if no_exists:
# 发送消息
mediakey = mediainfo.tmdb_id or mediainfo.douban_id
messages = [
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()]
for sea, no_exist in no_exists.get(mediakey).items()]
self.post_message(Notification(channel=channel,
title=f"{mediainfo.title_year}\n" + "\n".join(messages),
userid=userid))
@@ -353,6 +360,13 @@ class MessageChain(ChainBase):
else:
# 未完成下载
logger.info(f'{_current_media.title_year} 未下载未完整,添加订阅 ...')
if downloads and _current_media.type == MediaType.TV:
# 获取已下载剧集
downloaded = [download.meta_info.begin_episode for download in downloads
if download.meta_info.begin_episode]
note = json.dumps(downloaded)
else:
note = None
# 添加订阅状态为R
self.subscribechain.add(title=_current_media.title,
year=_current_media.year,
@@ -362,7 +376,8 @@ class MessageChain(ChainBase):
channel=channel,
userid=userid,
username=username,
state="R")
state="R",
note=note)
def __post_medias_message(self, channel: MessageChannel,
title: str, items: list, userid: str, total: int):

View File

@@ -31,14 +31,16 @@ class SearchChain(ChainBase):
self.systemconfig = SystemConfigOper()
self.torrenthelper = TorrentHelper()
def search_by_tmdbid(self, tmdbid: int, mtype: MediaType = None, area: str = "title") -> List[Context]:
def search_by_id(self, tmdbid: int = None, doubanid: str = None,
mtype: MediaType = None, area: str = "title") -> List[Context]:
"""
根据TMDB ID搜索资源精确匹配但不不过滤本地存在的资源
根据TMDBID/豆瓣ID搜索资源精确匹配但不不过滤本地存在的资源
:param tmdbid: TMDB ID
:param doubanid: 豆瓣 ID
:param mtype: 媒体,电影 or 电视剧
:param area: 搜索范围title or imdbid
"""
mediainfo = self.recognize_media(tmdbid=tmdbid, mtype=mtype)
mediainfo = self.recognize_media(tmdbid=tmdbid, doubanid=doubanid, mtype=mtype)
if not mediainfo:
logger.error(f'{tmdbid} 媒体信息识别失败!')
return []
@@ -92,19 +94,29 @@ class SearchChain(ChainBase):
:param filter_rule: 过滤规则,为空是使用默认过滤规则
:param area: 搜索范围title or imdbid
"""
# 豆瓣标题处理
if not mediainfo.tmdb_id:
meta = MetaInfo(title=mediainfo.title)
mediainfo.title = meta.name
mediainfo.season = meta.begin_season
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
# 补充媒体信息
if not mediainfo.names:
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id)
if not mediainfo:
logger.error(f'媒体信息识别失败!')
return []
# 缺失的季集
if no_exists and no_exists.get(mediainfo.tmdb_id):
mediakey = mediainfo.tmdb_id or mediainfo.douban_id
if no_exists and no_exists.get(mediakey):
# 过滤剧集
season_episodes = {sea: info.episodes
for sea, info in no_exists[mediainfo.tmdb_id].items()}
elif mediainfo.season:
# 豆瓣只搜索当前季
season_episodes = {mediainfo.season: []}
else:
season_episodes = None
# 搜索关键词
@@ -154,6 +166,7 @@ class SearchChain(ChainBase):
if mediainfo:
self.progress.start(ProgressKey.Search)
logger.info(f'开始匹配,总 {_total} 个资源 ...')
logger.info(f"标题:{mediainfo.title},原标题:{mediainfo.original_title},别名:{mediainfo.names}")
self.progress.update(value=0, text=f'开始匹配,总 {_total} 个资源 ...', key=ProgressKey.Search)
for torrent in torrents:
_count += 1
@@ -200,7 +213,7 @@ class SearchChain(ChainBase):
continue
# 在副标题中判断是否存在标题与原语种标题
if torrent.description:
subtitle = torrent.description.split()
subtitle = re.split(r'[\s/|]+', torrent.description)
if (StringUtils.is_chinese(mediainfo.title)
and str(mediainfo.title) in subtitle) \
or (StringUtils.is_chinese(mediainfo.original_title)

View File

@@ -6,10 +6,11 @@ from datetime import datetime
from typing import Dict, List, Optional, Union, Tuple
from app.chain import ChainBase
from app.chain.douban import DoubanChain
from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.chain.search import SearchChain
from app.chain.torrents import TorrentsChain
from app.core.config import settings
from app.core.context import TorrentInfo, Context, MediaInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
@@ -33,6 +34,7 @@ class SubscribeChain(ChainBase):
self.searchchain = SearchChain()
self.subscribeoper = SubscribeOper()
self.torrentschain = TorrentsChain()
self.mediachain = MediaChain()
self.message = MessageHelper()
self.systemconfig = SystemConfigOper()
@@ -51,32 +53,39 @@ class SubscribeChain(ChainBase):
识别媒体信息并添加订阅
"""
logger.info(f'开始添加订阅,标题:{title} ...')
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
metainfo = MetaInfo(title)
if year:
metainfo.year = year
if mtype:
metainfo.type = mtype
if season:
metainfo.type = MediaType.TV
metainfo.begin_season = season
# 识别媒体信息
if settings.RECOGNIZE_SOURCE == "themoviedb":
# TMDB识别模式
if not tmdbid and doubanid:
# 将豆瓣信息转换为TMDB信息
tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
if tmdbinfo:
mediainfo = MediaInfo(tmdb_info=tmdbinfo)
else:
# 识别TMDB信息
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
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)
# 豆瓣识别模式
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, doubanid=doubanid)
if mediainfo:
# 豆瓣标题处理
meta = MetaInfo(mediainfo.title)
mediainfo.title = meta.name
if not season:
season = meta.begin_season
# 识别失败
if not mediainfo or not metainfo or not mediainfo.tmdb_id:
logger.warn(f'未识别到媒体信息,标题:{title}tmdbid{tmdbid}')
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{title}tmdbid{tmdbid}doubanid{doubanid}')
return None, "未识别到媒体信息"
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 总集数
if mediainfo.type == MediaType.TV:
if not season:
@@ -86,16 +95,17 @@ class SubscribeChain(ChainBase):
if not mediainfo.seasons:
# 补充媒体信息
mediainfo = self.recognize_media(mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id)
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id)
if not mediainfo:
logger.error(f"媒体信息识别失败!")
return None, "媒体信息识别失败"
if not mediainfo.seasons:
logger.error(f"媒体信息中没有季集信息,标题:{title}tmdbid{tmdbid}")
logger.error(f"媒体信息中没有季集信息,标题:{title}tmdbid{tmdbid}doubanid{doubanid}")
return None, "媒体信息中没有季集信息"
total_episode = len(mediainfo.seasons.get(season) or [])
if not total_episode:
logger.error(f'未获取到总集数,标题:{title}tmdbid{tmdbid}')
logger.error(f'未获取到总集数,标题:{title}tmdbid{tmdbid}, doubanid{doubanid}')
return None, f"未获取到第 {season} 季的总集数"
kwargs.update({
'total_episode': total_episode
@@ -105,9 +115,13 @@ class SubscribeChain(ChainBase):
kwargs.update({
'lack_episode': kwargs.get('total_episode')
})
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 合并信息
if doubanid:
mediainfo.douban_id = doubanid
# 添加订阅
sid, err_msg = self.subscribeoper.add(mediainfo, doubanid=doubanid,
season=season, username=username, **kwargs)
sid, err_msg = self.subscribeoper.add(mediainfo, season=season, username=username, **kwargs)
if not sid:
logger.error(f'{mediainfo.title_year} {err_msg}')
if not exist_ok and message:
@@ -139,6 +153,7 @@ class SubscribeChain(ChainBase):
判断订阅是否已存在
"""
if self.subscribeoper.exists(tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id,
season=meta.begin_season if meta else None):
return True
return False
@@ -157,6 +172,7 @@ class SubscribeChain(ChainBase):
subscribes = self.subscribeoper.list(state)
# 遍历订阅
for subscribe in subscribes:
mediakey = subscribe.tmdbid or subscribe.doubanid
# 校验当前时间减订阅创建时间是否大于1分钟否则跳过先留出编辑订阅的时间
if subscribe.date:
now = datetime.now()
@@ -179,9 +195,11 @@ class SubscribeChain(ChainBase):
meta.begin_season = subscribe.season or None
meta.type = MediaType(subscribe.type)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid)
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}')
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}')
continue
# 非洗版状态
@@ -203,7 +221,7 @@ class SubscribeChain(ChainBase):
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.tmdbid: {
mediakey: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
@@ -225,15 +243,15 @@ class SubscribeChain(ChainBase):
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
mediakey=mediakey,
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 and no_exists.get(mediakey):
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
@@ -469,15 +487,17 @@ class SubscribeChain(ChainBase):
# 遍历订阅
for subscribe in subscribes:
logger.info(f'开始匹配订阅,标题:{subscribe.name} ...')
mediakey = subscribe.tmdbid or subscribe.doubanid
# 生成元数据
meta = MetaInfo(subscribe.name)
meta.year = subscribe.year
meta.begin_season = subscribe.season or None
meta.type = MediaType(subscribe.type)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid)
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}')
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}')
continue
# 非洗版
if not subscribe.best_version:
@@ -498,7 +518,7 @@ class SubscribeChain(ChainBase):
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
subscribe.tmdbid: {
mediakey: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
@@ -520,15 +540,15 @@ class SubscribeChain(ChainBase):
# 使用订阅的总集数和开始集数替换no_exists
no_exists = self.__get_subscribe_no_exits(
no_exists=no_exists,
tmdb_id=mediainfo.tmdb_id,
mediakey=mediakey,
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 and no_exists.get(mediakey):
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
if no_exists_info:
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
@@ -583,9 +603,9 @@ class SubscribeChain(ChainBase):
# 非洗版
if not subscribe.best_version:
# 不是缺失的剧集不要
if no_exists and no_exists.get(subscribe.tmdbid):
if no_exists and no_exists.get(mediakey):
# 缺失集
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
no_exists_info = no_exists.get(mediakey).get(subscribe.season)
if no_exists_info:
# 是否有交集
if no_exists_info.episodes and \
@@ -661,9 +681,10 @@ class SubscribeChain(ChainBase):
meta.begin_season = subscribe.season or None
meta.type = MediaType(subscribe.type)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid)
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type,
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}')
logger.warn(f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}')
continue
# 对于电视剧,获取当前季的总集数
episodes = mediainfo.seasons.get(subscribe.season) or []
@@ -705,7 +726,11 @@ class SubscribeChain(ChainBase):
mediainfo = context.media_info
if mediainfo.type != MediaType.TV:
continue
if mediainfo.tmdb_id != subscribe.tmdbid:
if subscribe.tmdbid and mediainfo.tmdb_id \
and mediainfo.tmdb_id != subscribe.tmdbid:
continue
if subscribe.doubanid and mediainfo.douban_id \
and mediainfo.douban_id != subscribe.doubanid:
continue
episodes = meta.episode_list
if not episodes:
@@ -739,7 +764,8 @@ class SubscribeChain(ChainBase):
"""
更新订阅剩余集数
"""
left_seasons = lefts.get(mediainfo.tmdb_id)
mediakey = subscribe.tmdbid or subscribe.doubanid
left_seasons = lefts.get(mediakey)
if left_seasons:
for season_info in left_seasons.values():
season = season_info.season
@@ -780,11 +806,17 @@ class SubscribeChain(ChainBase):
messages = []
for subscribe in subscribes:
if subscribe.type == MediaType.MOVIE.value:
tmdb_link = f"https://www.themoviedb.org/movie/{subscribe.tmdbid}"
messages.append(f"{subscribe.id}. [{subscribe.name}{subscribe.year}]({tmdb_link})")
if subscribe.tmdbid:
link = f"https://www.themoviedb.org/movie/{subscribe.tmdbid}"
else:
link = f"https://movie.douban.com/subject/{subscribe.doubanid}"
messages.append(f"{subscribe.id}. [{subscribe.name}{subscribe.year}]({link})")
else:
tmdb_link = f"https://www.themoviedb.org/tv/{subscribe.tmdbid}"
messages.append(f"{subscribe.id}. [{subscribe.name}{subscribe.year}]({tmdb_link}) "
if subscribe.tmdbid:
link = f"https://www.themoviedb.org/tv/{subscribe.tmdbid}"
else:
link = f"https://movie.douban.com/subject/{subscribe.doubanid}"
messages.append(f"{subscribe.id}. [{subscribe.name}{subscribe.year}]({link}) "
f"{subscribe.season}"
f"_{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}"
f"/{subscribe.total_episode}_")
@@ -819,24 +851,24 @@ class SubscribeChain(ChainBase):
@staticmethod
def __get_subscribe_no_exits(no_exists: Dict[int, Dict[int, NotExistMediaInfo]],
tmdb_id: int,
mediakey: Union[str, int],
begin_season: int,
total_episode: int,
start_episode: int):
"""
根据订阅开始集数和总集数结合TMDB信息计算当前订阅的缺失集数
:param no_exists: 缺失季集列表
:param tmdb_id: TMDB ID
:param mediakey: TMDB ID或豆瓣ID
:param begin_season: 开始季
:param total_episode: 订阅设定总集数
:param start_episode: 订阅设定开始集数
"""
# 使用订阅的总集数和开始集数替换no_exists
if no_exists \
and no_exists.get(tmdb_id) \
and no_exists.get(mediakey) \
and (total_episode or start_episode):
# 该季原缺失信息
no_exist_season = no_exists.get(tmdb_id).get(begin_season)
no_exist_season = no_exists.get(mediakey).get(begin_season)
if no_exist_season:
# 原集列表
episode_list = no_exist_season.episodes
@@ -868,7 +900,7 @@ class SubscribeChain(ChainBase):
# 与原集列表取交集
episodes = list(set(episode_list).intersection(set(new_episodes)))
# 更新集合
no_exists[tmdb_id][begin_season] = NotExistMediaInfo(
no_exists[mediakey][begin_season] = NotExistMediaInfo(
season=begin_season,
episodes=episodes,
total_episode=total_episode,

View File

@@ -87,7 +87,7 @@ class SystemChain(ChainBase, metaclass=Singleton):
"""
获取最新版本
"""
version_res = RequestUtils(proxies=settings.PROXY).get_res(
version_res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS).get_res(
"https://api.github.com/repos/jxxghp/MoviePilot/releases/latest")
if version_res:
ver_json = version_res.json()

View File

@@ -25,17 +25,21 @@ class TmdbChain(ChainBase, metaclass=Singleton):
:param page: 页码
:return: 媒体信息列表
"""
if settings.RECOGNIZE_SOURCE != "themoviedb":
return None
return self.run_module("tmdb_discover", mtype=mtype,
sort_by=sort_by, with_genres=with_genres,
with_original_language=with_original_language,
page=page)
def tmdb_trending(self, page: int = 1) -> List[dict]:
def tmdb_trending(self, page: int = 1) -> Optional[List[dict]]:
"""
TMDB流行趋势
:param page: 第几页
:return: TMDB信息列表
"""
if settings.RECOGNIZE_SOURCE != "themoviedb":
return None
return self.run_module("tmdb_trending", page=page)
def tmdb_seasons(self, tmdbid: int) -> List[schemas.TmdbSeason]:
@@ -58,28 +62,28 @@ class TmdbChain(ChainBase, metaclass=Singleton):
根据TMDBID查询类似电影
:param tmdbid: TMDBID
"""
return self.run_module("movie_similar", tmdbid=tmdbid)
return self.run_module("tmdb_movie_similar", tmdbid=tmdbid)
def tv_similar(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询类似电视剧
:param tmdbid: TMDBID
"""
return self.run_module("tv_similar", tmdbid=tmdbid)
return self.run_module("tmdb_tv_similar", tmdbid=tmdbid)
def movie_recommend(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询推荐电影
:param tmdbid: TMDBID
"""
return self.run_module("movie_recommend", tmdbid=tmdbid)
return self.run_module("tmdb_movie_recommend", tmdbid=tmdbid)
def tv_recommend(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询推荐电视剧
:param tmdbid: TMDBID
"""
return self.run_module("tv_recommend", tmdbid=tmdbid)
return self.run_module("tmdb_tv_recommend", tmdbid=tmdbid)
def movie_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
"""
@@ -87,7 +91,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
:param tmdbid: TMDBID
:param page: 页码
"""
return self.run_module("movie_credits", tmdbid=tmdbid, page=page)
return self.run_module("tmdb_movie_credits", tmdbid=tmdbid, page=page)
def tv_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
"""
@@ -95,14 +99,14 @@ class TmdbChain(ChainBase, metaclass=Singleton):
:param tmdbid: TMDBID
:param page: 页码
"""
return self.run_module("tv_credits", tmdbid=tmdbid, page=page)
return self.run_module("tmdb_tv_credits", tmdbid=tmdbid, page=page)
def person_detail(self, person_id: int) -> dict:
"""
根据TMDBID查询演职员详情
:param person_id: 人物ID
"""
return self.run_module("person_detail", person_id=person_id)
return self.run_module("tmdb_person_detail", person_id=person_id)
def person_credits(self, person_id: int, page: int = 1) -> List[dict]:
"""
@@ -110,7 +114,7 @@ class TmdbChain(ChainBase, metaclass=Singleton):
:param person_id: 人物ID
:param page: 页码
"""
return self.run_module("person_credits", person_id=person_id, page=page)
return self.run_module("tmdb_person_credits", person_id=person_id, page=page)
@cached(cache=TTLCache(maxsize=1, ttl=3600))
def get_random_wallpager(self):

View File

@@ -4,6 +4,7 @@ from typing import Dict, List, Union
from cachetools import cached, TTLCache
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.core.config import settings
from app.core.context import TorrentInfo, Context, MediaInfo
from app.core.metainfo import MetaInfo
@@ -11,6 +12,7 @@ from app.db.site_oper import SiteOper
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.rss import RssHelper
from app.helper.sites import SitesHelper
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas import Notification
from app.schemas.types import SystemConfigKey, MessageChannel, NotificationType
@@ -32,6 +34,8 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
self.siteoper = SiteOper()
self.rsshelper = RssHelper()
self.systemconfig = SystemConfigOper()
self.mediachain = MediaChain()
self.torrenthelper = TorrentHelper()
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
"""
@@ -58,7 +62,16 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
else:
return self.load_cache(self._rss_file) or {}
@cached(cache=TTLCache(maxsize=128, ttl=600))
def clear_torrents(self):
"""
清理种子缓存数据
"""
logger.info(f'开始清理种子缓存数据 ...')
self.remove_cache(self._spider_file)
self.remove_cache(self._rss_file)
logger.info(f'种子缓存数据清理完成')
@cached(cache=TTLCache(maxsize=128, ttl=595))
def browse(self, domain: str) -> List[TorrentInfo]:
"""
浏览站点首页内容返回种子清单TTL缓存10分钟
@@ -71,7 +84,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
return []
return self.refresh_torrents(site=site)
@cached(cache=TTLCache(maxsize=128, ttl=300))
@cached(cache=TTLCache(maxsize=128, ttl=295))
def rss(self, domain: str) -> List[TorrentInfo]:
"""
获取站点RSS内容返回种子清单TTL缓存5分钟
@@ -132,6 +145,11 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
# 读取缓存
torrents_cache = self.get_torrents()
# 缓存过滤掉无效种子
for _domain, _torrents in torrents_cache.items():
torrents_cache[_domain] = [_torrent for _torrent in _torrents
if not self.torrenthelper.is_invalid(_torrent.torrent_info.enclosure)]
# 所有站点索引
indexers = self.siteshelper.get_indexers()
# 遍历站点缓存资源
@@ -166,7 +184,7 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
# 识别
meta = MetaInfo(title=torrent.title, subtitle=torrent.description)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=meta)
mediainfo: MediaInfo = self.mediachain.recognize_by_meta(meta)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{torrent.title}')
# 存储空的媒体信息

View File

@@ -66,7 +66,8 @@ class TransferChain(ChainBase):
mtype = MediaType(downloadhis.type)
# 按TMDBID识别
mediainfo = self.recognize_media(mtype=mtype,
tmdbid=downloadhis.tmdbid)
tmdbid=downloadhis.tmdbid,
doubanid=downloadhis.doubanid)
else:
# 非MoviePilot下载的任务按文件识别
mediainfo = None
@@ -243,7 +244,7 @@ class TransferChain(ChainBase):
if not mediainfo:
# 识别媒体信息
file_mediainfo = self.recognize_media(meta=file_meta)
file_mediainfo = self.mediachain.recognize_by_meta(file_meta)
else:
file_mediainfo = mediainfo
@@ -275,9 +276,6 @@ class TransferChain(ChainBase):
logger.info(f"{file_path.name} 识别为:{file_mediainfo.type.value} {file_mediainfo.title_year}")
# 更新媒体图片
self.obtain_images(mediainfo=file_mediainfo)
# 获取集数据
if file_mediainfo.type == MediaType.TV:
episodes_info = self.tmdbchain.tmdb_episodes(tmdbid=file_mediainfo.tmdb_id,
@@ -449,7 +447,7 @@ class TransferChain(ChainBase):
def args_error():
self.post_message(Notification(channel=channel,
title="请输入正确的命令格式:/redo [id] [tmdbid]|[类型]"
title="请输入正确的命令格式:/redo [id] [tmdbid/豆瓣id]|[类型]"
"[id]历史记录编号", userid=userid))
if not arg_str:
@@ -464,31 +462,32 @@ class TransferChain(ChainBase):
if not logid.isdigit():
args_error()
return
# TMDB ID
tmdb_strs = arg_strs[1].split('|')
tmdbid = tmdb_strs[0]
# TMDBID/豆瓣ID
id_strs = arg_strs[1].split('|')
media_id = id_strs[0]
if not logid.isdigit():
args_error()
return
# 类型
type_str = tmdb_strs[1] if len(tmdb_strs) > 1 else None
type_str = id_strs[1] if len(id_strs) > 1 else None
if not type_str or type_str not in [MediaType.MOVIE.value, MediaType.TV.value]:
args_error()
return
state, errmsg = self.re_transfer(logid=int(logid),
mtype=MediaType(type_str), tmdbid=int(tmdbid))
mtype=MediaType(type_str),
mediaid=media_id)
if not state:
self.post_message(Notification(channel=channel, title="手动整理失败",
text=errmsg, userid=userid))
return
def re_transfer(self, logid: int,
mtype: MediaType = None, tmdbid: int = None) -> Tuple[bool, str]:
def re_transfer(self, logid: int, mtype: MediaType = None,
mediaid: str = None) -> Tuple[bool, str]:
"""
根据历史记录,重新识别转移,只支持简单条件
:param logid: 历史记录ID
:param mtype: 媒体类型
:param tmdbid: TMDB ID
:param mediaid: TMDB ID/豆瓣ID
"""
# 查询历史记录
history: TransferHistory = self.transferhis.get(logid)
@@ -501,17 +500,18 @@ class TransferChain(ChainBase):
return False, f"源目录不存在:{src_path}"
dest_path = Path(history.dest) if history.dest else None
# 查询媒体信息
if mtype and tmdbid:
mediainfo = self.recognize_media(mtype=mtype, tmdbid=tmdbid)
if mtype and mediaid:
mediainfo = self.recognize_media(mtype=mtype, tmdbid=int(mediaid) if str(mediaid).isdigit() else None,
doubanid=mediaid)
if mediainfo:
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
else:
meta = MetaInfoPath(src_path)
mediainfo = self.recognize_media(meta=meta)
mediainfo = self.mediachain.recognize_by_path(str(src_path))
if not mediainfo:
return False, f"未识别到媒体信息,类型:{mtype.value}tmdbid{tmdbid}"
return False, f"未识别到媒体信息,类型:{mtype.value}id{mediaid}"
# 重新执行转移
logger.info(f"{src_path.name} 识别为:{mediainfo.title_year}")
# 更新媒体图片
self.obtain_images(mediainfo=mediainfo)
# 删除旧的已整理文件
if history.dest:

View File

@@ -1,7 +1,7 @@
import secrets
import sys
from pathlib import Path
from typing import List
from typing import List, Optional
from pydantic import BaseSettings
@@ -43,14 +43,14 @@ class Settings(BaseSettings):
WALLPAPER: str = "tmdb"
# 网络代理 IP:PORT
PROXY_HOST: str = None
# 媒体信息搜索来源
SEARCH_SOURCE: str = "themoviedb"
# 媒体识别来源 themoviedb/douban
RECOGNIZE_SOURCE: str = "themoviedb"
# 刮削来源 themoviedb/douban
SCRAP_SOURCE: str = "themoviedb"
# 刮削入库的媒体文件
SCRAP_METADATA: bool = True
# 新增已入库媒体是否跟随TMDB信息变化
SCRAP_FOLLOW_TMDB: bool = True
# 刮削来源
SCRAP_SOURCE: str = "themoviedb"
# TMDB图片地址
TMDB_IMAGE_DOMAIN: str = "image.tmdb.org"
# TMDB API地址
@@ -156,7 +156,7 @@ class Settings(BaseSettings):
# 媒体服务器 emby/jellyfin/plex多个媒体服务器,分割
MEDIASERVER: str = "emby"
# 媒体服务器同步间隔(小时)
MEDIASERVER_SYNC_INTERVAL: int = 6
MEDIASERVER_SYNC_INTERVAL: Optional[int] = 6
# 媒体服务器同步黑名单,多个媒体库名称,分割
MEDIASERVER_SYNC_BLACKLIST: str = None
# EMBY服务器地址IP:PORT
@@ -180,7 +180,7 @@ class Settings(BaseSettings):
# CookieCloud端对端加密密码
COOKIECLOUD_PASSWORD: str = None
# CookieCloud同步间隔分钟
COOKIECLOUD_INTERVAL: int = 60 * 24
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
# OCR服务器地址
OCR_HOST: str = "https://movie-pilot.org"
# CookieCloud对应的浏览器UA
@@ -211,7 +211,11 @@ class Settings(BaseSettings):
# 大内存模式
BIG_MEMORY_MODE: bool = False
# 插件市场仓库地址,多个地址使用,分隔,地址以/结尾
PLUGIN_MARKET: str = "https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/"
PLUGIN_MARKET: str = "https://github.com/jxxghp/MoviePilot-Plugins"
# Github token提高请求api限流阈值 ghp_****
GITHUB_TOKEN: str = None
# 自动检查和更新站点资源包(站点索引、认证等)
AUTO_UPDATE_RESOURCE: bool = True
@property
def INNER_CONFIG_PATH(self):
@@ -321,6 +325,17 @@ class Settings(BaseSettings):
return Path(self.DOWNLOAD_ANIME_PATH)
return self.SAVE_TV_PATH
@property
def GITHUB_HEADERS(self):
"""
Github请求头
"""
if self.GITHUB_TOKEN:
return {
"Authorization": f"Bearer {self.GITHUB_TOKEN}"
}
return {}
def __init__(self, **kwargs):
super().__init__(**kwargs)
with self.CONFIG_PATH as p:
@@ -335,12 +350,6 @@ 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

@@ -414,24 +414,31 @@ class MediaInfo:
# 豆瓣ID
self.douban_id = str(info.get("id"))
# 类型
if not self.type:
if isinstance(info.get('media_type'), MediaType):
self.type = info.get('media_type')
else:
elif info.get("type"):
self.type = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
elif info.get("type_name"):
self.type = MediaType(info.get("type_name"))
# 标题
if not self.title:
self.title = info.get("title")
# 识别标题中的季
meta = MetaInfo(self.title)
self.season = meta.begin_season
# 原语种标题
if not self.original_title:
self.original_title = info.get("original_title")
# 年份
if not self.year:
self.year = info.get("year")[:4] if info.get("year") else None
# 识别标题中的季
meta = MetaInfo(info.get("title"))
# 季
if not self.season:
self.season = meta.begin_season
if self.season:
self.type = MediaType.TV
elif not self.type:
self.type = MediaType.MOVIE
# 评分
if not self.vote_average:
rating = info.get("rating")
@@ -472,14 +479,34 @@ class MediaInfo:
self.actors = info.get("actors") or []
# 别名
if not self.names:
self.names = info.get("aka") or []
akas = info.get("aka")
if akas:
self.names = [re.sub(r'\([港台豆友译名]+\)', "", aka) for aka in akas]
# 剧集
if self.type == MediaType.TV and not self.seasons:
meta = MetaInfo(info.get("title"))
if meta.begin_season:
episodes_count = info.get("episodes_count")
if episodes_count:
self.seasons[meta.begin_season] = list(range(1, episodes_count + 1))
season = meta.begin_season or 1
episodes_count = info.get("episodes_count")
if episodes_count:
self.seasons[season] = list(range(1, episodes_count + 1))
# 季年份
if self.type == MediaType.TV and not self.season_years:
season = self.season or 1
self.season_years = {
season: self.year
}
# 风格
if not self.genres:
self.genres = [{"id": genre, "name": genre} for genre in info.get("genres") or []]
# 时长
if not self.runtime and info.get("durations"):
# 查找数字
match = re.search(r'\d+', info.get("durations")[0])
if match:
self.runtime = int(match.group())
# 国家
if not self.production_countries:
self.production_countries = [{"id": country, "name": country} for country in info.get("countries") or []]
# 剩余属性赋值
for key, value in info.items():
if not hasattr(self, key):

View File

@@ -59,6 +59,9 @@ class MetaBase(object):
audio_encode: Optional[str] = None
# 应用的识别词信息
apply_words: Optional[List[str]] = None
# 附加信息
tmdbid: int = None
doubanid: str = None
# 副标题解析
_subtitle_flag = False

View File

@@ -34,6 +34,7 @@ class MetaVideo(MetaBase):
_name_no_begin_re = r"^\[.+?]"
_name_no_chinese_re = r".*版|.*字幕"
_name_se_words = ['', '', '', '', '', '', '']
_name_movie_words = ['剧场版', '劇場版', '电影版', '電影版']
_name_nostring_re = r"^PTS|^JADE|^AOD|^CHC|^[A-Z]{1,4}TV[\-0-9UVHDK]*" \
r"|HBO$|\s+HBO|\d{1,2}th|\d{1,2}bit|NETFLIX|AMAZON|IMAX|^3D|\s+3D|^BBC\s+|\s+BBC|BBC$|DISNEY\+?|XXX|\s+DC$" \
r"|[第\s共]+[0-9一二三四五六七八九十\-\s]+季" \
@@ -182,8 +183,9 @@ class MetaVideo(MetaBase):
if not self.cn_name:
self.cn_name = token
elif not self._stop_cnname_flag:
if not re.search("%s" % self._name_no_chinese_re, token, flags=re.IGNORECASE) \
and not re.search("%s" % self._name_se_words, token, flags=re.IGNORECASE):
if re.search("%s" % self._name_movie_words, token, flags=re.IGNORECASE) \
or (not re.search("%s" % self._name_no_chinese_re, token, flags=re.IGNORECASE)
and not re.search("%s" % self._name_se_words, token, flags=re.IGNORECASE)):
self.cn_name = "%s %s" % (self.cn_name, token)
self._stop_cnname_flag = True
else:

View File

@@ -36,6 +36,8 @@ def MetaInfo(title: str, subtitle: str = None) -> MetaBase:
# 修正媒体信息
if metainfo.get('tmdbid'):
meta.tmdbid = metainfo['tmdbid']
if metainfo.get('doubanid'):
meta.tmdbid = metainfo['doubanid']
if metainfo.get('type'):
meta.type = metainfo['type']
if metainfo.get('begin_season'):
@@ -93,6 +95,7 @@ def find_metainfo(title: str) -> Tuple[str, dict]:
"""
metainfo = {
'tmdbid': None,
'doubanid': None,
'type': None,
'begin_season': None,
'end_season': None,
@@ -108,10 +111,14 @@ def find_metainfo(title: str) -> Tuple[str, dict]:
if not results:
return title, metainfo
for result in results:
tmdbid = re.findall(r'(?<=tmdbid=)\d+', result)
# 查找tmdbid信息
tmdbid = re.findall(r'(?<=tmdbid=)\d+', result)
if tmdbid and tmdbid[0].isdigit():
metainfo['tmdbid'] = tmdbid[0]
# 查找豆瓣id信息
doubanid = re.findall(r'(?<=doubanid=)\d+', result)
if doubanid and doubanid[0].isdigit():
metainfo['doubanid'] = doubanid[0]
# 查找媒体类型
mtype = re.findall(r'(?<=type=)\d+', result)
if mtype:

View File

@@ -12,6 +12,7 @@ from app.schemas.types import SystemConfigKey
from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
class PluginManager(metaclass=Singleton):
@@ -30,11 +31,11 @@ class PluginManager(metaclass=Singleton):
def __init__(self):
self.siteshelper = SitesHelper()
self.pluginhelper = PluginHelper()
self.systemconfig = SystemConfigOper()
self.install_online_plugin()
self.init_config()
def init_config(self):
# 配置管理
self.systemconfig = SystemConfigOper()
# 停止已有插件
self.stop()
# 启动插件
@@ -44,7 +45,6 @@ class PluginManager(metaclass=Singleton):
"""
启动加载插件
"""
# 扫描插件目录
plugins = ModuleHelper.load(
"app.plugins",
@@ -102,6 +102,35 @@ class PluginManager(metaclass=Singleton):
self._plugins = {}
self._running_plugins = {}
def install_online_plugin(self):
"""
安装本地不存在的在线插件
"""
if SystemUtils.is_frozen():
return
logger.info("开始安装在线插件...")
# 已安装插件
install_plugins = self.systemconfig.get(SystemConfigKey.UserInstalledPlugins) or []
# 在线插件
online_plugins = self.get_online_plugins()
if not online_plugins:
logger.error("未获取到在线插件")
return
# 支持更新的插件自动更新
for plugin in online_plugins:
# 只处理已安装的插件
if plugin.get("id") in install_plugins and not self.is_plugin_exists(plugin.get("id")):
# 下载安装
state, msg = self.pluginhelper.install(pid=plugin.get("id"),
repo_url=plugin.get("repo_url"))
# 安装失败
if not state:
logger.error(
f"插件 {plugin.get('plugin_name')} v{plugin.get('plugin_version')} 安装失败:{msg}")
continue
logger.info(f"插件 {plugin.get('plugin_name')} 安装成功,版本:{plugin.get('plugin_version')}")
logger.info("在线插件安装完成")
def get_plugin_config(self, pid: str) -> dict:
"""
获取插件配置
@@ -118,6 +147,14 @@ class PluginManager(metaclass=Singleton):
return False
return self.systemconfig.set(self._config_key % pid, conf)
def delete_plugin_config(self, pid: str) -> bool:
"""
删除插件配置
"""
if not self._plugins.get(pid):
return False
return self.systemconfig.delete(self._config_key % pid)
def get_plugin_form(self, pid: str) -> Tuple[List[dict], Dict[str, Any]]:
"""
获取插件表单
@@ -229,7 +266,12 @@ class PluginManager(metaclass=Singleton):
conf.update({"has_update": True})
# 运行状态
if plugin_obj and hasattr(plugin_obj, "get_state"):
conf.update({"state": plugin_obj.get_state()})
try:
state = plugin_obj.get_state()
except Exception as e:
logger.error(f"获取插件 {pid} 状态出错:{str(e)}")
state = False
conf.update({"state": state})
else:
conf.update({"state": False})
# 是否有详情页面
@@ -254,9 +296,6 @@ class PluginManager(metaclass=Singleton):
# 图标
if plugin.get("icon"):
conf.update({"plugin_icon": plugin.get("icon")})
# 主题色
if plugin.get("color"):
conf.update({"plugin_color": plugin.get("color")})
# 作者
if plugin.get("author"):
conf.update({"plugin_author": plugin.get("author")})
@@ -293,7 +332,12 @@ class PluginManager(metaclass=Singleton):
conf.update({"installed": False})
# 运行状态
if plugin_obj and hasattr(plugin_obj, "get_state"):
conf.update({"state": plugin_obj.get_state()})
try:
state = plugin_obj.get_state()
except Exception as e:
logger.error(f"获取插件 {pid} 状态出错:{str(e)}")
state = False
conf.update({"state": state})
else:
conf.update({"state": False})
# 是否有详情页面
@@ -319,9 +363,6 @@ class PluginManager(metaclass=Singleton):
# 图标
if hasattr(plugin, "plugin_icon"):
conf.update({"plugin_icon": plugin.plugin_icon})
# 主题色
if hasattr(plugin, "plugin_color"):
conf.update({"plugin_color": plugin.plugin_color})
# 作者
if hasattr(plugin, "plugin_author"):
conf.update({"plugin_author": plugin.plugin_author})
@@ -335,3 +376,13 @@ class PluginManager(metaclass=Singleton):
# 汇总
all_confs.append(conf)
return all_confs
@staticmethod
def is_plugin_exists(pid: str) -> bool:
"""
判断插件是否存在
"""
if not pid:
return False
plugin_dir = settings.ROOT_PATH / "app" / "plugins" / pid.lower()
return plugin_dir.exists()

View File

@@ -52,6 +52,44 @@ def verify_token(token: str = Depends(reusable_oauth2)) -> schemas.TokenPayload:
)
def get_token(token: str = None) -> str:
"""
从请求URL中获取token
"""
return token
def get_apikey(apikey: str = None) -> str:
"""
从请求URL中获取apikey
"""
return apikey
def verify_uri_token(token: str = Depends(get_token)) -> str:
"""
通过依赖项使用token进行身份认证
"""
if token != settings.API_TOKEN:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="token校验不通过"
)
return token
def verify_uri_apikey(apikey: str = Depends(get_apikey)) -> str:
"""
通过依赖项使用apikey进行身份认证
"""
if apikey != settings.API_TOKEN:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="apikey校验不通过"
)
return apikey
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)

View File

@@ -1,7 +1,10 @@
from typing import Any, Self, List
from typing import Tuple, Optional, Generator
from sqlalchemy import create_engine, QueuePool
from sqlalchemy.orm import sessionmaker, Session, scoped_session
from sqlalchemy import inspect
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import sessionmaker, Session, scoped_session, as_declarative
from app.core.config import settings
@@ -135,6 +138,52 @@ def db_query(func):
return wrapper
@as_declarative()
class Base:
id: Any
__name__: str
@db_update
def create(self, db: Session):
db.add(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)
if inspect(self).detached:
db.add(self)
@classmethod
@db_update
def delete(cls, db: Session, rid):
db.query(cls).filter(cls.id == rid).delete()
@classmethod
@db_update
def truncate(cls, db: Session):
db.query(cls).delete()
@classmethod
@db_query
def list(cls, db: Session) -> List[Self]:
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}
@declared_attr
def __tablename__(self) -> str:
return self.__name__.lower()
class DbOper:
"""
数据库操作基类

View File

@@ -57,7 +57,14 @@ class DownloadHistoryOper(DbOper):
按fullpath查询下载文件记录
:param fullpath: 数据key
"""
return DownloadFiles.get_by_fullpath(self._db, fullpath)
return DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=False)
def get_files_by_fullpath(self, fullpath: str) -> List[DownloadFiles]:
"""
按fullpath查询下载文件记录
:param fullpath: 数据key
"""
return DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=True)
def get_files_by_savepath(self, fullpath: str) -> List[DownloadFiles]:
"""
@@ -78,7 +85,7 @@ class DownloadHistoryOper(DbOper):
按fullpath查询下载文件记录hash
:param fullpath: 数据key
"""
fileinfo: DownloadFiles = DownloadFiles.get_by_fullpath(self._db, fullpath)
fileinfo: DownloadFiles = DownloadFiles.get_by_fullpath(self._db, fullpath=fullpath, all_files=False)
if fileinfo:
return fileinfo.download_hash
return ""
@@ -115,3 +122,13 @@ class DownloadHistoryOper(DbOper):
return DownloadHistory.list_by_user_date(db=self._db,
date=date,
username=username)
def list_by_date(self, date: str, type: str, tmdbid: str, seasons: str = None) -> List[DownloadHistory]:
"""
查询某时间之后的下载历史
"""
return DownloadHistory.list_by_date(db=self._db,
date=date,
type=type,
tmdbid=tmdbid,
seasons=seasons)

View File

@@ -1,14 +1,10 @@
import importlib
from pathlib import Path
from alembic.command import upgrade
from alembic.config import Config
from app.core.config import settings
from app.core.security import get_password_hash
from app.db import Engine, SessionFactory
from app.db.models import Base
from app.db.models.user import User
from app.db import Engine, SessionFactory, Base
from app.db.models import *
from app.log import logger
@@ -16,21 +12,18 @@ def init_db():
"""
初始化数据库
"""
# 导入模块,避免建表缺失
for module in Path(__file__).with_name("models").glob("*.py"):
importlib.import_module(f"app.db.models.{module.stem}")
# 全量建表
Base.metadata.create_all(bind=Engine)
# 初始化超级管理员
with SessionFactory() as db:
user = User.get_by_name(db=db, name=settings.SUPERUSER)
if not user:
user = User(
_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)
_user.create(db)
def update_db():

View File

@@ -25,7 +25,7 @@ class MediaServerOper(DbOper):
return True
return False
def empty(self, server: str):
def empty(self, server: Optional[str] = None):
"""
清空媒体服务器数据
"""
@@ -39,10 +39,12 @@ class MediaServerOper(DbOper):
# 优先按TMDBID查
item = MediaServerItem.exist_by_tmdbid(self._db, tmdbid=kwargs.get("tmdbid"),
mtype=kwargs.get("mtype"))
else:
elif kwargs.get("title"):
# 按标题、类型、年份查
item = MediaServerItem.exists_by_title(self._db, title=kwargs.get("title"),
mtype=kwargs.get("mtype"), year=kwargs.get("year"))
else:
return None
if not item:
return None

View File

@@ -1,52 +1,9 @@
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
@db_update
def create(self, db: Session):
db.add(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)
if inspect(self).detached:
db.add(self)
@classmethod
@db_update
def delete(cls, db: Session, rid):
db.query(cls).filter(cls.id == rid).delete()
@classmethod
@db_update
def truncate(cls, db: Session):
db.query(cls).delete()
@classmethod
@db_query
def list(cls, db: Session) -> List[Self]:
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}
@declared_attr
def __tablename__(self) -> str:
return self.__name__.lower()
from .downloadhistory import DownloadHistory, DownloadFiles
from .mediaserver import MediaServerItem
from .plugindata import PluginData
from .site import Site
from .siteicon import SiteIcon
from .subscribe import Subscribe
from .systemconfig import SystemConfig
from .transferhistory import TransferHistory
from .user import User

View File

@@ -1,8 +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, db_update
from app.db import db_query, db_update, Base
class DownloadHistory(Base):
@@ -123,6 +122,24 @@ class DownloadHistory(Base):
DownloadHistory.id.desc()).all()
return list(result)
@staticmethod
@db_query
def list_by_date(db: Session, date: str, type: str, tmdbid: str, seasons: str = None):
"""
查询某时间之后的下载历史
"""
if seasons:
return db.query(DownloadHistory).filter(DownloadHistory.date > date,
DownloadHistory.type == type,
DownloadHistory.tmdbid == tmdbid,
DownloadHistory.seasons == seasons).order_by(
DownloadHistory.id.desc()).all()
else:
return db.query(DownloadHistory).filter(DownloadHistory.date > date,
DownloadHistory.type == type,
DownloadHistory.tmdbid == tmdbid).order_by(
DownloadHistory.id.desc()).all()
class DownloadFiles(Base):
"""
@@ -157,9 +174,13 @@ class DownloadFiles(Base):
@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()
def get_by_fullpath(db: Session, fullpath: str, all_files: bool = False):
if not all_files:
return db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath).order_by(
DownloadFiles.id.desc()).first()
else:
return db.query(DownloadFiles).filter(DownloadFiles.fullpath == fullpath).order_by(
DownloadFiles.id.desc()).all()
@staticmethod
@db_query

View File

@@ -1,15 +1,15 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_query
from app.db.models import Base, db_update
from app.db import db_query, db_update, Base
class MediaServerItem(Base):
"""
站点
媒体服务器媒体条目
"""
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
# 服务器类型
@@ -48,8 +48,11 @@ class MediaServerItem(Base):
@staticmethod
@db_update
def empty(db: Session, server: str):
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
def empty(db: Session, server: Optional[str] = None):
if server is None:
db.query(MediaServerItem).delete()
else:
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
@staticmethod
@db_query

View File

@@ -1,8 +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, db_update
from app.db import db_query, db_update, Base
class PluginData(Base):

View File

@@ -3,8 +3,7 @@ from datetime import datetime
from sqlalchemy import Boolean, Column, Integer, String, Sequence
from sqlalchemy.orm import Session
from app.db import db_query
from app.db.models import Base, db_update
from app.db import db_query, db_update, Base
class Site(Base):

View File

@@ -1,8 +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
from app.db import db_query, Base
class SiteIcon(Base):

View File

@@ -1,8 +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
from app.db import db_query, db_update, Base
class Subscribe(Base):
@@ -69,11 +68,15 @@ class Subscribe(Base):
@staticmethod
@db_query
def exists(db: Session, tmdbid: int, season: int = None):
if season:
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
Subscribe.season == season).first()
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).first()
def exists(db: Session, tmdbid: int = None, doubanid: str = None, season: int = None):
if tmdbid:
if season:
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid,
Subscribe.season == season).first()
return db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).first()
elif doubanid:
return db.query(Subscribe).filter(Subscribe.doubanid == doubanid).first()
return None
@staticmethod
@db_query
@@ -93,7 +96,10 @@ class Subscribe(Base):
@staticmethod
@db_query
def get_by_title(db: Session, title: str):
def get_by_title(db: Session, title: str, season: int = None):
if season:
return db.query(Subscribe).filter(Subscribe.name == title,
Subscribe.season == season).first()
return db.query(Subscribe).filter(Subscribe.name == title).first()
@staticmethod

View File

@@ -1,8 +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
from app.db import db_query, db_update, Base
class SystemConfig(Base):

View File

@@ -3,8 +3,7 @@ import time
from sqlalchemy import Column, Integer, String, Sequence, Boolean, func
from sqlalchemy.orm import Session
from app.db import db_query
from app.db.models import Base, db_update
from app.db import db_query, db_update, Base
class TransferHistory(Base):

View File

@@ -2,8 +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
from app.db import db_query, db_update, Base
class User(Base):

View File

@@ -2,7 +2,7 @@ import json
from typing import Any
from app.db import DbOper
from app.db.models.plugin import PluginData
from app.db.models.plugindata import PluginData
from app.utils.object import ObjectUtils

View File

@@ -15,7 +15,10 @@ class SubscribeOper(DbOper):
"""
新增订阅
"""
subscribe = Subscribe.exists(self._db, tmdbid=mediainfo.tmdb_id, season=kwargs.get('season'))
subscribe = Subscribe.exists(self._db,
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id,
season=kwargs.get('season'))
if not subscribe:
subscribe = Subscribe(name=mediainfo.title,
year=mediainfo.year,
@@ -23,6 +26,7 @@ class SubscribeOper(DbOper):
tmdbid=mediainfo.tmdb_id,
imdbid=mediainfo.imdb_id,
tvdbid=mediainfo.tvdb_id,
doubanid=mediainfo.douban_id,
poster=mediainfo.get_poster_image(),
backdrop=mediainfo.get_backdrop_image(),
vote=mediainfo.vote_average,
@@ -31,19 +35,26 @@ class SubscribeOper(DbOper):
**kwargs)
subscribe.create(self._db)
# 查询订阅
subscribe = Subscribe.exists(self._db, tmdbid=mediainfo.tmdb_id, season=kwargs.get('season'))
subscribe = Subscribe.exists(self._db,
tmdbid=mediainfo.tmdb_id,
doubanid=mediainfo.douban_id,
season=kwargs.get('season'))
return subscribe.id, "新增订阅成功"
else:
return subscribe.id, "订阅已存在"
def exists(self, tmdbid: int, season: int) -> bool:
def exists(self, tmdbid: int = None, doubanid: str = None, season: int = None) -> bool:
"""
判断是否存在
"""
if season:
return True if Subscribe.exists(self._db, tmdbid=tmdbid, season=season) else False
else:
return True if Subscribe.exists(self._db, tmdbid=tmdbid) else False
if tmdbid:
if season:
return True if Subscribe.exists(self._db, tmdbid=tmdbid, season=season) else False
else:
return True if Subscribe.exists(self._db, tmdbid=tmdbid) else False
elif doubanid:
return True if Subscribe.exists(self._db, doubanid=doubanid) else False
return False
def get(self, sid: int) -> Subscribe:
"""

View File

@@ -56,6 +56,20 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
return self.__SYSTEMCONF
return self.__SYSTEMCONF.get(key)
def delete(self, key: Union[str, SystemConfigKey]):
"""
删除系统设置
"""
if isinstance(key, SystemConfigKey):
key = key.value
# 更新内存
self.__SYSTEMCONF.pop(key, None)
# 写入数据库
conf = SystemConfig.get_by_key(self._db, key)
if conf:
conf.delete(self._db, conf.id)
return True
def __del__(self):
if self._db:
self._db.close()

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import importlib
import pkgutil
from pathlib import Path
class ModuleHelper:
@@ -35,3 +36,17 @@ class ModuleHelper:
print(f'加载模块 {package_name} 失败:{err}')
return submodules
@staticmethod
def dynamic_import_all_modules(base_path: Path, package_name: str):
"""
动态导入目录下所有模块
"""
modules = []
# 遍历文件夹,找到所有模块文件
for file in base_path.glob("*.py"):
file_name = file.stem
if file_name != "__init__":
modules.append(file_name)
full_module_name = f"{package_name}.{file_name}"
importlib.import_module(full_module_name)

View File

@@ -16,6 +16,8 @@ class PluginHelper(metaclass=Singleton):
插件市场管理,下载安装插件到本地
"""
_base_url = "https://raw.githubusercontent.com/%s/%s/main/"
@cached(cache=TTLCache(maxsize=10, ttl=1800))
def get_plugins(self, repo_url: str) -> Dict[str, dict]:
"""
@@ -24,32 +26,53 @@ class PluginHelper(metaclass=Singleton):
"""
if not repo_url:
return {}
res = RequestUtils(proxies=settings.PROXY, timeout=10).get_res(f"{repo_url}package.json")
user, repo = self.get_repo_info(repo_url)
if not user or not repo:
return {}
raw_url = self._base_url % (user, repo)
res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS,
timeout=10).get_res(f"{raw_url}package.json")
if res:
return json.loads(res.text)
return {}
@staticmethod
def install(pid: str, repo_url: str) -> Tuple[bool, str]:
def get_repo_info(repo_url: str) -> Tuple[Optional[str], Optional[str]]:
"""
安装插件
获取Github仓库信息
:param repo_url: Github仓库地址
"""
# 从Github的repo_url获取用户和项目名
if not repo_url:
return None, None
if not repo_url.endswith("/"):
repo_url += "/"
if repo_url.count("/") < 6:
repo_url = f"{repo_url}main/"
try:
user, repo = repo_url.split("/")[-4:-2]
except Exception as e:
return False, f"不支持的插件仓库地址格式:{str(e)}"
if not user or not repo:
return False, "不支持的插件仓库地址格式"
print(str(e))
return None, None
return user, repo
def install(self, pid: str, repo_url: str) -> Tuple[bool, str]:
"""
安装插件
"""
if SystemUtils.is_frozen():
return False, "可执行文件模式下,只能安装本地插件"
# 从Github的repo_url获取用户和项目名
user, repo = self.get_repo_info(repo_url)
if not user or not repo:
return False, "不支持的插件仓库地址格式"
def __get_filelist(_p: str) -> Tuple[Optional[list], Optional[str]]:
"""
获取插件的文件列表
"""
file_api = f"https://api.github.com/repos/{user}/{repo}/contents/plugins/{_p.lower()}"
r = RequestUtils(proxies=settings.PROXY).get_res(file_api)
r = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS, timeout=30).get_res(file_api)
if not r or r.status_code != 200:
return None, f"连接仓库失败:{r.status_code} - {r.reason}"
ret = r.json()
@@ -66,7 +89,8 @@ class PluginHelper(metaclass=Singleton):
for item in _l:
if item.get("download_url"):
# 下载插件文件
res = RequestUtils(proxies=settings.PROXY).get_res(item["download_url"])
res = RequestUtils(proxies=settings.PROXY,
headers=settings.GITHUB_HEADERS, timeout=60).get_res(item["download_url"])
if not res:
return False, f"文件 {item.get('name')} 下载失败!"
elif res.status_code != 200:
@@ -83,7 +107,7 @@ class PluginHelper(metaclass=Singleton):
l, m = __get_filelist(p)
if not l:
return False, m
return __download_files(p, l)
__download_files(p, l)
return True, ""
if not pid or not repo_url:
@@ -120,4 +144,8 @@ class PluginHelper(metaclass=Singleton):
shutil.rmtree(plugin_dir, ignore_errors=True)
# 下载所有文件
__download_files(pid.lower(), file_list)
# 插件目录下如有requirements.txt则安装依赖
requirements_file = plugin_dir / "requirements.txt"
if requirements_file.exists():
SystemUtils.execute(f"pip install -r {requirements_file}")
return True, ""

103
app/helper/resource.py Normal file
View File

@@ -0,0 +1,103 @@
import json
from pathlib import Path
from app.core.config import settings
from app.helper.sites import SitesHelper
from app.log import logger
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
class ResourceHelper(metaclass=Singleton):
"""
检测和更新资源包
"""
# 资源包的git仓库地址
_repo = "https://raw.githubusercontent.com/jxxghp/MoviePilot-Resources/main/package.json"
_files_api = f"https://api.github.com/repos/jxxghp/MoviePilot-Resources/contents/resources"
_base_dir: Path = settings.ROOT_PATH
def __init__(self):
self.siteshelper = SitesHelper()
self.check()
def check(self):
"""
检测是否有更新,如有则下载安装
"""
if not settings.AUTO_UPDATE_RESOURCE:
return
if SystemUtils.is_frozen():
return
logger.info("开始检测资源包版本...")
res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS, timeout=10).get_res(self._repo)
if res:
resource_info = json.loads(res.text)
else:
logger.warn("无法连接资源包仓库!")
return
online_version = resource_info.get("version")
if online_version:
logger.info(f"最新资源包版本v{online_version}")
# 需要更新的资源包
need_updates = {}
# 资源明细
resources: dict = resource_info.get("resources") or {}
for rname, resource in resources.items():
rtype = resource.get("type")
platform = resource.get("platform")
target = resource.get("target")
version = resource.get("version")
# 判断平台
if platform and platform != SystemUtils.platform:
continue
# 判断本地是否存在
local_path = self._base_dir / target / rname
if not local_path.exists():
continue
# 判断版本号
if rtype == "auth":
# 站点认证资源
local_version = self.siteshelper.auth_version
elif rtype == "sites":
# 站点索引资源
local_version = self.siteshelper.indexer_version
else:
continue
if StringUtils.compare_version(version, local_version) > 0:
logger.info(f"{rname} 资源包有更新最新版本v{version}")
else:
continue
# 需要安装
need_updates[rname] = target
if need_updates:
# 下载文件信息列表
r = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS,
timeout=30).get_res(self._files_api)
if not r or r.status_code != 200:
return None, f"连接仓库失败:{r.status_code} - {r.reason}"
files_info = r.json()
for item in files_info:
save_path = need_updates.get(item.get("name"))
if not save_path:
continue
if item.get("download_url"):
# 下载资源文件
res = RequestUtils(proxies=settings.PROXY, headers=settings.GITHUB_HEADERS,
timeout=180).get_res(item["download_url"])
if not res:
logger.error(f"文件 {item.get('name')} 下载失败!")
elif res.status_code != 200:
logger.error(f"下载文件 {item.get('name')} 失败:{res.status_code} - {res.reason}")
# 创建插件文件夹
file_path = self._base_dir / save_path / item.get("name")
if not file_path.parent.exists():
file_path.parent.mkdir(parents=True, exist_ok=True)
# 写入文件
file_path.write_bytes(res.content)
logger.info("资源包更新完成,开始重启服务...")
SystemUtils.restart()
else:
logger.info("所有资源已最新,无需更新")

View File

@@ -1,11 +1,14 @@
import re
import xml.dom.minidom
from typing import List, Tuple, Union
from urllib.parse import urljoin
import chardet
from lxml import etree
from app.core.config import settings
from app.helper.browser import PlaywrightHelper
from app.log import logger
from app.utils.dom import DomUtils
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
@@ -240,8 +243,28 @@ class RssHelper:
print(str(err))
return []
if ret:
ret_xml = ret.text
ret_xml = ""
try:
# 使用chardet检测字符编码
raw_data = ret.content
if raw_data:
try:
result = chardet.detect(raw_data)
encoding = result['encoding']
# 解码为字符串
ret_xml = raw_data.decode(encoding)
except Exception as e:
logger.debug(f"chardet解码失败{str(e)}")
# 探测utf-8解码
match = re.search(r'encoding\s*=\s*["\']([^"\']+)["\']', ret.text)
if match:
encoding = match.group(1)
if encoding:
ret_xml = raw_data.decode(encoding)
else:
ret.encoding = ret.apparent_encoding
if not ret_xml:
ret_xml = ret.text
# 解析XML
dom_tree = xml.dom.minidom.parseString(ret_xml)
rootNode = dom_tree.documentElement

View File

@@ -14,13 +14,17 @@ from app.db.systemconfig_oper import SystemConfigOper
from app.log import logger
from app.utils.http import RequestUtils
from app.schemas.types import MediaType, SystemConfigKey
from app.utils.singleton import Singleton
class TorrentHelper:
class TorrentHelper(metaclass=Singleton):
"""
种子帮助类
"""
# 失败的种子:站点链接
_invalid_torrents = []
def __init__(self):
self.system_config = SystemConfigOper()
@@ -123,6 +127,8 @@ class TorrentHelper:
elif req.status_code == 429:
return None, None, "", [], "触发站点流控,请稍后重试"
else:
# 把错误的种子记下来,避免重复使用
self.add_invalid(url)
return None, None, "", [], f"下载种子出错,状态码:{req.status_code}"
@staticmethod
@@ -276,3 +282,16 @@ class TorrentHelper:
continue
episodes = list(set(episodes).union(set(meta.episode_list)))
return episodes
def is_invalid(self, url: str) -> bool:
"""
判断种子是否是无效种子
"""
return url in self._invalid_torrents
def add_invalid(self, url: str):
"""
添加无效种子
"""
if url not in self._invalid_torrents:
self._invalid_torrents.append(url)

View File

@@ -22,10 +22,12 @@ 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.resource import ResourceHelper
from app.helper.sites import SitesHelper
from app.scheduler import Scheduler
from app.command import Command
# App
App = FastAPI(title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json")
@@ -60,7 +62,9 @@ def start_frontend():
"""
启动前端服务
"""
if not SystemUtils.is_frozen():
# 仅Windows可执行文件支持内嵌nginx
if not SystemUtils.is_frozen() \
or not SystemUtils.is_windows():
return
# 临时Nginx目录
nginx_path = settings.ROOT_PATH / 'nginx'
@@ -73,27 +77,20 @@ def start_frontend():
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)
subprocess.Popen("start nginx.exe",
cwd=run_nginx_dir,
shell=True)
def stop_frontend():
"""
停止前端服务
"""
if not SystemUtils.is_frozen():
if not SystemUtils.is_frozen() \
or not SystemUtils.is_windows():
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)
subprocess.Popen(f"taskkill /f /im nginx.exe", shell=True)
def start_tray():
@@ -104,6 +101,9 @@ def start_tray():
if not SystemUtils.is_frozen():
return
if not SystemUtils.is_windows():
return
def open_web():
"""
调用浏览器打开前端页面
@@ -169,6 +169,8 @@ def start_module():
DisplayHelper()
# 站点管理
SitesHelper()
# 资源包检测
ResourceHelper()
# 加载模块
ModuleManager()
# 加载插件

View File

@@ -9,6 +9,7 @@ from app.core.metainfo import MetaInfo
from app.log import logger
from app.modules import _ModuleBase
from app.modules.douban.apiv2 import DoubanApi
from app.modules.douban.douban_cache import DoubanCache
from app.modules.douban.scraper import DoubanScraper
from app.schemas.types import MediaType
from app.utils.common import retry
@@ -18,10 +19,12 @@ from app.utils.system import SystemUtils
class DoubanModule(_ModuleBase):
doubanapi: DoubanApi = None
scraper: DoubanScraper = None
cache: DoubanCache = None
def init_module(self) -> None:
self.doubanapi = DoubanApi()
self.scraper = DoubanScraper()
self.cache = DoubanCache()
def stop(self):
pass
@@ -29,10 +32,87 @@ class DoubanModule(_ModuleBase):
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
def douban_info(self, doubanid: str) -> Optional[dict]:
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
doubanid: str = None,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息
:param meta: 识别的元数据
:param mtype: 识别的媒体类型与doubanid配套
:param doubanid: 豆瓣ID
:return: 识别的媒体信息,包括剧集信息
"""
if settings.RECOGNIZE_SOURCE != "douban":
return None
if not meta:
cache_info = {}
else:
if mtype:
meta.type = mtype
cache_info = self.cache.get(meta)
if not cache_info:
# 缓存没有或者强制不使用缓存
if doubanid:
# 直接查询详情
info = self.douban_info(doubanid=doubanid, mtype=mtype or meta.type)
elif meta:
if meta.begin_season:
logger.info(f"正在识别 {meta.name}{meta.begin_season}季 ...")
else:
logger.info(f"正在识别 {meta.name} ...")
# 匹配豆瓣信息
match_info = self.match_doubaninfo(name=meta.name,
mtype=mtype or meta.type,
year=meta.year,
season=meta.begin_season)
if match_info:
# 匹配到豆瓣信息
info = self.douban_info(
doubanid=match_info.get("id"),
mtype=mtype or meta.type
)
else:
logger.info(f"{meta.name if meta else doubanid} 未匹配到豆瓣媒体信息")
return None
else:
logger.error("识别媒体信息时未提供元数据或豆瓣ID")
return None
# 保存到缓存
if meta:
self.cache.update(meta, info)
else:
# 使用缓存信息
if cache_info.get("title"):
logger.info(f"{meta.name} 使用豆瓣识别缓存:{cache_info.get('title')}")
info = self.douban_info(mtype=cache_info.get("type"),
doubanid=cache_info.get("id"))
else:
logger.info(f"{meta.name} 使用豆瓣识别缓存:无法识别")
info = None
if info:
# 赋值TMDB信息并返回
mediainfo = MediaInfo(douban_info=info)
if meta:
logger.info(f"{meta.name} 豆瓣识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year} "
f"{mediainfo.douban_id}")
else:
logger.info(f"{doubanid} 豆瓣识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year}")
return mediainfo
else:
logger.info(f"{meta.name if meta else doubanid} 未匹配到豆瓣媒体信息")
return None
def douban_info(self, doubanid: str, mtype: MediaType = None) -> Optional[dict]:
"""
获取豆瓣信息
:param doubanid: 豆瓣ID
:param mtype: 媒体类型
:return: 豆瓣信息
"""
"""
@@ -300,22 +380,40 @@ class DoubanModule(_ModuleBase):
"interest_cmt_earlier_tip_desc": "该短评的发布时间早于公开上映时间,作者可能通过其他渠道提前观看,请谨慎参考。其评分将不计入总评分。"
}
"""
def __douban_tv():
"""
获取豆瓣剧集信息
"""
info = self.doubanapi.tv_detail(doubanid)
if info:
celebrities = self.doubanapi.tv_celebrities(doubanid)
if celebrities:
info["directors"] = celebrities.get("directors")
info["actors"] = celebrities.get("actors")
return info
def __douban_movie():
"""
获取豆瓣电影信息
"""
info = self.doubanapi.movie_detail(doubanid)
if info:
celebrities = self.doubanapi.movie_celebrities(doubanid)
if celebrities:
info["directors"] = celebrities.get("directors")
info["actors"] = celebrities.get("actors")
return info
if not doubanid:
return None
logger.info(f"开始获取豆瓣信息:{doubanid} ...")
douban_info = self.doubanapi.movie_detail(doubanid)
if douban_info:
celebrities = self.doubanapi.movie_celebrities(doubanid)
if celebrities:
douban_info["directors"] = celebrities.get("directors")
douban_info["actors"] = celebrities.get("actors")
if mtype == MediaType.TV:
return __douban_tv()
elif mtype == MediaType.MOVIE:
return __douban_movie()
else:
douban_info = self.doubanapi.tv_detail(doubanid)
celebrities = self.doubanapi.tv_celebrities(doubanid)
if douban_info and celebrities:
douban_info["directors"] = celebrities.get("directors")
douban_info["actors"] = celebrities.get("actors")
return douban_info
return __douban_movie() or __douban_tv()
def douban_discover(self, mtype: MediaType, sort: str, tags: str,
page: int = 1, count: int = 30) -> Optional[List[dict]]:
@@ -379,6 +477,26 @@ class DoubanModule(_ModuleBase):
return []
return infos.get("subject_collection_items")
def movie_hot(self, page: int = 1, count: int = 30) -> List[dict]:
"""
获取豆瓣热门电影
"""
infos = self.doubanapi.movie_hot_gaia(start=(page - 1) * count,
count=count)
if not infos:
return []
return infos.get("subject_collection_items")
def tv_hot(self, page: int = 1, count: int = 30) -> List[dict]:
"""
获取豆瓣热门剧集
"""
infos = self.doubanapi.tv_hot(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]]:
"""
搜索媒体信息
@@ -386,7 +504,7 @@ class DoubanModule(_ModuleBase):
:reutrn: 媒体信息
"""
# 未启用豆瓣搜索时返回None
if settings.SEARCH_SOURCE != "douban":
if settings.RECOGNIZE_SOURCE != "douban":
return None
if not meta.name:
@@ -397,7 +515,7 @@ class DoubanModule(_ModuleBase):
# 返回数据
ret_medias = []
for item_obj in result.get("items"):
if meta.type and meta.type.value != item_obj.get("type_name"):
if meta.type and meta.type != MediaType.UNKNOWN and meta.type.value != item_obj.get("type_name"):
continue
if item_obj.get("type_name") not in (MediaType.TV.value, MediaType.MOVIE.value):
continue
@@ -407,12 +525,12 @@ class DoubanModule(_ModuleBase):
@retry(Exception, 5, 3, 3, logger=logger)
def match_doubaninfo(self, name: str, imdbid: str = None,
mtype: str = None, year: str = None, season: int = None) -> dict:
mtype: MediaType = None, year: str = None, season: int = None) -> dict:
"""
搜索和匹配豆瓣信息
:param name: 名称
:param imdbid: IMDB ID
:param mtype: 类型 电影/电视剧
:param mtype: 类型
:param year: 年份
:param season: 季号
"""
@@ -441,9 +559,9 @@ class DoubanModule(_ModuleBase):
type_name = item_obj.get("type_name")
if type_name not in [MediaType.TV.value, MediaType.MOVIE.value]:
continue
if mtype and mtype != type_name:
if mtype and mtype.value != type_name:
continue
if mtype == MediaType.TV and not season:
if mtype and mtype == MediaType.TV and not season:
season = 1
item = item_obj.get("target")
title = item.get("title")
@@ -486,21 +604,31 @@ class DoubanModule(_ModuleBase):
meta = MetaInfo(path.stem)
if not meta.name:
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"))
if not mediainfo.douban_id:
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type,
year=mediainfo.year)
if not doubaninfo:
logger.warn(f"未找到 {mediainfo.title} 的豆瓣信息")
return
doubaninfo = self.douban_info(doubanid=doubaninfo.get("id"), mtype=mediainfo.type)
else:
doubaninfo = self.douban_info(doubanid=mediainfo.douban_id,
mtype=mediainfo.type)
if not doubaninfo:
logger(f"未获取到 {mediainfo.douban_id} 的豆瓣媒体信息,无法刮削!")
return
# 豆瓣媒体信息
mediainfo = MediaInfo(douban_info=doubaninfo)
# 补充图片
self.obtain_images(mediainfo)
# 刮削路径
scrape_path = path / path.name
self.scraper.gen_scraper_files(meta=meta,
mediainfo=MediaInfo(douban_info=doubaninfo),
mediainfo=mediainfo,
file_path=scrape_path,
transfer_type=transfer_type)
else:
@@ -513,22 +641,117 @@ class DoubanModule(_ModuleBase):
meta = MetaInfo(file.stem)
if not meta.name:
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 mediainfo.douban_id:
# 根据名称查询豆瓣数据
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
imdbid=mediainfo.imdb_id,
mtype=mediainfo.type,
year=mediainfo.year,
season=meta.begin_season)
if not doubaninfo:
logger.warn(f"未找到 {mediainfo.title} 的豆瓣信息")
break
# 查询豆瓣详情
doubaninfo = self.douban_info(doubanid=doubaninfo.get("id"), mtype=mediainfo.type)
else:
doubaninfo = self.douban_info(doubanid=mediainfo.douban_id,
mtype=mediainfo.type)
if not doubaninfo:
logger.warn(f"{mediainfo.title} 的豆瓣信息")
break
# 查询豆瓣详情
doubaninfo = self.douban_info(doubaninfo.get("id"))
logger(f"获取{mediainfo.douban_id} 的豆瓣媒体信息,无法刮削!")
continue
# 豆瓣媒体信息
mediainfo = MediaInfo(douban_info=doubaninfo)
# 补充图片
self.obtain_images(mediainfo)
# 刮削
self.scraper.gen_scraper_files(meta=meta,
mediainfo=MediaInfo(douban_info=doubaninfo),
mediainfo=mediainfo,
file_path=file,
transfer_type=transfer_type)
except Exception as e:
logger.error(f"刮削文件 {file} 失败,原因:{str(e)}")
logger.info(f"{path} 刮削完成")
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
"""
补充抓取媒体信息图片
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
if settings.RECOGNIZE_SOURCE != "douban":
return None
if not mediainfo.douban_id:
return None
if mediainfo.backdrop_path:
# 没有图片缺失
return mediainfo
# 调用图片接口
if not mediainfo.backdrop_path:
if mediainfo.type == MediaType.MOVIE:
info = self.doubanapi.movie_photos(mediainfo.douban_id)
else:
info = self.doubanapi.tv_photos(mediainfo.douban_id)
if not info:
return mediainfo
images = info.get("photos")
# 背景图
if images:
backdrop = images[0].get("image", {}).get("large") or {}
if backdrop:
mediainfo.backdrop_path = backdrop.get("url")
return mediainfo
def clear_cache(self):
"""
清除缓存
"""
logger.info("开始清除豆瓣缓存 ...")
self.doubanapi.clear_cache()
self.cache.clear()
logger.info("豆瓣缓存清除完成")
def douban_movie_credits(self, doubanid: str, page: int = 1, count: int = 20) -> List[dict]:
"""
根据TMDBID查询电影演职员表
:param doubanid: 豆瓣ID
:param page: 页码
:param count: 数量
"""
result = self.doubanapi.movie_celebrities(subject_id=doubanid)
if not result:
return []
ret_list = result.get("actors") or []
if ret_list:
return ret_list[(page - 1) * count: page * count]
else:
return []
def douban_tv_credits(self, doubanid: str, page: int = 1, count: int = 20) -> List[dict]:
"""
根据TMDBID查询电视剧演职员表
:param doubanid: 豆瓣ID
:param page: 页码
:param count: 数量
"""
result = self.doubanapi.tv_celebrities(subject_id=doubanid)
if not result:
return []
ret_list = result.get("actors") or []
if ret_list:
return ret_list[(page - 1) * count: page * count]
else:
return []
def douban_movie_recommend(self, doubanid: str) -> List[dict]:
"""
根据豆瓣ID查询推荐电影
:param doubanid: 豆瓣ID
"""
return self.doubanapi.movie_recommendations(subject_id=doubanid) or []
def douban_tv_recommend(self, doubanid: str) -> List[dict]:
"""
根据豆瓣ID查询推荐电视剧
:param doubanid: 豆瓣ID
"""
return self.doubanapi.tv_recommendations(subject_id=doubanid) or []

View File

@@ -427,6 +427,60 @@ class DoubanApi(metaclass=Singleton):
return self.__invoke(self._urls["doulist_items"] % subject_id,
start=start, count=count, _ts=ts)
def movie_recommendations(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影推荐
:param subject_id: 电影id
:param start: 开始
:param count: 数量
:param ts: 时间戳
"""
return self.__invoke(self._urls["movie_recommendations"] % subject_id,
start=start, count=count, _ts=ts)
def tv_recommendations(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧推荐
:param subject_id: 电视剧id
:param start: 开始
:param count: 数量
:param ts: 时间戳
"""
return self.__invoke(self._urls["tv_recommendations"] % subject_id,
start=start, count=count, _ts=ts)
def movie_photos(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电影剧照
:param subject_id: 电影id
:param start: 开始
:param count: 数量
:param ts: 时间戳
"""
return self.__invoke(self._urls["movie_photos"] % subject_id,
start=start, count=count, _ts=ts)
def tv_photos(self, subject_id: str, start: int = 0, count: int = 20,
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
电视剧剧照
:param subject_id: 电视剧id
:param start: 开始
:param count: 数量
:param ts: 时间戳
"""
return self.__invoke(self._urls["tv_photos"] % subject_id,
start=start, count=count, _ts=ts)
def clear_cache(self):
"""
清空LRU缓存
"""
self.__invoke.cache_clear()
def __del__(self):
if self._session:
self._session.close()

View File

@@ -0,0 +1,232 @@
import pickle
import random
import time
from pathlib import Path
from threading import RLock
from typing import Optional
from app.core.config import settings
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.utils.singleton import Singleton
from app.schemas.types import MediaType
lock = RLock()
CACHE_EXPIRE_TIMESTAMP_STR = "cache_expire_timestamp"
EXPIRE_TIMESTAMP = settings.CACHE_CONF.get('meta')
class DoubanCache(metaclass=Singleton):
"""
豆瓣缓存数据
{
"id": '',
"title": '',
"year": '',
"type": MediaType
}
"""
_meta_data: dict = {}
# 缓存文件路径
_meta_path: Path = None
# TMDB缓存过期
_tmdb_cache_expire: bool = True
def __init__(self):
self._meta_path = settings.TEMP_PATH / "__douban_cache__"
self._meta_data = self.__load(self._meta_path)
def clear(self):
"""
清空所有TMDB缓存
"""
with lock:
self._meta_data = {}
@staticmethod
def __get_key(meta: MetaBase) -> str:
"""
获取缓存KEY
"""
return f"[{meta.type.value if meta.type else '未知'}]{meta.name}-{meta.year}-{meta.begin_season}"
def get(self, meta: MetaBase):
"""
根据KEY值获取缓存值
"""
key = self.__get_key(meta)
with lock:
info: dict = self._meta_data.get(key)
if info:
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire or int(time.time()) < expire:
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
self._meta_data[key] = info
elif expire and self._tmdb_cache_expire:
self.delete(key)
return info or {}
def delete(self, key: str) -> dict:
"""
删除缓存信息
@param key: 缓存key
@return: 被删除的缓存内容
"""
with lock:
return self._meta_data.pop(key, None)
def delete_by_doubanid(self, doubanid: str) -> None:
"""
清空对应豆瓣ID的所有缓存记录以强制更新TMDB中最新的数据
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == doubanid:
with lock:
self._meta_data.pop(key)
def delete_unknown(self) -> None:
"""
清除未识别的缓存记录以便重新搜索TMDB
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == "0":
with lock:
self._meta_data.pop(key)
def modify(self, key: str, title: str) -> dict:
"""
删除缓存信息
@param key: 缓存key
@param title: 标题
@return: 被修改后缓存内容
"""
with lock:
if self._meta_data.get(key):
self._meta_data[key]['title'] = title
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
return self._meta_data.get(key)
@staticmethod
def __load(path: Path) -> dict:
"""
从文件中加载缓存
"""
try:
if path.exists():
with open(path, 'rb') as f:
data = pickle.load(f)
return data
return {}
except Exception as e:
print(str(e))
return {}
def update(self, meta: MetaBase, info: dict) -> None:
"""
新增或更新缓存条目
"""
with lock:
if info:
# 缓存标题
cache_title = info.get("title")
# 缓存年份
cache_year = info.get('year')
# 类型
if isinstance(info.get('media_type'), MediaType):
mtype = info.get('media_type')
elif info.get("type"):
mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
else:
meta = MetaInfo(cache_title)
if meta.begin_season:
mtype = MediaType.TV
else:
mtype = MediaType.MOVIE
# 海报
poster_path = info.get("pic", {}).get("large")
if not poster_path and info.get("cover_url"):
poster_path = info.get("cover_url")
if not poster_path and info.get("cover"):
poster_path = info.get("cover").get("url")
self._meta_data[self.__get_key(meta)] = {
"id": info.get("id"),
"type": mtype,
"year": cache_year,
"title": cache_title,
"poster_path": poster_path,
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
}
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': "0"}
def save(self, force: bool = False) -> None:
"""
保存缓存数据到文件
"""
meta_data = self.__load(self._meta_path)
new_meta_data = {k: v for k, v in self._meta_data.items() if v.get("id")}
if not force \
and not self._random_sample(new_meta_data) \
and meta_data.keys() == new_meta_data.keys():
return
with open(self._meta_path, 'wb') as f:
pickle.dump(new_meta_data, f, pickle.HIGHEST_PROTOCOL)
def _random_sample(self, new_meta_data: dict) -> bool:
"""
采样分析是否需要保存
"""
ret = False
if len(new_meta_data) < 25:
keys = list(new_meta_data.keys())
for k in keys:
info = new_meta_data.get(k)
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire:
ret = True
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire:
ret = True
if self._tmdb_cache_expire:
new_meta_data.pop(k)
else:
count = 0
keys = random.sample(sorted(new_meta_data.keys()), 25)
for k in keys:
info = new_meta_data.get(k)
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire:
ret = True
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire:
ret = True
if self._tmdb_cache_expire:
new_meta_data.pop(k)
count += 1
if count >= 5:
ret |= self._random_sample(new_meta_data)
return ret
def get_title(self, key: str) -> Optional[str]:
"""
获取缓存的标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info or not cache_media_info.get("id"):
return None
return cache_media_info.get("title")
def set_title(self, key: str, cn_title: str) -> None:
"""
重新设置缓存标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info:
return
self._meta_data[key]['title'] = cn_title

View File

@@ -41,6 +41,10 @@ class DoubanScraper:
# 生成电影图片
self.__save_image(url=mediainfo.poster_path,
file_path=file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}"))
# 背景图
if mediainfo.backdrop_path:
self.__save_image(url=mediainfo.backdrop_path,
file_path=file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}"))
# 电视剧
else:
# 不存在时才处理
@@ -51,6 +55,10 @@ class DoubanScraper:
# 生成根目录图片
self.__save_image(url=mediainfo.poster_path,
file_path=file_path.with_name(f"poster{Path(mediainfo.poster_path).suffix}"))
# 背景图
if mediainfo.backdrop_path:
self.__save_image(url=mediainfo.backdrop_path,
file_path=file_path.with_name(f"backdrop{Path(mediainfo.backdrop_path).suffix}"))
# 季目录NFO
if not file_path.with_name("season.nfo").exists():
self.__gen_tv_season_nfo_file(mediainfo=mediainfo,

View File

@@ -366,7 +366,7 @@ class Emby(metaclass=Singleton):
season_episodes[season_index] = []
season_episodes[season_index].append(episode_index)
# 返回
return tv_item.get("Id"), season_episodes
return item_id, season_episodes
except Exception as e:
logger.error(f"连接Shows/Id/Episodes出错" + str(e))
return None, None

View File

@@ -326,17 +326,19 @@ class FanartModule(_ModuleBase):
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
if not mediainfo.tmdb_id and not mediainfo.tvdb_id:
return None
if mediainfo.type == MediaType.MOVIE:
result = self.__request_fanart(mediainfo.type, mediainfo.tmdb_id)
else:
if mediainfo.tvdb_id:
result = self.__request_fanart(mediainfo.type, mediainfo.tvdb_id)
else:
logger.info(f"{mediainfo.title_year} 没有tvdbid无法获取Fanart图片")
return
logger.info(f"{mediainfo.title_year} 没有tvdbid无法获取fanart图片")
return None
if not result or result.get('status') == 'error':
logger.warn(f"没有获取到 {mediainfo.title_year}Fanart图片数据")
return
logger.warn(f"没有获取到 {mediainfo.title_year}fanart图片数据")
return None
# 获取所有图片
for name, images in result.items():
if not images:

View File

@@ -8,7 +8,7 @@ from jinja2 import Template
from app.core.config import settings
from app.core.context import MediaInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.core.metainfo import MetaInfo, MetaInfoPath
from app.log import logger
from app.modules import _ModuleBase
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode
@@ -475,21 +475,32 @@ class FileTransferModule(_ModuleBase):
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
else:
return TransferInfo(success=False,
message=f"媒体库中已存在,且质量更好",
path=in_path,
target_path=new_file,
fail_list=[str(in_path)])
case 'never':
pass
# 存在不覆盖
return TransferInfo(success=False,
message=f"媒体库中已存在,当前设置为不覆盖",
path=in_path,
target_path=new_file,
fail_list=[str(in_path)])
case 'latest':
# 仅保留最新版本
self.delete_all_version_files(new_file)
overflag = True
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
# 转移文件
@@ -536,12 +547,14 @@ class FileTransferModule(_ModuleBase):
return {
# 标题
"title": mediainfo.title,
# 原文件名
"original_name": f"{meta.org_string}{file_ext}",
# 原语种标题
"original_title": mediainfo.original_title,
# 识别名称
# 原文件名
"original_name": f"{meta.org_string}{file_ext}",
# 识别名称(优先使用中文)
"name": meta.name,
# 识别的英文名称(可能为空)
"en_name": meta.en_name,
# 年份
"year": mediainfo.year or meta.year,
# 资源类型
@@ -562,6 +575,8 @@ class FileTransferModule(_ModuleBase):
"tmdbid": mediainfo.tmdb_id,
# IMDBID
"imdbid": mediainfo.imdb_id,
# 豆瓣ID
"doubanid": mediainfo.douban_id,
# 季号
"season": meta.season_seq,
# 集号
@@ -716,3 +731,34 @@ class FileTransferModule(_ModuleBase):
return ExistMediaInfo(type=MediaType.TV, seasons=seasons)
# 不存在
return None
@staticmethod
def delete_all_version_files(path: Path) -> bool:
"""
删除目录下的所有版本文件
:param path: 目录路径
"""
if not path.exists():
return False
# 识别文件中的季集信息
meta = MetaInfoPath(path)
season = meta.season
episode = meta.episode
# 检索媒体文件
logger.warn(f"正在删除目标目录中其它版本的文件:{path.parent}")
media_files = SystemUtils.list_files(directory=path.parent, extensions=settings.RMT_MEDIAEXT)
if not media_files:
logger.info(f"目录中没有媒体文件:{path.parent}")
return False
# 删除文件
for media_file in media_files:
if media_file == path:
continue
# 识别文件中的季集信息
filemeta = MetaInfoPath(media_file)
# 相同季集的文件才删除
if filemeta.season != season or filemeta.episode != episode:
continue
logger.info(f"正在删除文件:{media_file}")
media_file.unlink()
return True

View File

@@ -332,7 +332,7 @@ class Jellyfin(metaclass=Singleton):
if not season_episodes.get(season_index):
season_episodes[season_index] = []
season_episodes[season_index].append(episode_index)
return tv_info.get('Id'), season_episodes
return item_id, season_episodes
except Exception as e:
logger.error(f"连接Shows/Id/Episodes出错" + str(e))
return None, None

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from typing import Set, Tuple, Optional, Union, List
from qbittorrentapi import TorrentFilesList
from torrentool.torrent import Torrent
from app import schemas
from app.core.config import settings
@@ -47,6 +48,21 @@ class QbittorrentModule(_ModuleBase):
:param category: 分类
:return: 种子Hash错误信息
"""
def __get_torrent_info() -> Tuple[str, int]:
"""
获取种子名称
"""
try:
if isinstance(content, Path):
torrentinfo = Torrent.from_file(content)
else:
torrentinfo = Torrent.from_string(content)
return torrentinfo.name, torrentinfo.total_size
except Exception as e:
logger.error(f"获取种子名称失败:{e}")
return "", 0
if not content:
return
if isinstance(content, Path) and not content.exists():
@@ -70,6 +86,28 @@ class QbittorrentModule(_ModuleBase):
category=category
)
if not state:
# 读取种子的名称
torrent_name, torrent_size = __get_torrent_info()
if not torrent_name:
return None, f"添加种子任务失败:无法读取种子文件"
# 查询所有下载器的种子
torrents, error = self.qbittorrent.get_torrents()
if error:
return None, "无法连接qbittorrent下载器"
if torrents:
for torrent in torrents:
# 名称与大小相等则认为是同一个种子
if torrent.get("name") == torrent_name and torrent.get("total_size") == torrent_size:
torrent_hash = torrent.get("hash")
torrent_tags = [str(tag).strip() for tag in torrent.get("tags").split(',')]
logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.get('name')}")
# 给种子打上标签
if "已整理" in torrent_tags:
self.qbittorrent.remove_torrents_tag(ids=torrent_hash, tag=['已整理'])
if settings.TORRENT_TAG and settings.TORRENT_TAG not in torrent_tags:
logger.info(f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}")
self.qbittorrent.set_torrents_tag(ids=torrent_hash, tags=[settings.TORRENT_TAG])
return torrent_hash, f"下载任务已存在"
return None, f"添加种子任务失败:{content}"
else:
# 获取种子Hash

View File

@@ -16,14 +16,20 @@ class Qbittorrent(metaclass=Singleton):
_host: str = None
_port: int = None
_username: str = None
_passowrd: str = None
_password: str = None
qbc: Client = None
def __init__(self):
self._host, self._port = StringUtils.get_domain_address(address=settings.QB_HOST, prefix=True)
self._username = settings.QB_USER
self._password = settings.QB_PASSWORD
def __init__(self, host: str = None, port: int = None, username: str = None, password: str = None):
"""
若不设置参数,则创建配置文件设置的下载器
"""
if host and port:
self._host, self._port = host, port
else:
self._host, self._port = StringUtils.get_domain_address(address=settings.QB_HOST, prefix=True)
self._username = username if username else settings.QB_USER
self._password = password if password else settings.QB_PASSWORD
if self._host and self._port:
self.qbc = self.__login_qbittorrent()
@@ -352,26 +358,28 @@ class Qbittorrent(metaclass=Singleton):
logger.error(f"设置速度限制出错:{str(err)}")
return False
def recheck_torrents(self, ids: Union[str, list]):
def recheck_torrents(self, ids: Union[str, list]) -> bool:
"""
重新校验种子
"""
if not self.qbc:
return False
try:
return self.qbc.torrents_recheck(torrent_hashes=ids)
self.qbc.torrents_recheck(torrent_hashes=ids)
return True
except Exception as err:
logger.error(f"重新校验种子出错:{str(err)}")
return False
def add_trackers(self, ids: Union[str, list], trackers: list):
def update_tracker(self, hash_string: str, tracker_list: list) -> bool:
"""
添加tracker
"""
if not self.qbc:
return False
try:
return self.qbc.torrents_add_trackers(torrent_hashes=ids, urls=trackers)
self.qbc.torrents_add_trackers(torrent_hash=hash_string, urls=tracker_list)
return True
except Exception as err:
logger.error(f"添加tracker出错{str(err)}")
logger.error(f"修改tracker出错{str(err)}")
return False

View File

@@ -12,6 +12,7 @@ from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.common import retry
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
from app.utils.string import StringUtils
@@ -174,6 +175,7 @@ class Telegram(metaclass=Singleton):
logger.error(f"发送消息失败:{msg_e}")
return False
@retry(Exception, logger=logger)
def __send_request(self, userid: str = None, image="", caption="") -> bool:
"""
向Telegram发送报文
@@ -181,7 +183,9 @@ class Telegram(metaclass=Singleton):
if image:
req = RequestUtils(proxies=settings.PROXY).get_res(image)
if req and req.content:
if req is None:
raise Exception("获取图片失败")
if req.content:
image_file = Path(settings.TEMP_PATH) / Path(image).name
image_file.write_bytes(req.content)
photo = InputFile(image_file)
@@ -189,12 +193,15 @@ class Telegram(metaclass=Singleton):
photo=photo,
caption=caption,
parse_mode="Markdown")
if ret is None:
raise Exception("发送图片消息失败")
if ret:
return True
ret = self._bot.send_message(chat_id=userid or self._telegram_chat_id,
text=caption,
parse_mode="Markdown")
if ret is None:
raise Exception("发送文本消息失败")
return True if ret else False
def register_commands(self, commands: Dict[str, dict]):

View File

@@ -43,7 +43,8 @@ class TheMovieDbModule(_ModuleBase):
def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None,
tmdbid: int = None) -> Optional[MediaInfo]:
tmdbid: int = None,
**kwargs) -> Optional[MediaInfo]:
"""
识别媒体信息
:param meta: 识别的元数据
@@ -51,6 +52,9 @@ class TheMovieDbModule(_ModuleBase):
:param tmdbid: tmdbid
:return: 识别的媒体信息,包括剧集信息
"""
if settings.RECOGNIZE_SOURCE != "themoviedb":
return None
if not meta:
cache_info = {}
else:
@@ -112,11 +116,11 @@ class TheMovieDbModule(_ModuleBase):
else:
# 使用缓存信息
if cache_info.get("title"):
logger.info(f"{meta.name} 使用识别缓存:{cache_info.get('title')}")
logger.info(f"{meta.name} 使用TMDB识别缓存:{cache_info.get('title')}")
info = self.tmdb.get_info(mtype=cache_info.get("type"),
tmdbid=cache_info.get("id"))
else:
logger.info(f"{meta.name} 使用识别缓存:无法识别")
logger.info(f"{meta.name} 使用TMDB识别缓存:无法识别")
info = None
if info:
@@ -129,11 +133,11 @@ class TheMovieDbModule(_ModuleBase):
mediainfo = MediaInfo(tmdb_info=info)
mediainfo.set_category(cat)
if meta:
logger.info(f"{meta.name} 识别结果:{mediainfo.type.value} "
logger.info(f"{meta.name} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year} "
f"{mediainfo.tmdb_id}")
else:
logger.info(f"{tmdbid} 识别结果:{mediainfo.type.value} "
logger.info(f"{tmdbid} TMDB识别结果:{mediainfo.type.value} "
f"{mediainfo.title_year}")
# 补充剧集年份
@@ -143,10 +147,31 @@ class TheMovieDbModule(_ModuleBase):
mediainfo.season_years = episode_years
return mediainfo
else:
logger.info(f"{meta.name if meta else tmdbid} 未匹配到媒体信息")
logger.info(f"{meta.name if meta else tmdbid} 未匹配到TMDB媒体信息")
return None
def match_tmdbinfo(self, name: str, mtype: MediaType = None,
year: str = None, season: int = None) -> dict:
"""
搜索和匹配TMDB信息
:param name: 名称
:param mtype: 类型
:param year: 年份
:param season: 季号
"""
# 搜索
logger.info(f"开始使用 名称:{name}、年份:{year} 匹配TMDB信息 ...")
info = self.tmdb.match(name=name,
year=year,
mtype=mtype,
season_year=year,
season_number=season)
if info and not info.get("genres"):
info = self.tmdb.get_info(mtype=info.get("media_type"),
tmdbid=info.get("id"))
return info
def tmdb_info(self, tmdbid: int, mtype: MediaType) -> Optional[dict]:
"""
获取TMDB信息
@@ -163,7 +188,7 @@ class TheMovieDbModule(_ModuleBase):
:reutrn: 媒体信息列表
"""
# 未启用时返回None
if settings.SEARCH_SOURCE != "themoviedb":
if settings.RECOGNIZE_SOURCE != "themoviedb":
return None
if not meta.name:
@@ -287,6 +312,8 @@ class TheMovieDbModule(_ModuleBase):
:param mediainfo: 识别的媒体信息
:return: 更新后的媒体信息
"""
if settings.RECOGNIZE_SOURCE != "themoviedb":
return None
if not mediainfo.tmdb_id:
return mediainfo
if mediainfo.logo_path \
@@ -357,35 +384,35 @@ class TheMovieDbModule(_ModuleBase):
return f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/{image_prefix}{image_path}"
return None
def movie_similar(self, tmdbid: int) -> List[dict]:
def tmdb_movie_similar(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询类似电影
:param tmdbid: TMDBID
"""
return self.tmdb.get_movie_similar(tmdbid=tmdbid)
def tv_similar(self, tmdbid: int) -> List[dict]:
def tmdb_tv_similar(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询类似电视剧
:param tmdbid: TMDBID
"""
return self.tmdb.get_tv_similar(tmdbid=tmdbid)
def movie_recommend(self, tmdbid: int) -> List[dict]:
def tmdb_movie_recommend(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询推荐电影
:param tmdbid: TMDBID
"""
return self.tmdb.get_movie_recommend(tmdbid=tmdbid)
def tv_recommend(self, tmdbid: int) -> List[dict]:
def tmdb_tv_recommend(self, tmdbid: int) -> List[dict]:
"""
根据TMDBID查询推荐电视剧
:param tmdbid: TMDBID
"""
return self.tmdb.get_tv_recommend(tmdbid=tmdbid)
def movie_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
def tmdb_movie_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
"""
根据TMDBID查询电影演职员表
:param tmdbid: TMDBID
@@ -393,7 +420,7 @@ class TheMovieDbModule(_ModuleBase):
"""
return self.tmdb.get_movie_credits(tmdbid=tmdbid, page=page)
def tv_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
def tmdb_tv_credits(self, tmdbid: int, page: int = 1) -> List[dict]:
"""
根据TMDBID查询电视剧演职员表
:param tmdbid: TMDBID
@@ -401,14 +428,14 @@ class TheMovieDbModule(_ModuleBase):
"""
return self.tmdb.get_tv_credits(tmdbid=tmdbid, page=page)
def person_detail(self, person_id: int) -> dict:
def tmdb_person_detail(self, person_id: int) -> dict:
"""
根据TMDBID查询人物详情
:param person_id: 人物ID
"""
return self.tmdb.get_person_detail(person_id=person_id)
def person_credits(self, person_id: int, page: int = 1) -> List[dict]:
def tmdb_person_credits(self, person_id: int, page: int = 1) -> List[dict]:
"""
根据TMDBID查询人物参演作品
:param person_id: 人物ID
@@ -420,5 +447,7 @@ class TheMovieDbModule(_ModuleBase):
"""
清除缓存
"""
logger.info("开始清除TMDB缓存 ...")
self.tmdb.clear_cache()
self.cache.clear()
logger.info("TMDB缓存清除完成")

View File

@@ -4,11 +4,11 @@ import logging
import os
import time
from datetime import datetime
from functools import lru_cache
import requests
import requests.exceptions
from app.utils.common import lru_cache_without_none
from app.utils.http import RequestUtils
from .exceptions import TMDbException
@@ -137,7 +137,7 @@ class TMDb(object):
def cache(self, cache):
os.environ[self.TMDB_CACHE_ENABLED] = str(cache)
@lru_cache_without_none(maxsize=REQUEST_CACHE_MAXSIZE)
@lru_cache(maxsize=REQUEST_CACHE_MAXSIZE)
def cached_request(self, method, url, data, json,
_ts=datetime.strftime(datetime.now(), '%Y%m%d')):
"""
@@ -147,9 +147,12 @@ class TMDb(object):
def request(self, method, url, data, json):
if method == "GET":
return self._req.get_res(url, params=data, json=json)
req = self._req.get_res(url, params=data, json=json)
else:
return self._req.post_res(url, data=data, json=json)
req = self._req.post_res(url, data=data, json=json)
if req is None:
raise TMDbException("无法连接TheMovieDb请检查网络连接")
return req
def cache_clear(self):
return self.cached_request.cache_clear()
@@ -157,7 +160,7 @@ class TMDb(object):
def _request_obj(self, action, params="", call_cached=True,
method="GET", data=None, json=None, key=None):
if self.api_key is None or self.api_key == "":
raise TMDbException("No API key found.")
raise TMDbException("TheMovieDb API Key 未设置!")
url = "https://%s/3%s?api_key=%s&%s&language=%s" % (
self.domain,
@@ -173,7 +176,7 @@ class TMDb(object):
req = self.request(method, url, data, json)
if req is None:
raise TMDbException("Failed to establish a new connection: no response from the server.")
return None
headers = req.headers
@@ -188,11 +191,11 @@ class TMDb(object):
sleep_time = self._reset - current_time
if self.wait_on_rate_limit:
logger.warning("Rate limit reached. Sleeping for: %d" % sleep_time)
logger.warning("达到请求频率限制,休眠:%d 秒..." % sleep_time)
time.sleep(abs(sleep_time))
return self._request_obj(action, params, call_cached, method, data, json, key)
else:
raise TMDbException("Rate limit reached. Try again in %d seconds." % sleep_time)
raise TMDbException("达到请求频率限制,将在 %d 秒后重试..." % sleep_time)
json = req.json()

View File

@@ -2,6 +2,7 @@ import shutil
from pathlib import Path
from typing import Set, Tuple, Optional, Union, List
from torrentool.torrent import Torrent
from transmission_rpc import File
from app import schemas
@@ -47,6 +48,21 @@ class TransmissionModule(_ModuleBase):
:param category: 分类TR中未使用
:return: 种子Hash
"""
def __get_torrent_info() -> Tuple[str, int]:
"""
获取种子名称
"""
try:
if isinstance(content, Path):
torrentinfo = Torrent.from_file(content)
else:
torrentinfo = Torrent.from_string(content)
return torrentinfo.name, torrentinfo.total_size
except Exception as e:
logger.error(f"获取种子名称失败:{e}")
return "", 0
if not content:
return
if isinstance(content, Path) and not content.exists():
@@ -68,6 +84,33 @@ class TransmissionModule(_ModuleBase):
cookie=cookie
)
if not torrent:
# 读取种子的名称
torrent_name, torrent_size = __get_torrent_info()
if not torrent_name:
return None, f"添加种子任务失败:无法读取种子文件"
# 查询所有下载器的种子
torrents, error = self.transmission.get_torrents()
if error:
return None, "无法连接transmission下载器"
if torrents:
for torrent in torrents:
# 名称与大小相等则认为是同一个种子
if torrent.name == torrent_name and torrent.total_size == torrent_size:
torrent_hash = torrent.hashString
logger.warn(f"下载器中已存在该种子任务:{torrent_hash} - {torrent.name}")
# 给种子打上标签
if settings.TORRENT_TAG:
logger.info(f"给种子 {torrent_hash} 打上标签:{settings.TORRENT_TAG}")
# 种子标签
labels = [str(tag).strip()
for tag in torrent.labels] if hasattr(torrent, "labels") else []
if "已整理" in labels:
labels.remove("已整理")
self.transmission.set_torrent_tag(ids=torrent_hash, tags=labels)
if settings.TORRENT_TAG and settings.TORRENT_TAG not in labels:
labels.append(settings.TORRENT_TAG)
self.transmission.set_torrent_tag(ids=torrent_hash, tags=labels)
return torrent_hash, f"下载任务已存在"
return None, f"添加种子任务失败:{content}"
else:
torrent_hash = torrent.hashString

View File

@@ -14,7 +14,7 @@ class Transmission(metaclass=Singleton):
_host: str = None
_port: int = None
_username: str = None
_passowrd: str = None
_password: str = None
trc: Optional[Client] = None
@@ -24,10 +24,16 @@ class Transmission(metaclass=Singleton):
"peersGettingFromUs", "peersSendingToUs", "uploadRatio", "uploadedEver", "downloadedEver", "downloadDir",
"error", "errorString", "doneDate", "queuePosition", "activityDate", "trackers"]
def __init__(self):
self._host, self._port = StringUtils.get_domain_address(address=settings.TR_HOST, prefix=False)
self._username = settings.TR_USER
self._password = settings.TR_PASSWORD
def __init__(self, host: str = None, port: int = None, username: str = None, password: str = None):
"""
若不设置参数,则创建配置文件设置的下载器
"""
if host and port:
self._host, self._port = host, port
else:
self._host, self._port = StringUtils.get_domain_address(address=settings.TR_HOST, prefix=False)
self._username = username if username else settings.TR_USER
self._password = password if password else settings.TR_PASSWORD
if self._host and self._port:
self.trc = self.__login_transmission()
@@ -271,36 +277,25 @@ class Transmission(metaclass=Singleton):
logger.error(f"设置速度限制出错:{str(err)}")
return False
def recheck_torrents(self, ids: Union[str, list]):
def recheck_torrents(self, ids: Union[str, list]) -> bool:
"""
重新校验种子
"""
if not self.trc:
return False
try:
return self.trc.verify_torrent(ids=ids)
self.trc.verify_torrent(ids=ids)
return True
except Exception as err:
logger.error(f"重新校验种子出错:{str(err)}")
return False
def add_trackers(self, ids: Union[str, list], trackers: list):
"""
添加Tracker
"""
if not self.trc:
return False
try:
return self.trc.change_torrent(ids=ids, tracker_list=[trackers])
except Exception as err:
logger.error(f"添加Tracker出错{str(err)}")
return False
def change_torrent(self,
hash_string: str,
upload_limit=None,
download_limit=None,
ratio_limit=None,
seeding_time_limit=None):
seeding_time_limit=None) -> bool:
"""
设置种子
:param hash_string: ID
@@ -346,6 +341,22 @@ class Transmission(metaclass=Singleton):
seedRatioLimit=seedRatioLimit,
seedIdleMode=seedIdleMode,
seedIdleLimit=seedIdleLimit)
return True
except Exception as err:
logger.error(f"设置种子出错:{str(err)}")
return False
def update_tracker(self, hash_string: str, tracker_list: list = None) -> bool:
"""
tr4.0及以上弃用直接设置tracker 共用change方法
https://github.com/trim21/transmission-rpc/blob/8eb82629492a0eeb0bb565f82c872bf9ccdcb313/transmission_rpc/client.py#L654
"""
if not self.trc:
return False
try:
self.trc.change_torrent(ids=hash_string,
tracker_list=tracker_list)
return True
except Exception as err:
logger.error(f"修改tracker出错{str(err)}")
return False

View File

@@ -86,6 +86,7 @@ class _PluginBase(metaclass=ABCMeta):
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
"""
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
插件配置页面使用Vuetify组件拼装参考https://vuetifyjs.com/
"""
pass
@@ -93,6 +94,7 @@ class _PluginBase(metaclass=ABCMeta):
def get_page(self) -> List[dict]:
"""
拼装插件详情页面,需要返回页面配置,同时附带数据
插件详情页面使用Vuetify组件拼装参考https://vuetifyjs.com/
"""
pass

View File

@@ -13,6 +13,7 @@ from app.chain.cookiecloud import CookieCloudChain
from app.chain.mediaserver import MediaServerChain
from app.chain.subscribe import SubscribeChain
from app.chain.tmdb import TmdbChain
from app.chain.torrents import TorrentsChain
from app.chain.transfer import TransferChain
from app.core.config import settings
from app.log import logger
@@ -43,6 +44,14 @@ class Scheduler(metaclass=Singleton):
_event = threading.Event()
def __init__(self):
def clear_cache():
"""
清理缓存
"""
TorrentsChain().clear_cache()
SchedulerChain().clear_cache()
# 各服务的运行状态
self._jobs = {
"cookiecloud": {
@@ -73,7 +82,7 @@ class Scheduler(metaclass=Singleton):
"running": False,
},
"clear_cache": {
"func": SchedulerChain().clear_cache,
"func": clear_cache,
"running": False,
}
}

View File

@@ -14,8 +14,6 @@ class Plugin(BaseModel):
plugin_desc: Optional[str] = None
# 插件图标
plugin_icon: Optional[str] = None
# 主题色
plugin_color: Optional[str] = None
# 插件版本
plugin_version: Optional[str] = None
# 插件作者

View File

@@ -49,3 +49,14 @@ class TmdbPerson(BaseModel):
popularity: Optional[float] = None
images: Optional[dict] = {}
biography: Optional[str] = None
class DoubanPerson(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
roles: Optional[list] = []
title: Optional[str] = None
url: Optional[str] = None
character: Optional[str] = None
avatar: Optional[dict] = None
latin_name: Optional[str] = None

View File

@@ -16,16 +16,12 @@ class TorrentStatus(Enum):
class EventType(Enum):
# 插件重载
PluginReload = "plugin.reload"
# 插件动作
PluginAction = "plugin.action"
# 执行命令
CommandExcute = "command.excute"
# 站点签到
SiteSignin = "site.signin"
# 站点数据统计
SiteStatistic = "site.statistic"
# 站点删除
SiteDeleted = "site.deleted"
# 豆瓣想看
DoubanSync = "douban.sync"
# Webhook消息
WebhookMessage = "webhook.message"
# 转移完成
@@ -44,14 +40,6 @@ class EventType(Enum):
NameRecognize = "name.recognize"
# 名称识别结果
NameRecognizeResult = "name.recognize.result"
# 目录监控同步
DirectorySync = "directory.sync"
# Cloudflare IP优选
CloudFlareSpeedTest = "cloudflare.speedtest"
# 站点自动登录更新Cookie、Ua
SiteLogin = "site.login"
# 网盘删除
NetworkDiskDel = "networkdisk.del"
# 系统配置Key字典

View File

@@ -1,6 +1,5 @@
import time
from typing import Any
from functools import lru_cache
def retry(ExceptionToCheck: Any,
@@ -33,26 +32,3 @@ def retry(ExceptionToCheck: Any,
return f_retry
return deco_retry
def lru_cache_without_none(maxsize=None, typed=False):
"""
不缓存None的lru_cache
:param maxsize: 缓存大小
:param typed: 是否区分参数类型
"""
def decorator(func):
cache = lru_cache(maxsize=maxsize, typed=typed)(func)
def wrapper(*args, **kwargs):
result = cache(*args, **kwargs)
if result is not None:
return result
def cache_clear():
cache.cache_clear()
wrapper.cache_clear = cache_clear
return wrapper
return decorator

View File

@@ -9,9 +9,10 @@ class Singleton(abc.ABCMeta, type):
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
key = (cls, args, frozenset(kwargs.items()))
if key not in cls._instances:
cls._instances[key] = super().__call__(*args, **kwargs)
return cls._instances[key]
class AbstractSingleton(abc.ABC, metaclass=Singleton):

View File

@@ -2,62 +2,165 @@
# 【*】为必配项,其余为选配项,选配项可以删除整项配置项或者保留配置默认值 #
#######################################################################
####################################
# 系统设置 #
####################################
# 【*】API监听地址注意不是前端访问地址
HOST=0.0.0.0
# 是否调试模式,打开后将输出更多日志
DEBUG=false
# 是否开发模式,打开后后台服务将不会启动
DEV=false
# 【*】超级管理员,设置后一但重启将固化到数据库中,修改将无效
SUPERUSER=admin
# 【*】超级管理员初始密码,设置后一但重启将固化到数据库中,修改将无效
SUPERUSER_PASSWORD=password
# 大内存模式,开启后会增加缓存数量,但会占用更多内存
BIG_MEMORY_MODE=false
# 自动检查和更新站点资源包(索引、认证等)
AUTO_UPDATE_RESOURCE=true
####################################
# 消息通知渠道(按需配置) #
####################################
# WeChat企业ID
WECHAT_CORPID=
# WeChat应用Secret
WECHAT_APP_SECRET=
# WeChat应用ID
WECHAT_APP_ID=
# WeChat代理服务器无需代理需保留默认值
WECHAT_PROXY=https://qyapi.weixin.qq.com
# WeChat Token
WECHAT_TOKEN=
# WeChat EncodingAESKey
WECHAT_ENCODING_AESKEY=
# WeChat 管理员
WECHAT_ADMINS=
# Telegram Bot Token
TELEGRAM_TOKEN=
# Telegram Chat ID
TELEGRAM_CHAT_ID=
# Telegram 用户ID使用,分隔
TELEGRAM_USERS=
# Telegram 管理员ID使用,分隔
TELEGRAM_ADMINS=
# Slack Bot User OAuth Token
SLACK_OAUTH_TOKEN=
# Slack App-Level Token
SLACK_APP_TOKEN=
# Slack 频道名称
SLACK_CHANNEL=
# SynologyChat Webhook
SYNOLOGYCHAT_WEBHOOK=
# SynologyChat Token
SYNOLOGYCHAT_TOKEN=
####################################
# 下载器(按需配置) #
####################################
# Qbittorrent地址IP:PORT
QB_HOST=
# Qbittorrent用户名
QB_USER=
# Qbittorrent密码
QB_PASSWORD=
# Qbittorrent分类自动管理
QB_CATEGORY=false
# Qbittorrent按顺序下载
QB_SEQUENTIAL=true
# Qbittorrent忽略队列限制强制继续
QB_FORCE_RESUME=true
# Transmission地址IP:PORT
TR_HOST=
# Transmission用户名
TR_USER=
# Transmission密码
TR_PASSWORD=
####################################
# 媒体服务器(按需配置) #
####################################
# EMBY服务器地址IP:PORT
EMBY_HOST=
# EMBY Api Key
EMBY_API_KEY=
# Jellyfin服务器地址IP:PORT
JELLYFIN_HOST=
# Jellyfin Api Key
JELLYFIN_API_KEY=
# Plex服务器地址IP:PORT
PLEX_HOST=
# Plex Token
PLEX_TOKEN=
####################################
# 基础设置 #
####################################
# 【*】API监听地址
HOST=0.0.0.0
# 是否调试模式
DEBUG=false
# 是否开发模式
DEV=false
# 【*】超级管理员
SUPERUSER=admin
# 【*】超级管理员初始密码
SUPERUSER_PASSWORD=password
# 【*】API密钥建议更换复杂字符串
# 【*】API密钥建议更换复杂字符串有Jellyseerr/Overseerr、媒体服务器Webhook等配置以及部分支持API_TOKEN的API中使用
API_TOKEN=moviepilot
# 登录页面电影海报,tmdb/bing
# 登录页面电影海报tmdb/bingtmdb要求能正常连接api.themoviedb.org
WALLPAPER=tmdb
# TMDB图片地址无需修改需保留默认值
# TMDB图片地址无需修改需保留默认值,如果默认地址连通性不好可以尝试修改为:`static-mdb.v.geilijiasu.com`
TMDB_IMAGE_DOMAIN=image.tmdb.org
# TMDB API地址无需修改需保留默认值
# TMDB API地址无需修改需保留默认值,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
TMDB_API_DOMAIN=api.themoviedb.org
# 大内存模式
BIG_MEMORY_MODE=false
# 媒体识别来源 themoviedb/douban使用themoviedb时需要确保能正常连接api.themoviedb.org使用douban时不支持二级分类
RECOGNIZE_SOURCE=themoviedb
# 【*】消息通知渠道 telegram/wechat/slack多个通知渠道用,分隔,需要在上面配置对应消息通知渠道的参数
MESSAGER=telegram
# 【*】下载器 qbittorrent/transmission仅支持单个下载器做为主下载器使用需要在上面配置对应消下载器的参数
DOWNLOADER=qbittorrent
# 下载器监控开关
DOWNLOADER_MONITOR=true
# 【*】媒体服务器 emby/jellyfin/plex多个媒体服务器,分割
MEDIASERVER=emby
# 媒体服务器同步间隔(小时)
MEDIASERVER_SYNC_INTERVAL=6
# 媒体服务器同步黑名单,多个媒体库名称,分割
MEDIASERVER_SYNC_BLACKLIST=
####################################
# 媒体识别&刮削 #
####################################
# 媒体信息搜索来源 themoviedb/douban
SEARCH_SOURCE=themoviedb
# 刮削入库的媒体文件 true/false
SCRAP_METADATA=true
# 新增已入库媒体是否跟随TMDB信息变化
# 新增已入库媒体是否跟随TMDB信息变化true/false为false时即使TMDB信息变化时也会仍然按历史记录中已入库的信息进行刮削
SCRAP_FOLLOW_TMDB=true
# 刮削来源 themoviedb/douban
# 刮削来源 themoviedb/douban使用themoviedb时需要确保能正常连接api.themoviedb.org使用douban时会缺失部分信息
SCRAP_SOURCE=themoviedb
####################################
# 媒体库 #
# 文件整理 & 媒体库 #
####################################
# 【*】转移方式 link/copy/move/softlink/rclone_copy/rclone_move
TRANSFER_TYPE=copy
# 转移覆盖模式
# 转移覆盖模式`nerver`/`size`/`always`/`latest`,分别表示`不覆盖同名文件`/`同名文件根据文件大小覆盖(大覆盖小)`/`总是覆盖同名文件`/`仅保留最新版本,删除旧版本文件(包括非同名文件)`
OVERWRITE_MODE=size
# 【*】媒体库目录,多个目录使用,分隔
LIBRARY_PATH=
LIBRARY_PATH=/media
# 电影媒体库目录名,默认电影
LIBRARY_MOVIE_NAME=
LIBRARY_MOVIE_NAME=电影
# 电视剧媒体库目录名,默认电视剧
LIBRARY_TV_NAME=
LIBRARY_TV_NAME=电视剧
# 动漫媒体库目录名,默认电视剧/动漫
LIBRARY_ANIME_NAME=
# 二级分类
LIBRARY_ANIME_NAME=电视剧/动漫
# 二级分类,开启后会根据配置 [category.yaml](https://github.com/jxxghp/MoviePilot/raw/main/config/category.yaml) 自动在媒体库目录下建立二级目录分类
LIBRARY_CATEGORY=true
# 电影重命名格式
# 电影重命名格式Jinja2语法参考https://jinja.palletsprojects.com/en/3.0.x/templates/
MOVIE_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}{{fileExt}}
# 电视剧重命名格式
# 电视剧重命名格式Jinja2语法参考https://jinja.palletsprojects.com/en/3.0.x/templates/
TV_RENAME_FORMAT={{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}}{% endif %}{{fileExt}}
####################################
@@ -77,120 +180,39 @@ USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
####################################
# 订阅 & 搜索 #
####################################
# 订阅模式 spider/rss
# 订阅模式 spider/rss`rss`模式通过定时刷新RSS来匹配订阅RSS地址会自动获取也可手动维护`spider`为爬虫模式随机时间间隔20-40分钟爬取站点首页种子
SUBSCRIBE_MODE=spider
# RSS订阅模式刷新时间间隔分钟
SUBSCRIBE_RSS_INTERVAL=30
# 订阅搜索开关
# 订阅搜索开关开启后会每隔24小时对所有订阅进行全量搜索以补齐缺失剧集
SUBSCRIBE_SEARCH=false
# 交互搜索自动下载用户ID,使用,分割
# 交互搜索自动下载用户ID消息通知渠道的用户ID使用,分割,未设置需要用户手动选择资源或者回复`0`
AUTO_DOWNLOAD_USER=
####################################
# 消息通知 #
####################################
# 【*】消息通知渠道 telegram/wechat/slack多个通知渠道用,分隔
MESSAGER=telegram
# WeChat企业ID
WECHAT_CORPID=
# WeChat应用Secret
WECHAT_APP_SECRET=
# WeChat应用ID
WECHAT_APP_ID=
# WeChat代理服务器无需代理需保留默认值
WECHAT_PROXY=https://qyapi.weixin.qq.com
# WeChat Token
WECHAT_TOKEN=
# WeChat EncodingAESKey
WECHAT_ENCODING_AESKEY=
# WeChat 管理员
WECHAT_ADMINS=
# Telegram Bot Token
TELEGRAM_TOKEN=
# Telegram Chat ID
TELEGRAM_CHAT_ID=
# Telegram 用户ID使用,分隔
TELEGRAM_USERS=
# Telegram 管理员ID使用,分隔
TELEGRAM_ADMINS=
# Slack Bot User OAuth Token
SLACK_OAUTH_TOKEN=
# Slack App-Level Token
SLACK_APP_TOKEN=
# Slack 频道名称
SLACK_CHANNEL=
# SynologyChat Webhook
SYNOLOGYCHAT_WEBHOOK=
# SynologyChat Token
SYNOLOGYCHAT_TOKEN=
####################################
# 下载 #
####################################
# 【*】下载器 qbittorrent/transmission
DOWNLOADER=qbittorrent
# 下载器监控开关
DOWNLOADER_MONITOR=true
# Qbittorrent地址IP:PORT
QB_HOST=
# Qbittorrent用户名
QB_USER=
# Qbittorrent密码
QB_PASSWORD=
# Qbittorrent分类自动管理
QB_CATEGORY=false
# Qbittorrent按顺序下载
QB_SEQUENTIAL=true
# Qbittorrent忽略队列限制强制继续
QB_FORCE_RESUME=false
# Transmission地址IP:PORT
TR_HOST=
# Transmission用户名
TR_USER=
# Transmission密码
TR_PASSWORD=
# 【*】下载保存目录,容器内映射路径需要一致,支持不同类型设置不同的下载目录(跨盘)
DOWNLOAD_PATH=/downloads
# 电影下载保存目录(路径),容器内映射路径需要一致
DOWNLOAD_MOVIE_PATH=
# 电视剧下载保存目录(路径),容器内映射路径需要一致
DOWNLOAD_TV_PATH=
# 动漫下载保存目录(路径),容器内映射路径需要一致
DOWNLOAD_ANIME_PATH=
# 下载目录二级分类,开启后会根据配置 [category.yaml](https://github.com/jxxghp/MoviePilot/raw/main/config/category.yaml) 自动在下载目录下建立二级目录分类
DOWNLOAD_CATEGORY=false
# 种子标签
TORRENT_TAG=MOVIEPILOT
# 【*】下载保存目录,容器内映射路径需要一致
DOWNLOAD_PATH=/downloads
# 电影下载保存目录,容器内映射路径需要一致
DOWNLOAD_MOVIE_PATH=
# 电视剧下载保存目录,容器内映射路径需要一致
DOWNLOAD_TV_PATH=
# 动漫下载保存目录,容器内映射路径需要一致
DOWNLOAD_ANIME_PATH=
# 下载目录二级分类
DOWNLOAD_CATEGORY=false
# 下载站点字幕
# 自动下载站点字幕(如有)
DOWNLOAD_SUBTITLE=true
####################################
# 媒体服务器 #
####################################
# 【*】媒体服务器 emby/jellyfin/plex多个媒体服务器,分割
MEDIASERVER=emby
# 媒体服务器同步间隔(小时)
MEDIASERVER_SYNC_INTERVAL=6
# 媒体服务器同步黑名单,多个媒体库名称,分割
MEDIASERVER_SYNC_BLACKLIST=
# EMBY服务器地址IP:PORT
EMBY_HOST=
# EMBY Api Key
EMBY_API_KEY=
# Jellyfin服务器地址IP:PORT
JELLYFIN_HOST=
# Jellyfin Api Key
JELLYFIN_API_KEY=
# Plex服务器地址IP:PORT
PLEX_HOST=
# Plex Token
PLEX_TOKEN=
####################################
# 第三方服务 #
# 扩展 #
####################################
# OCR服务器地址
OCR_HOST=https://movie-pilot.org
# 插件市场仓库地址,多个地址使用`,`分隔,保留最后的/
PLUGIN_MARKET=https://raw.githubusercontent.com/jxxghp/MoviePilot-Plugins/main/
PLUGIN_MARKET=https://github.com/jxxghp/MoviePilot-Plugins

View File

@@ -5,7 +5,7 @@ from sqlalchemy import pool
from alembic import context
from app.db.models import Base
from app.db import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

View File

@@ -3,13 +3,9 @@
# 使用 `envsubst` 将模板文件中的 ${NGINX_PORT} 替换为实际的环境变量值
envsubst '${NGINX_PORT}${PORT}' < /etc/nginx/nginx.template.conf > /etc/nginx/nginx.conf
# 自动更新
if [ "${MOVIEPILOT_AUTO_UPDATE}" = "true" ]; then
cd /
/usr/local/bin/mp_update
cd /app
else
echo "程序自动升级已关闭如需自动升级请在创建容器时设置环境变量MOVIEPILOT_AUTO_UPDATE=true"
fi
cd /
/usr/local/bin/mp_update
cd /app
# 更改 moviepilot userid 和 groupid
groupmod -o -g ${PGID} moviepilot
usermod -o -u ${PUID} moviepilot

View File

@@ -8,17 +8,22 @@ def collect_pkg_data(package: str, include_py_files: bool = False, subdir: str =
from PyInstaller.utils.hooks import get_package_paths, PY_IGNORE_EXTENSIONS
from PyInstaller.building.datastruct import TOC
data_toc = TOC()
# Accept only strings as packages.
if type(package) is not str:
raise ValueError
pkg_base, pkg_dir = get_package_paths(package)
try:
pkg_base, pkg_dir = get_package_paths(package)
except ValueError:
return data_toc
if subdir:
pkg_path = Path(pkg_dir) / subdir
else:
pkg_path = Path(pkg_dir)
# Walk through all file in the given package, looking for data files.
data_toc = TOC()
if not pkg_path.exists():
return data_toc
for file in pkg_path.rglob('*'):
if file.is_file():
extension = file.suffix
@@ -37,6 +42,8 @@ def collect_local_submodules(package: str):
package_dir = Path(package.replace('.', os.sep))
submodules = [package]
# Walk through all file in the given package, looking for data files.
if not package_dir.exists():
return []
for file in package_dir.rglob('*.py'):
if file.name == '__init__.py':
module = f"{file.parent}".replace(os.sep, '.')
@@ -82,6 +89,7 @@ exe = EXE(
collect_pkg_data('zhconv'),
collect_pkg_data('cn2an'),
collect_pkg_data('database', include_py_files=True),
collect_pkg_data('app.helper'),
[],
name='MoviePilot',
debug=False,

View File

@@ -54,4 +54,5 @@ parse~=1.19.0
docker~=6.1.3
cachetools~=5.3.1
fast-bencode~=1.1.3
pystray~=0.19.5
pystray~=0.19.5
pypushdeer~=0.0.3

View File

@@ -15,7 +15,7 @@ class RecognizeTest(TestCase):
pass
def test_recognize(self):
result = MediaChain().recognize_by_title(title="我和我的祖国 2019")
self.assertEqual(result.media_info.tmdb_id, 612845)
exists = DownloadChain().get_no_exists_info(MetaInfo("我和我的祖国 2019"), result.media_info)
media_info = MediaChain().recognize_by_meta(MetaInfo("我和我的祖国 2019"))
self.assertEqual(media_info.tmdb_id, 612845)
exists = DownloadChain().get_no_exists_info(MetaInfo("我和我的祖国 2019"), media_info)
self.assertTrue(exists[0])

75
update
View File

@@ -5,7 +5,7 @@ download_and_unzip() {
url="$1"
target_dir="$2"
echo "正在下载 ${url}..."
curl ${CURL_OPTIONS} "$url" | busybox unzip -d /tmp -
curl ${CURL_OPTIONS} "$url" ${CURL_HEADERS} | busybox unzip -d /tmp -
if [ $? -eq 0 ]; then
if [ -e /tmp/MoviePilot-* ]; then
mv /tmp/MoviePilot-* /tmp/${target_dir}
@@ -30,7 +30,7 @@ install_backend_and_download_resources() {
download_and_unzip "https://github.com/jxxghp/MoviePilot-Resources/archive/refs/heads/main.zip" "Resources"
if [ $? -eq 0 ]; then
echo "资源包下载成功"
frontend_version=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest" | jq -r .tag_name)
frontend_version=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest" ${CURL_HEADERS} | jq -r .tag_name)
if [[ "${frontend_version}" == *v* ]]; then
download_and_unzip "https://github.com/jxxghp/MoviePilot-Frontend/releases/download/${frontend_version}/dist.zip" "dist"
if [ $? -eq 0 ]; then
@@ -41,7 +41,7 @@ install_backend_and_download_resources() {
# 清空目录
rm -rf /app
# 后端程序
mv -f /tmp/App /app
mv /tmp/App /app
# 恢复插件目录
mv -f /plugins/* /app/app/plugins/
# 插件仓库
@@ -74,39 +74,48 @@ install_backend_and_download_resources() {
fi
}
# ...
if [ -n "${PROXY_HOST}" ]; then
CURL_OPTIONS="-sL -x ${PROXY_HOST}"
PIP_OPTIONS="--proxy=${PROXY_HOST}"
echo "使用代理更新程序"
else
CURL_OPTIONS="-sL"
echo "不使用代理更新程序"
fi
if [ "${MOVIEPILOT_AUTO_UPDATE_DEV}" = "true" ]; then
echo "Dev 更新模式"
install_backend_and_download_resources "heads/main.zip"
else
old_version=$(cat /app/version.py)
if [[ "${old_version}" == *APP_VERSION* ]]; then
current_version=v$(echo ${old_version} | sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "当前版本号:${current_version}"
new_version=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot/releases/latest" | jq -r .tag_name)
if [[ "${new_version}" == *v* ]]; then
release_version=${new_version}
echo "最新版本号:${release_version}"
if [ "${current_version}" != "${release_version}" ]; then
echo "发现新版本,开始自动升级..."
install_backend_and_download_resources "tags/${release_version}.zip"
if [[ "${MOVIEPILOT_AUTO_UPDATE}" = "true" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}" = "release" ]] || [[ "${MOVIEPILOT_AUTO_UPDATE}" = "dev" ]]; then
if [ -n "${PROXY_HOST}" ]; then
CURL_OPTIONS="-sL -x ${PROXY_HOST}"
PIP_OPTIONS="--proxy=${PROXY_HOST}"
echo "使用代理更新程序"
else
CURL_OPTIONS="-sL"
echo "不使用代理更新程序"
fi
if [ -n "${GITHUB_TOKEN}" ]; then
CURL_HEADERS="--header 'Authorization: Bearer ${GITHUB_TOKEN}'"
else
CURL_HEADERS=""
fi
if [ "${MOVIEPILOT_AUTO_UPDATE}" = "dev" ]; then
echo "Dev 更新模式"
install_backend_and_download_resources "heads/main.zip"
else
echo "Release 更新模式"
old_version=$(cat /app/version.py)
if [[ "${old_version}" == *APP_VERSION* ]]; then
current_version=v$(echo ${old_version} | sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
echo "当前版本号:${current_version}"
new_version=$(curl ${CURL_OPTIONS} "https://api.github.com/repos/jxxghp/MoviePilot/releases/latest" ${CURL_HEADERS} | jq -r .tag_name)
if [[ "${new_version}" == *v* ]]; then
release_version=${new_version}
echo "最新版本号:${release_version}"
if [ "${current_version}" != "${release_version}" ]; then
echo "发现新版本,开始自动升级..."
install_backend_and_download_resources "tags/${release_version}.zip"
else
echo "未发现新版本,跳过更新步骤..."
fi
else
echo "未发现新版本,跳过更新步骤..."
echo "最新版本号获取失败,继续启动..."
fi
else
echo "最新版本号获取失败,继续启动..."
echo "当前版本号获取失败,继续启动..."
fi
else
echo "当前版本号获取失败,继续启动..."
fi
elif [[ "${MOVIEPILOT_AUTO_UPDATE}" = "false" ]]; then
echo "程序自动升级已关闭如需自动升级请在创建容器时设置环境变量MOVIEPILOT_AUTO_UPDATE=release"
else
echo "MOVIEPILOT_AUTO_UPDATE 变量设置错误"
fi

View File

@@ -1 +1 @@
APP_VERSION = 'v1.3.9-2'
APP_VERSION = 'v1.4.8-2'