mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-08 09:13:15 +08:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c37e02009f | ||
|
|
a96b8a4e07 | ||
|
|
79b4d5fb8e | ||
|
|
de128f5e6a | ||
|
|
ef8ddcde07 | ||
|
|
eaff557d70 | ||
|
|
38f7a31200 | ||
|
|
97f16289c9 | ||
|
|
e15f5ab93e | ||
|
|
15fd312765 | ||
|
|
eea316865f | ||
|
|
05bbfbbd54 | ||
|
|
6039a9d0d5 | ||
|
|
0159b02916 | ||
|
|
8bbd4dc913 | ||
|
|
9e3ded6ad5 | ||
|
|
fe63275a6b | ||
|
|
81ed465607 | ||
|
|
d9aa281ce1 | ||
|
|
56648d664e | ||
|
|
da49d5577a | ||
|
|
f3dbdefdb1 | ||
|
|
d4302759e6 | ||
|
|
914f192fb2 | ||
|
|
522b554e36 | ||
|
|
4c54ab5319 | ||
|
|
d7f4ed069c | ||
|
|
7ea0c5ee4c | ||
|
|
e773a9d9d4 | ||
|
|
b570542fab | ||
|
|
09716e98ba | ||
|
|
9236b361e2 | ||
|
|
f281d8c068 | ||
|
|
83ed17d5c1 | ||
|
|
e2671dd4ed | ||
|
|
4c4d640331 | ||
|
|
6c4307c918 | ||
|
|
5a7062c699 | ||
|
|
7da01f7404 | ||
|
|
2b695cb8c6 | ||
|
|
599817eec7 | ||
|
|
11fa33be0a | ||
|
|
b5ac9d4ce4 | ||
|
|
78f0ac0042 | ||
|
|
00ecd7adc5 | ||
|
|
c39cb3bffc | ||
|
|
2fa902bfff | ||
|
|
f8bcd351ae | ||
|
|
6013d99bf6 | ||
|
|
e7c3977f7b | ||
|
|
47e1218fe0 | ||
|
|
a71a95892f | ||
|
|
b5f53e309f | ||
|
|
3164ba2d98 | ||
|
|
89854d188d | ||
|
|
79c7475435 | ||
|
|
2ee477c35e | ||
|
|
5bcd90c569 | ||
|
|
1a49c7c59e |
65
.github/workflows/build-windows.yml
vendored
Normal file
65
.github/workflows/build-windows.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: MoviePilot Windows Builder
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- version.py
|
||||
|
||||
jobs:
|
||||
Windows-build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Release Version
|
||||
id: release_version
|
||||
run: |
|
||||
$app_version = Select-String -Path "version.py" -Pattern "APP_VERSION\s=\s'v(.*)'" | ForEach-Object { $_.Matches.Groups[1].Value }
|
||||
$env:GITHUB_ENV += "app_version=$app_version"
|
||||
|
||||
- name: Init Python 3.11.4
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11.4'
|
||||
|
||||
- name: Install Dependent Packages
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install wheel pyinstaller
|
||||
pip install -r requirements.txt
|
||||
shell: pwsh
|
||||
|
||||
- name: Pyinstaller
|
||||
run: |
|
||||
pyinstaller windows.spec
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload Windows File
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows
|
||||
path: dist/MoviePilot.exe
|
||||
|
||||
- name: Generate Release
|
||||
id: generate_release
|
||||
uses: actions/create-release@latest
|
||||
with:
|
||||
tag_name: v${{ env.app_version }}
|
||||
release_name: v${{ env.app_version }}
|
||||
body: ${{ github.event.commits[0].message }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
uses: dwenegar/upload-release-assets@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
release_id: ${{ steps.generate_release.outputs.id }}
|
||||
assets_path: |
|
||||
dist/MoviePilot.exe
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: MoviePilot Builder
|
||||
name: MoviePilot Docker Builder
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -8,23 +8,20 @@ on:
|
||||
- version.py
|
||||
|
||||
jobs:
|
||||
build:
|
||||
Docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Docker Image
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
-
|
||||
name: Release version
|
||||
- name: Release version
|
||||
id: release_version
|
||||
run: |
|
||||
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
||||
echo "app_version=$app_version" >> $GITHUB_ENV
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
@@ -33,23 +30,19 @@ jobs:
|
||||
type=raw,value=${{ env.app_version }}
|
||||
type=raw,value=latest
|
||||
|
||||
-
|
||||
name: Set Up QEMU
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
-
|
||||
name: Set Up Buildx
|
||||
- name: Set Up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
-
|
||||
name: Login DockerHub
|
||||
- name: Login DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
-
|
||||
name: Build Image
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
|
||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: MoviePilot Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- version.py
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build Docker Image
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
-
|
||||
name: Release Version
|
||||
id: release_version
|
||||
run: |
|
||||
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
||||
echo "app_version=$app_version" >> $GITHUB_ENV
|
||||
|
||||
-
|
||||
name: Generate Release
|
||||
uses: actions/create-release@latest
|
||||
with:
|
||||
tag_name: v${{ env.app_version }}
|
||||
release_name: v${{ env.app_version }}
|
||||
body: ${{ github.event.commits[0].message }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
28
Dockerfile
28
Dockerfile
@@ -3,36 +3,14 @@ ARG MOVIEPILOT_VERSION
|
||||
ENV LANG="C.UTF-8" \
|
||||
HOME="/moviepilot" \
|
||||
TERM="xterm" \
|
||||
TZ="Asia/Shanghai" \
|
||||
PUID=0 \
|
||||
PGID=0 \
|
||||
UMASK=000 \
|
||||
MOVIEPILOT_AUTO_UPDATE=true \
|
||||
MOVIEPILOT_AUTO_UPDATE_DEV=false \
|
||||
PORT=3001 \
|
||||
NGINX_PORT=3000 \
|
||||
CONFIG_DIR="/config" \
|
||||
API_TOKEN="moviepilot" \
|
||||
AUTH_SITE="iyuu" \
|
||||
DOWNLOAD_PATH="/downloads" \
|
||||
DOWNLOAD_CATEGORY="false" \
|
||||
TORRENT_TAG="MOVIEPILOT" \
|
||||
LIBRARY_PATH="" \
|
||||
LIBRARY_CATEGORY="false" \
|
||||
TRANSFER_TYPE="copy" \
|
||||
COOKIECLOUD_HOST="https://movie-pilot.org/cookiecloud" \
|
||||
COOKIECLOUD_KEY="" \
|
||||
COOKIECLOUD_PASSWORD="" \
|
||||
MESSAGER="telegram" \
|
||||
TELEGRAM_TOKEN="" \
|
||||
TELEGRAM_CHAT_ID="" \
|
||||
DOWNLOADER="qbittorrent" \
|
||||
QB_HOST="127.0.0.1:8080" \
|
||||
QB_USER="admin" \
|
||||
QB_PASSWORD="adminadmin" \
|
||||
MEDIASERVER="emby" \
|
||||
EMBY_HOST="http://127.0.0.1:8096" \
|
||||
EMBY_API_KEY=""
|
||||
MOVIEPILOT_AUTO_UPDATE=true \
|
||||
MOVIEPILOT_AUTO_UPDATE_DEV=false \
|
||||
CONFIG_DIR="/config"
|
||||
WORKDIR "/app"
|
||||
RUN apt-get update -y \
|
||||
&& apt-get -y install \
|
||||
|
||||
123
README.md
123
README.md
@@ -15,23 +15,23 @@ Docker:https://hub.docker.com/r/jxxghp/moviepilot
|
||||
|
||||
## 安装
|
||||
|
||||
1. **安装CookieCloud插件**
|
||||
### 1. **安装CookieCloud插件**
|
||||
|
||||
站点信息需要通过CookieCloud同步获取,因此需要安装CookieCloud插件,将浏览器中的站点Cookie数据同步到云端后再同步到MoviePilot使用。 插件下载地址请点击 [这里](https://github.com/easychen/CookieCloud/releases)。
|
||||
|
||||
2. **安装CookieCloud服务端(可选)**
|
||||
### 2. **安装CookieCloud服务端(可选)**
|
||||
|
||||
MoviePilot内置了公共CookieCloud服务器,如果需要自建服务,可参考 [CookieCloud](https://github.com/easychen/CookieCloud) 项目进行搭建,docker镜像请点击 [这里](https://hub.docker.com/r/easychen/cookiecloud)。
|
||||
|
||||
**声明:** 本项目不会收集用户敏感数据,Cookie同步也是基于CookieCloud项目实现,非本项目提供的能力。技术角度上CookieCloud采用端到端加密,在个人不泄露`用户KEY`和`端对端加密密码`的情况下第三方无法窃取任何用户信息(包括服务器持有者)。如果你不放心,可以不使用公共服务或者不使用本项目,但如果使用后发生了任何信息泄露与本项目无关!
|
||||
|
||||
3. **安装配套管理软件**
|
||||
### 3. **安装配套管理软件**
|
||||
|
||||
MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- 下载器支持:qBittorrent、Transmission,QB版本号要求>= 4.3.9,TR版本号要求>= 3.0,推荐使用QB。
|
||||
- 媒体服务器支持:Jellyfin、Emby、Plex,推荐使用Emby。
|
||||
|
||||
4. **安装MoviePilot**
|
||||
### 4. **安装MoviePilot**
|
||||
|
||||
目前仅提供docker镜像,点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令:
|
||||
|
||||
@@ -41,51 +41,56 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
## 配置
|
||||
|
||||
项目的所有配置均通过环境变量进行设置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
|
||||
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
|
||||
- 在docker环境变量部分进行参数配置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
|
||||
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
|
||||
|
||||
配置文件映射路径:`/config`
|
||||
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,部分参数如路径映射、站点认证、权限端口等必须通过环境变量进行配置。
|
||||
|
||||
> $\color{red}{*}$ 号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
|
||||
|
||||
### 1. **基础设置**
|
||||
|
||||
- **PUID**:运行程序用户的`uid`,默认`0`
|
||||
- **PGID**:运行程序用户的`gid`,默认`0`
|
||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`
|
||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**
|
||||
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`
|
||||
- **NGINX_PORT:** WEB服务端口,默认`3000`,可自行修改,不能与API服务端口冲突
|
||||
- **PORT:** API服务端口,默认`3001`,可自行修改,不能与WEB服务端口冲突
|
||||
- **SUPERUSER:** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
||||
- **SUPERUSER_PASSWORD:** 超级管理员初始密码,默认`password`,建议修改为复杂密码
|
||||
- **API_TOKEN:** API密钥,默认`moviepilot`,在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
|
||||
- **PROXY_HOST:** 网络代理(可选),访问themoviedb或者重启更新需要使用代理访问,格式为`http(s)://ip:port`
|
||||
- **NGINX_PORT $\color{red}{*}$ :** WEB服务端口,默认`3000`,可自行修改,不能与API服务端口冲突(仅支持环境变量配置)
|
||||
- **PORT $\color{red}{*}$ :** API服务端口,默认`3001`,可自行修改,不能与WEB服务端口冲突(仅支持环境变量配置)
|
||||
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
|
||||
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
|
||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
|
||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**(仅支持环境变量配置)
|
||||
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`(仅支持环境变量配置)
|
||||
---
|
||||
- **SUPERUSER $\color{red}{*}$ :** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
||||
- **SUPERUSER_PASSWORD $\color{red}{*}$ :** 超级管理员初始密码,默认`password`,建议修改为复杂密码
|
||||
- **API_TOKEN $\color{red}{*}$ :** API密钥,默认`moviepilot`,在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
|
||||
- **PROXY_HOST:** 网络代理,访问themoviedb或者重启更新需要使用代理访问,格式为`http(s)://ip:port`、`socks5://user:pass@host:port`(可选)
|
||||
- **TMDB_API_DOMAIN:** TMDB API地址,默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
|
||||
- **DOWNLOAD_PATH:** 下载保存目录,**注意:需要将`moviepilot`及`下载器`的映射路径保持一致**,否则会导致下载文件无法转移
|
||||
- **DOWNLOAD_MOVIE_PATH:** 电影下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_TV_PATH:** 电视剧下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_ANIME_PATH:** 动漫下载保存目录,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_CATEGORY:** 下载二级分类开关,`true`/`false`,默认`false`,开启后会根据配置`category.yaml`自动在下载目录下建立二级目录分类
|
||||
- **DOWNLOAD_SUBTITLE:** 下载站点字幕,`true`/`false`,默认`true`
|
||||
- **REFRESH_MEDIASERVER:** 入库刷新媒体库,`true`/`false`,默认`true`
|
||||
- **TMDB_IMAGE_DOMAIN:** TMDB图片地址,默认`image.tmdb.org`,可配置为其它中转代理以加速TMDB图片显示,如:`static-mdb.v.geilijiasu.com`
|
||||
---
|
||||
- **SCRAP_METADATA:** 刮削入库的媒体文件,`true`/`false`,默认`true`
|
||||
- **SCRAP_SOURCE:** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
|
||||
- **SCRAP_FOLLOW_TMDB:** 新增已入库媒体是否跟随TMDB信息变化,`true`/`false`,默认`true`
|
||||
- **TORRENT_TAG:** 种子标签,默认为`MOVIEPILOT`,设置后只有MoviePilot添加的下载才会处理,留空所有下载器中的任务均会处理
|
||||
- **LIBRARY_PATH:** 媒体库目录,多个目录使用`,`分隔
|
||||
- **LIBRARY_MOVIE_NAME:** 电影媒体库目录名,默认`电影`
|
||||
- **LIBRARY_TV_NAME:** 电视剧媒体库目录名,默认`电视剧`
|
||||
- **LIBRARY_ANIME_NAME:** 动漫媒体库目录名,默认`电视剧/动漫`
|
||||
- **LIBRARY_CATEGORY:** 媒体库二级分类开关,`true`/`false`,默认`false`,开启后会根据配置`category.yaml`自动在媒体库目录下建立二级目录分类
|
||||
- **TRANSFER_TYPE:** 转移方式,支持`link`/`copy`/`move`/`softlink` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响**
|
||||
- **COOKIECLOUD_HOST:** CookieCloud服务器地址,格式:`http(s)://ip:port`,不配置默认使用内建服务器`https://movie-pilot.org/cookiecloud`
|
||||
- **COOKIECLOUD_KEY:** CookieCloud用户KEY
|
||||
- **COOKIECLOUD_PASSWORD:** CookieCloud端对端加密密码
|
||||
- **COOKIECLOUD_INTERVAL:** CookieCloud同步间隔(分钟)
|
||||
- **OCR_HOST:** OCR识别服务器地址,格式:`http(s)://ip:port`,用于识别站点二维码实现自动登录获取Cookie等,不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
|
||||
- **USER_AGENT:** CookieCloud对应的浏览器UA,可选,设置后可增加连接站点的成功率,同步站点后可以在管理界面中修改
|
||||
- **AUTO_DOWNLOAD_USER:** 交互搜索自动下载用户ID,使用,分割
|
||||
---
|
||||
- **TRANSFER_TYPE $\color{red}{*}$ :** 整理转移方式,支持`link`/`copy`/`move`/`softlink` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响**
|
||||
- **LIBRARY_PATH $\color{red}{*}$ :** 媒体库目录,多个目录使用`,`分隔
|
||||
- **LIBRARY_MOVIE_NAME:** 电影媒体库目录名称(不是完整路径),默认`电影`
|
||||
- **LIBRARY_TV_NAME:** 电视剧媒体库目录称(不是完整路径),默认`电视剧`
|
||||
- **LIBRARY_ANIME_NAME:** 动漫媒体库目录称(不是完整路径),默认`电视剧/动漫`
|
||||
- **LIBRARY_CATEGORY:** 媒体库二级分类开关,`true`/`false`,默认`false`,开启后会根据配置 [category.yaml](https://github.com/jxxghp/MoviePilot/raw/main/config/category.yaml) 自动在媒体库目录下建立二级目录分类
|
||||
---
|
||||
- **COOKIECLOUD_HOST $\color{red}{*}$ :** CookieCloud服务器地址,格式:`http(s)://ip:port`,不配置默认使用内建服务器`https://movie-pilot.org/cookiecloud`
|
||||
- **COOKIECLOUD_KEY $\color{red}{*}$ :** CookieCloud用户KEY
|
||||
- **COOKIECLOUD_PASSWORD $\color{red}{*}$ :** CookieCloud端对端加密密码
|
||||
- **COOKIECLOUD_INTERVAL $\color{red}{*}$ :** CookieCloud同步间隔(分钟)
|
||||
- **USER_AGENT $\color{red}{*}$ :** CookieCloud保存Cookie对应的浏览器UA,建议配置,设置后可增加连接站点的成功率,同步站点后可以在管理界面中修改
|
||||
- **OCR_HOST:** OCR识别服务器地址,格式:`http(s)://ip:port`,用于识别站点验证码实现自动登录获取Cookie等,不配置默认使用内建服务器`https://movie-pilot.org`,可使用 [这个镜像](https://hub.docker.com/r/jxxghp/moviepilot-ocr) 自行搭建。
|
||||
---
|
||||
- **SUBSCRIBE_MODE:** 订阅模式,`rss`/`spider`,默认`spider`,`rss`模式通过定时刷新RSS来匹配订阅(RSS地址会自动获取,也可手动维护),对站点压力小,同时可设置订阅刷新周期,24小时运行,但订阅和下载通知不能过滤和显示免费,推荐使用rss模式。
|
||||
- **SUBSCRIBE_RSS_INTERVAL:** RSS订阅模式刷新时间间隔(分钟),默认`30`分钟,不能小于5分钟。
|
||||
- **SUBSCRIBE_SEARCH:** 订阅搜索,`true`/`false`,默认`false`,开启后会每隔24小时对所有订阅进行全量搜索,以补齐缺失剧集(一般情况下正常订阅即可,订阅搜索只做为兜底,会增加站点压力,不建议开启)。
|
||||
- **MESSAGER:** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
|
||||
- **SEARCH_SOURCE:** 媒体信息搜索来源,`themoviedb`/`douban`,默认`themoviedb`
|
||||
---
|
||||
- **AUTO_DOWNLOAD_USER:** 远程交互搜索时自动择优下载的用户ID,多个用户使用,分割,未设置需要选择资源或者回复`0`
|
||||
- **MESSAGER $\color{red}{*}$ :** 消息通知渠道,支持 `telegram`/`wechat`/`slack`/`synologychat`,开启多个渠道时使用`,`分隔。同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`telegram`
|
||||
|
||||
- `wechat`设置项:
|
||||
|
||||
@@ -102,21 +107,29 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **TELEGRAM_TOKEN:** Telegram Bot Token
|
||||
- **TELEGRAM_CHAT_ID:** Telegram Chat ID
|
||||
- **TELEGRAM_USERS:** Telegram 用户ID,多个使用,分隔,只有用户ID在列表中才可以使用Bot,如未设置则均可以使用Bot
|
||||
- **TELEGRAM_ADMINS:** Telegram 管理员ID,多个使用,分隔,只有管理员才可以操作Bot菜单,如未设置则均可以操作菜单
|
||||
- **TELEGRAM_ADMINS:** Telegram 管理员ID,多个使用,分隔,只有管理员才可以操作Bot菜单,如未设置则均可以操作菜单(可选)
|
||||
|
||||
- `slack`设置项:
|
||||
|
||||
- **SLACK_OAUTH_TOKEN:** Slack Bot User OAuth Token
|
||||
- **SLACK_APP_TOKEN:** Slack App-Level Token
|
||||
- **SLACK_CHANNEL:** Slack 频道名称,默认`全体`
|
||||
- **SLACK_CHANNEL:** Slack 频道名称,默认`全体`(可选)
|
||||
|
||||
- `synologychat`设置项:
|
||||
|
||||
- **SYNOLOGYCHAT_WEBHOOK:** 在Synology Chat中创建机器人,获取机器人`传入URL`
|
||||
- **SYNOLOGYCHAT_TOKEN:** SynologyChat机器人`令牌`
|
||||
|
||||
|
||||
- **DOWNLOADER:** 下载器,支持`qbittorrent`/`transmission`,QB版本号要求>= 4.3.9,TR版本号要求>= 3.0,同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`qbittorrent`
|
||||
---
|
||||
- **DOWNLOAD_PATH $\color{red}{*}$ :** 下载保存目录,**注意:需要将`moviepilot`及`下载器`的映射路径保持一致**,否则会导致下载文件无法转移
|
||||
- **DOWNLOAD_MOVIE_PATH:** 电影下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **DOWNLOAD_TV_PATH:** 电视剧下载保存目录路径,不设置则下载到`DOWNLOAD_PATH`
|
||||
- **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 $\color{red}{*}$ :** 下载器,支持`qbittorrent`/`transmission`,QB版本号要求>= 4.3.9,TR版本号要求>= 3.0,同时还需要配置对应渠道的环境变量,非对应渠道的变量可删除,推荐使用`qbittorrent`
|
||||
|
||||
- `qbittorrent`设置项:
|
||||
|
||||
@@ -131,9 +144,9 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **TR_USER:** transmission用户名
|
||||
- **TR_PASSWORD:** transmission密码
|
||||
|
||||
- **DOWNLOADER_MONITOR:** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
|
||||
|
||||
- **MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||
---
|
||||
- **REFRESH_MEDIASERVER:** 入库后是否刷新媒体服务器,`true`/`false`,默认`true`
|
||||
- **MEDIASERVER $\color{red}{*}$ :** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||
|
||||
- `emby`设置项:
|
||||
|
||||
@@ -156,9 +169,9 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
### 2. **用户认证**
|
||||
|
||||
- **AUTH_SITE:** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过docker环境变量配置**)
|
||||
|
||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数。
|
||||
- **AUTH_SITE $\color{red}{*}$ :** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||
|
||||
| 站点 | 参数 |
|
||||
|:------------:|:-----------------------------------------------------:|
|
||||
@@ -195,6 +208,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
> `edition`: 版本(资源类型+特效)
|
||||
> `videoFormat`: 分辨率
|
||||
> `releaseGroup`: 制作组/字幕组
|
||||
> `customization`: 自定义占位符
|
||||
> `videoCodec`: 视频编码
|
||||
> `audioCodec`: 音频编码
|
||||
> `tmdbid`: TMDBID
|
||||
@@ -224,9 +238,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
```
|
||||
|
||||
|
||||
### 3. **过滤规则**
|
||||
|
||||
在`设定`-`规则`中设定,规则说明:
|
||||
### 3. **优先级规则**
|
||||
|
||||
- 仅支持使用内置规则进行排列组合,内置规则有:`蓝光原盘`、`4K`、`1080P`、`中文字幕`、`特效字幕`、`H265`、`H264`、`杜比`、`HDR`、`REMUX`、`WEB-DL`、`免费`、`国语配音` 等
|
||||
- 符合任一层级规则的资源将被标识选中,匹配成功的层级做为该资源的优先级,排越前面优先级超高
|
||||
@@ -243,10 +255,9 @@ docker pull jxxghp/moviepilot:latest
|
||||
- 将MoviePilot做为Radarr或Sonarr服务器添加到Overseerr或Jellyseerr(`API服务端口`),可使用Overseerr/Jellyseerr浏览订阅。
|
||||
- 映射宿主机docker.sock文件到容器`/var/run/docker.sock`,以支持内建重启操作。实例:`-v /var/run/docker.sock:/var/run/docker.sock:ro`
|
||||
|
||||
**注意**
|
||||
|
||||
1) 容器首次启动需要下载浏览器内核,根据网络情况可能需要较长时间,此时无法登录。可映射`/moviepilot`目录避免容器重置后重新触发浏览器内核下载。
|
||||
2) 使用反向代理时,需要添加以下配置,否则可能会导致部分功能无法访问(`ip:port`修改为实际值):
|
||||
### **注意**
|
||||
- 容器首次启动需要下载浏览器内核,根据网络情况可能需要较长时间,此时无法登录。可映射`/moviepilot`目录避免容器重置后重新触发浏览器内核下载。
|
||||
- 使用反向代理时,需要添加以下配置,否则可能会导致部分功能无法访问(`ip:port`修改为实际值):
|
||||
```nginx configuration
|
||||
location / {
|
||||
proxy_pass http://ip:port;
|
||||
@@ -256,7 +267,7 @@ location / {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
3) 新建的企业微信应用需要固定公网IP的代理才能收到消息,代理添加以下代码:
|
||||
- 新建的企业微信应用需要固定公网IP的代理才能收到消息,代理添加以下代码:
|
||||
```nginx configuration
|
||||
location /cgi-bin/gettoken {
|
||||
proxy_pass https://qyapi.weixin.qq.com;
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from requests import Session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
from app.chain.dashboard import DashboardChain
|
||||
|
||||
@@ -16,10 +16,16 @@ router = APIRouter()
|
||||
IMAGE_TYPES = [".jpg", ".png", ".gif", ".bmp", ".jpeg", ".webp"]
|
||||
|
||||
|
||||
@router.get("/list", summary="所有插件", response_model=List[schemas.FileItem])
|
||||
def list_path(path: str, sort: str = 'time', _: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
@router.get("/list", summary="所有目录和文件", response_model=List[schemas.FileItem])
|
||||
def list_path(path: str,
|
||||
sort: str = 'time',
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询当前目录下所有目录和文件
|
||||
:param path: 目录路径
|
||||
:param sort: 排序方式,name:按名称排序,time:按修改时间排序
|
||||
:param _: token
|
||||
:return: 所有目录和文件
|
||||
"""
|
||||
# 返回结果
|
||||
ret_items = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from requests import Session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
from app.chain.media import MediaChain
|
||||
|
||||
@@ -74,8 +74,7 @@ class DownloadChain(ChainBase):
|
||||
title=f"{mediainfo.title_year} "
|
||||
f"{meta.season_episode} 开始下载",
|
||||
text=msg_text,
|
||||
image=mediainfo.get_message_image(),
|
||||
userid=userid))
|
||||
image=mediainfo.get_message_image()))
|
||||
|
||||
def download_torrent(self, torrent: TorrentInfo,
|
||||
channel: MessageChannel = None,
|
||||
|
||||
@@ -3,7 +3,7 @@ import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union, Tuple
|
||||
|
||||
from requests import Session
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.download import DownloadChain
|
||||
|
||||
@@ -5,6 +5,7 @@ from cachetools import cached, TTLCache
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import settings
|
||||
from app.schemas import MediaType
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
@@ -122,5 +123,5 @@ class TmdbChain(ChainBase, metaclass=Singleton):
|
||||
while True:
|
||||
info = random.choice(infos)
|
||||
if info and info.get("backdrop_path"):
|
||||
return f"https://image.tmdb.org/t/p/original{info.get('backdrop_path')}"
|
||||
return f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('backdrop_path')}"
|
||||
return None
|
||||
|
||||
@@ -622,8 +622,9 @@ class TransferChain(ChainBase):
|
||||
if not path.exists():
|
||||
return
|
||||
if path.is_file():
|
||||
# 删除文件、nfo、jpg
|
||||
files = glob.glob(f"{Path(path.parent).joinpath(path.stem)}*")
|
||||
# 删除文件、nfo、jpg等同名文件
|
||||
pattern = path.stem.replace('[', '?').replace(']', '?')
|
||||
files = path.parent.glob(f"{pattern}.*")
|
||||
for file in files:
|
||||
Path(file).unlink()
|
||||
logger.warn(f"文件 {path} 已删除")
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 项目名称
|
||||
@@ -208,7 +212,11 @@ class Settings(BaseSettings):
|
||||
def CONFIG_PATH(self):
|
||||
if self.CONFIG_DIR:
|
||||
return Path(self.CONFIG_DIR)
|
||||
return self.INNER_CONFIG_PATH
|
||||
elif SystemUtils.is_docker():
|
||||
return Path("/config")
|
||||
elif SystemUtils.is_frozen():
|
||||
return Path(sys.executable).parent / "config"
|
||||
return self.ROOT_PATH / "config"
|
||||
|
||||
@property
|
||||
def TEMP_PATH(self):
|
||||
@@ -268,11 +276,14 @@ class Settings(BaseSettings):
|
||||
return [Path(path) for path in self.LIBRARY_PATH.split(",")]
|
||||
return []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
with self.CONFIG_PATH as p:
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
if SystemUtils.is_frozen():
|
||||
if not (p / "app.env").exists():
|
||||
SystemUtils.copy(self.INNER_CONFIG_PATH / "app.env", p / "app.env")
|
||||
with self.TEMP_PATH as p:
|
||||
if not p.exists():
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
@@ -284,4 +295,7 @@ class Settings(BaseSettings):
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
settings = Settings()
|
||||
settings = Settings(
|
||||
_env_file=Settings().CONFIG_PATH / "app.env",
|
||||
_env_file_encoding="utf-8"
|
||||
)
|
||||
|
||||
47
app/core/meta/customization.py
Normal file
47
app/core/meta/customization.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import regex as re
|
||||
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
class CustomizationMatcher(metaclass=Singleton):
|
||||
"""
|
||||
识别自定义占位符
|
||||
"""
|
||||
customization = None
|
||||
custom_separator = None
|
||||
|
||||
def __init__(self):
|
||||
self.systemconfig = SystemConfigOper()
|
||||
self.customization = None
|
||||
self.custom_separator = None
|
||||
|
||||
def match(self, title=None):
|
||||
"""
|
||||
:param title: 资源标题或文件名
|
||||
:return: 匹配结果
|
||||
"""
|
||||
if not title:
|
||||
return ""
|
||||
if not self.customization:
|
||||
# 自定义占位符
|
||||
customization = self.systemconfig.get(SystemConfigKey.Customization)
|
||||
if not customization:
|
||||
return ""
|
||||
if isinstance(customization, str):
|
||||
customization = customization.replace("\n", ";").replace("|", ";").strip(";").split(";")
|
||||
self.customization = "|".join([f"({item})" for item in customization])
|
||||
|
||||
customization_re = re.compile(r"%s" % self.customization)
|
||||
# 处理重复多次的情况,保留先后顺序(按添加自定义占位符的顺序)
|
||||
unique_customization = {}
|
||||
for item in re.findall(customization_re, title):
|
||||
if not isinstance(item, tuple):
|
||||
item = (item,)
|
||||
for i in range(len(item)):
|
||||
if item[i] and unique_customization.get(item[i]) is None:
|
||||
unique_customization[item[i]] = i
|
||||
unique_customization = list(dict(sorted(unique_customization.items(), key=lambda x: x[1])).keys())
|
||||
separator = self.custom_separator or "@"
|
||||
return separator.join(unique_customization)
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import zhconv
|
||||
import anitopy
|
||||
from app.core.meta.customization import CustomizationMatcher
|
||||
from app.core.meta.metabase import MetaBase
|
||||
from app.core.meta.releasegroup import ReleaseGroupsMatcher
|
||||
from app.utils.string import StringUtils
|
||||
@@ -144,6 +145,8 @@ class MetaAnime(MetaBase):
|
||||
self.resource_team = \
|
||||
ReleaseGroupsMatcher().match(title=original_title) or \
|
||||
anitopy_info_origin.get("release_group") or None
|
||||
# 自定义占位符
|
||||
self.customization = CustomizationMatcher().match(title=original_title) or None
|
||||
# 视频编码
|
||||
self.video_encode = anitopy_info.get("video_term")
|
||||
if isinstance(self.video_encode, list):
|
||||
|
||||
@@ -51,6 +51,8 @@ class MetaBase(object):
|
||||
resource_pix: Optional[str] = None
|
||||
# 识别的制作组/字幕组
|
||||
resource_team: Optional[str] = None
|
||||
# 识别的自定义占位符
|
||||
customization: Optional[str] = None
|
||||
# 视频编码
|
||||
video_encode: Optional[str] = None
|
||||
# 音频编码
|
||||
@@ -492,6 +494,9 @@ class MetaBase(object):
|
||||
# 制作组/字幕组
|
||||
if not self.resource_team:
|
||||
self.resource_team = meta.resource_team
|
||||
# 自定义占位符
|
||||
if not self.customization:
|
||||
self.customization = meta.customization
|
||||
# 特效
|
||||
if not self.resource_effect:
|
||||
self.resource_effect = meta.resource_effect
|
||||
|
||||
@@ -2,6 +2,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.meta.customization import CustomizationMatcher
|
||||
from app.core.meta.metabase import MetaBase
|
||||
from app.core.meta.releasegroup import ReleaseGroupsMatcher
|
||||
from app.utils.string import StringUtils
|
||||
@@ -130,6 +131,8 @@ class MetaVideo(MetaBase):
|
||||
self.part = None
|
||||
# 制作组/字幕组
|
||||
self.resource_team = ReleaseGroupsMatcher().match(title=original_title) or None
|
||||
# 自定义占位符
|
||||
self.customization = CustomizationMatcher().match(title=original_title) or None
|
||||
|
||||
def __fix_name(self, name: str):
|
||||
if not name:
|
||||
|
||||
@@ -39,7 +39,7 @@ def update_db():
|
||||
更新数据库
|
||||
"""
|
||||
db_location = settings.CONFIG_PATH / 'user.db'
|
||||
script_location = settings.ROOT_PATH / 'alembic'
|
||||
script_location = settings.ROOT_PATH / 'database'
|
||||
try:
|
||||
alembic_cfg = Config()
|
||||
alembic_cfg.set_main_option('script_location', str(script_location))
|
||||
|
||||
@@ -2,12 +2,15 @@ from pyvirtualdisplay import Display
|
||||
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
class DisplayHelper(metaclass=Singleton):
|
||||
_display: Display = None
|
||||
|
||||
def __init__(self):
|
||||
if not SystemUtils.is_docker():
|
||||
return
|
||||
try:
|
||||
self._display = Display(visible=False, size=(1024, 768))
|
||||
self._display.start()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
@@ -10,6 +11,7 @@ from app.modules import _ModuleBase
|
||||
from app.modules.douban.apiv2 import DoubanApi
|
||||
from app.modules.douban.scraper import DoubanScraper
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
|
||||
@@ -393,6 +395,7 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
return ret_medias
|
||||
|
||||
@retry(Exception, 5, 3, 3, logger=logger)
|
||||
def match_doubaninfo(self, name: str, mtype: str = None,
|
||||
year: str = None, season: int = None) -> dict:
|
||||
"""
|
||||
@@ -402,9 +405,15 @@ class DoubanModule(_ModuleBase):
|
||||
:param year: 年份
|
||||
:param season: 季号
|
||||
"""
|
||||
result = self.doubanapi.search(f"{name} {year or ''}".strip())
|
||||
result = self.doubanapi.search(f"{name} {year or ''}".strip(),
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d%H%M%S'))
|
||||
if not result:
|
||||
logger.warn(f"未找到 {name} 的豆瓣信息")
|
||||
return {}
|
||||
# 触发rate limit
|
||||
if "search_access_rate_limit" in result.values():
|
||||
logger.warn(f"触发豆瓣API速率限制 错误信息 {result} ...")
|
||||
raise Exception("触发豆瓣API速率限制")
|
||||
for item_obj in result.get("items"):
|
||||
type_name = item_obj.get("type_name")
|
||||
if type_name not in [MediaType.TV.value, MediaType.MOVIE.value]:
|
||||
|
||||
@@ -146,72 +146,113 @@ class DoubanApi(metaclass=Singleton):
|
||||
_api_secret_key = "bf7dddc7c9cfe6f7"
|
||||
_api_key = "0dad551ec0f84ed02907ff5c42e8ec70"
|
||||
_base_url = "https://frodo.douban.com/api/v2"
|
||||
_session = requests.Session()
|
||||
_session = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self._session = requests.Session()
|
||||
|
||||
@classmethod
|
||||
def __sign(cls, url: str, ts: int, method='GET') -> str:
|
||||
url_path = parse.urlparse(url).path
|
||||
raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)])
|
||||
return base64.b64encode(hmac.new(cls._api_secret_key.encode(), raw_sign.encode(), hashlib.sha1).digest()
|
||||
).decode()
|
||||
return base64.b64encode(
|
||||
hmac.new(
|
||||
cls._api_secret_key.encode(),
|
||||
raw_sign.encode(),
|
||||
hashlib.sha1
|
||||
).digest()
|
||||
).decode()
|
||||
|
||||
@classmethod
|
||||
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
|
||||
def __invoke(cls, url, **kwargs):
|
||||
req_url = cls._base_url + url
|
||||
def __invoke(self, url, **kwargs):
|
||||
req_url = self._base_url + url
|
||||
|
||||
params = {'apiKey': cls._api_key}
|
||||
params = {'apiKey': self._api_key}
|
||||
if kwargs:
|
||||
params.update(kwargs)
|
||||
|
||||
ts = params.pop('_ts', int(datetime.strftime(datetime.now(), '%Y%m%d')))
|
||||
params.update({'os_rom': 'android', 'apiKey': cls._api_key, '_ts': ts, '_sig': cls.__sign(url=req_url, ts=ts)})
|
||||
|
||||
resp = RequestUtils(ua=choice(cls._user_agents), session=cls._session).get_res(url=req_url, params=params)
|
||||
|
||||
ts = params.pop(
|
||||
'_ts',
|
||||
datetime.strftime(datetime.now(), '%Y%m%d')
|
||||
)
|
||||
params.update({
|
||||
'os_rom': 'android',
|
||||
'apiKey': self._api_key,
|
||||
'_ts': ts,
|
||||
'_sig': self.__sign(url=req_url, ts=ts)
|
||||
})
|
||||
resp = RequestUtils(
|
||||
ua=choice(self._user_agents),
|
||||
session=self._session
|
||||
).get_res(url=req_url, params=params)
|
||||
if resp.status_code == 400 and "rate_limit" in resp.text:
|
||||
return resp.json()
|
||||
return resp.json() if resp else {}
|
||||
|
||||
def search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["search"], q=keyword, start=start, count=count, _ts=ts)
|
||||
def search(self, keyword, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["search"], q=keyword,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_search"], q=keyword, start=start, count=count, _ts=ts)
|
||||
def movie_search(self, keyword, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_search"], q=keyword,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_search"], q=keyword, start=start, count=count, _ts=ts)
|
||||
def tv_search(self, keyword, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_search"], q=keyword,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def book_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["book_search"], q=keyword, start=start, count=count, _ts=ts)
|
||||
def book_search(self, keyword, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["book_search"], q=keyword,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def group_search(self, keyword, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["group_search"], q=keyword, start=start, count=count, _ts=ts)
|
||||
def group_search(self, keyword, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["group_search"], q=keyword,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_showing(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_showing"], start=start, count=count, _ts=ts)
|
||||
def movie_showing(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_showing"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_soon(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_soon"], start=start, count=count, _ts=ts)
|
||||
def movie_soon(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_soon"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_hot_gaia(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_hot_gaia"], start=start, count=count, _ts=ts)
|
||||
def movie_hot_gaia(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_hot_gaia"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_hot(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_hot"], start=start, count=count, _ts=ts)
|
||||
def tv_hot(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_hot"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_animation(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_animation"], start=start, count=count, _ts=ts)
|
||||
def tv_animation(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_animation"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_variety_show(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_variety_show"], start=start, count=count, _ts=ts)
|
||||
def tv_variety_show(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_variety_show"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_rank_list(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_rank_list"], start=start, count=count, _ts=ts)
|
||||
def tv_rank_list(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_rank_list"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def show_hot(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["show_hot"], start=start, count=count, _ts=ts)
|
||||
def show_hot(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["show_hot"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_detail(self, subject_id):
|
||||
return self.__invoke(self._urls["movie_detail"] + subject_id)
|
||||
@@ -228,20 +269,30 @@ class DoubanApi(metaclass=Singleton):
|
||||
def book_detail(self, subject_id):
|
||||
return self.__invoke(self._urls["book_detail"] + subject_id)
|
||||
|
||||
def movie_top250(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_top250"], start=start, count=count, _ts=ts)
|
||||
def movie_top250(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_top250"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def movie_recommend(self, tags='', sort='R', start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort, start=start, count=count, _ts=ts)
|
||||
def movie_recommend(self, tags='', sort='R', start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_recommend(self, tags='', sort='R', start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort, start=start, count=count, _ts=ts)
|
||||
def tv_recommend(self, tags='', sort='R', start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_chinese_best_weekly(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_chinese_best_weekly"], start=start, count=count, _ts=ts)
|
||||
def tv_chinese_best_weekly(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_chinese_best_weekly"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def tv_global_best_weekly(self, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_global_best_weekly"], start=start, count=count, _ts=ts)
|
||||
def tv_global_best_weekly(self, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
return self.__invoke(self._urls["tv_global_best_weekly"],
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def doulist_detail(self, subject_id):
|
||||
"""
|
||||
@@ -250,7 +301,8 @@ class DoubanApi(metaclass=Singleton):
|
||||
"""
|
||||
return self.__invoke(self._urls["doulist"] + subject_id)
|
||||
|
||||
def doulist_items(self, subject_id, start=0, count=20, ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
def doulist_items(self, subject_id, start=0, count=20,
|
||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
"""
|
||||
豆列列表
|
||||
:param subject_id: 豆列id
|
||||
@@ -258,4 +310,9 @@ class DoubanApi(metaclass=Singleton):
|
||||
:param count: 数量
|
||||
:param ts: 时间戳
|
||||
"""
|
||||
return self.__invoke(self._urls["doulist_items"] % subject_id, start=start, count=count, _ts=ts)
|
||||
return self.__invoke(self._urls["doulist_items"] % subject_id,
|
||||
start=start, count=count, _ts=ts)
|
||||
|
||||
def __del__(self):
|
||||
if self._session:
|
||||
self._session.close()
|
||||
|
||||
@@ -473,8 +473,8 @@ class Emby(metaclass=Singleton):
|
||||
return None
|
||||
# 查找需要刷新的媒体库ID
|
||||
item_path = Path(item.target_path)
|
||||
# 匹配子目录
|
||||
for folder in self.folders:
|
||||
# 匹配子目录
|
||||
for subfolder in folder.get("SubFolders"):
|
||||
try:
|
||||
# 匹配子目录
|
||||
@@ -483,7 +483,8 @@ class Emby(metaclass=Singleton):
|
||||
return folder.get("Id")
|
||||
except Exception as err:
|
||||
print(str(err))
|
||||
# 如果找不到,只要路径中有分类目录名就命中
|
||||
# 如果找不到,只要路径中有分类目录名就命中
|
||||
for folder in self.folders:
|
||||
for subfolder in folder.get("SubFolders"):
|
||||
if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category,
|
||||
subfolder.get("Path")):
|
||||
|
||||
@@ -542,7 +542,9 @@ class FileTransferModule(_ModuleBase):
|
||||
# 剧集标题
|
||||
"episode_title": episode_title,
|
||||
# 文件后缀
|
||||
"fileExt": file_ext
|
||||
"fileExt": file_ext,
|
||||
# 自定义占位符
|
||||
"customization": meta.customization
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -225,6 +225,8 @@ class QbittorrentModule(_ModuleBase):
|
||||
"""
|
||||
# 调用Qbittorrent API查询实时信息
|
||||
info = self.qbittorrent.transfer_info()
|
||||
if not info:
|
||||
return schemas.DownloaderInfo()
|
||||
return schemas.DownloaderInfo(
|
||||
download_speed=info.get("dl_info_speed"),
|
||||
upload_speed=info.get("up_info_speed"),
|
||||
|
||||
@@ -345,7 +345,7 @@ class TheMovieDbModule(_ModuleBase):
|
||||
image_path = seasoninfo.get(image_type.value)
|
||||
|
||||
if image_path:
|
||||
return f"https://image.tmdb.org/t/p/{image_prefix}{image_path}"
|
||||
return f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/{image_prefix}{image_path}"
|
||||
return None
|
||||
|
||||
def movie_similar(self, tmdbid: int) -> List[dict]:
|
||||
|
||||
@@ -165,6 +165,10 @@ class TmdbScraper:
|
||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||
DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "")
|
||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||
DomUtils.add_node(doc, xactor, "thumb",
|
||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}")
|
||||
DomUtils.add_node(doc, xactor, "profile",
|
||||
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
||||
# 风格
|
||||
genres = mediainfo.genres or []
|
||||
for genre in genres:
|
||||
@@ -317,6 +321,10 @@ class TmdbScraper:
|
||||
DomUtils.add_node(doc, xactor, "name", actor.get("name") or "")
|
||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||
DomUtils.add_node(doc, xactor, "thumb",
|
||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}")
|
||||
DomUtils.add_node(doc, xactor, "profile",
|
||||
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
||||
# 保存文件
|
||||
self.__save_nfo(doc, file_path.with_suffix(".nfo"))
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from app.utils.http import RequestUtils
|
||||
from .exceptions import TMDbException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,7 +26,15 @@ class TMDb(object):
|
||||
TMDB_DOMAIN = "TMDB_DOMAIN"
|
||||
REQUEST_CACHE_MAXSIZE = None
|
||||
|
||||
def __init__(self, obj_cached=True):
|
||||
_req = None
|
||||
_session = None
|
||||
|
||||
def __init__(self, obj_cached=True, session=None):
|
||||
if session is not None:
|
||||
self._req = RequestUtils(session=session, proxies=self.proxies)
|
||||
else:
|
||||
self._session = requests.Session()
|
||||
self._req = RequestUtils(session=self._session, proxies=self.proxies)
|
||||
self._remaining = 40
|
||||
self._reset = None
|
||||
self._timeout = 15
|
||||
@@ -128,10 +138,18 @@ class TMDb(object):
|
||||
os.environ[self.TMDB_CACHE_ENABLED] = str(cache)
|
||||
|
||||
@lru_cache(maxsize=REQUEST_CACHE_MAXSIZE)
|
||||
def cached_request(self, method, url, data, json):
|
||||
with requests.Session() as s:
|
||||
return s.request(method, url, data=data, json=json,
|
||||
timeout=self._timeout, proxies=self.proxies)
|
||||
def cached_request(self, method, url, data, json,
|
||||
_ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||
"""
|
||||
缓存请求,时间默认1天
|
||||
"""
|
||||
return self.request(method, url, data, json)
|
||||
|
||||
def request(self, method, url, data, json):
|
||||
if method == "GET":
|
||||
return self._req.get_res(url, params=data, json=json)
|
||||
else:
|
||||
return self._req.post_res(url, data=data, json=json)
|
||||
|
||||
def cache_clear(self):
|
||||
return self.cached_request.cache_clear()
|
||||
@@ -152,9 +170,7 @@ class TMDb(object):
|
||||
if self.cache and self.obj_cached and call_cached and method != "POST":
|
||||
req = self.cached_request(method, url, data, json)
|
||||
else:
|
||||
with requests.Session() as s:
|
||||
req = s.request(method, url, data=data, json=json,
|
||||
timeout=self._timeout, proxies=self.proxies)
|
||||
req = self.request(method, url, data, json)
|
||||
|
||||
headers = req.headers
|
||||
|
||||
@@ -199,3 +215,7 @@ class TMDb(object):
|
||||
if key:
|
||||
return json.get(key)
|
||||
return json
|
||||
|
||||
def __del__(self):
|
||||
if self._session:
|
||||
self._session.close()
|
||||
|
||||
@@ -211,6 +211,8 @@ class TransmissionModule(_ModuleBase):
|
||||
下载器信息
|
||||
"""
|
||||
info = self.transmission.transfer_info()
|
||||
if not info:
|
||||
return schemas.DownloaderInfo()
|
||||
return schemas.DownloaderInfo(
|
||||
download_speed=info.download_speed,
|
||||
upload_speed=info.upload_speed,
|
||||
|
||||
@@ -237,7 +237,7 @@ class BrushFlow(_PluginBase):
|
||||
self._scheduler.add_job(self.brush, 'interval', minutes=self._cron)
|
||||
except Exception as e:
|
||||
logger.error(f"站点刷流服务启动失败:{e}")
|
||||
self.systemmessage(f"站点刷流服务启动失败:{e}")
|
||||
self.systemmessage.put(f"站点刷流服务启动失败:{e}")
|
||||
return
|
||||
if self._onlyonce:
|
||||
logger.info(f"站点刷流服务启动,立即运行一次")
|
||||
@@ -1882,7 +1882,7 @@ class BrushFlow(_PluginBase):
|
||||
pubdate = pubdate.replace("T", " ").replace("Z", "")
|
||||
pubdate = datetime.strptime(pubdate, "%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.now()
|
||||
return (now - pubdate).seconds // 60
|
||||
return (now - pubdate).total_seconds() // 60
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return 0
|
||||
|
||||
@@ -87,6 +87,8 @@ class DirMonitor(_PluginBase):
|
||||
_exclude_keywords = ""
|
||||
# 存储源目录与目的目录关系
|
||||
_dirconf: Dict[str, Path] = {}
|
||||
# 存储源目录转移方式
|
||||
_transferconf: Dict[str, str] = {}
|
||||
_medias = {}
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
@@ -98,6 +100,7 @@ class DirMonitor(_PluginBase):
|
||||
self.tmdbchain = TmdbChain(self.db)
|
||||
# 清空配置
|
||||
self._dirconf = {}
|
||||
self._transferconf = {}
|
||||
|
||||
# 读取配置
|
||||
if config:
|
||||
@@ -132,6 +135,13 @@ class DirMonitor(_PluginBase):
|
||||
paths = [mon_path]
|
||||
else:
|
||||
paths = mon_path.split(":")
|
||||
|
||||
# 自定义转移方式
|
||||
if mon_path.count("#") == 1:
|
||||
self._transferconf[mon_path] = mon_path.split("#")[1]
|
||||
else:
|
||||
self._transferconf[mon_path] = self._transfer_type
|
||||
|
||||
target_path = None
|
||||
if len(paths) > 1:
|
||||
mon_path = paths[0]
|
||||
@@ -247,6 +257,8 @@ class DirMonitor(_PluginBase):
|
||||
|
||||
# 查询转移目的目录
|
||||
target: Path = self._dirconf.get(mon_path)
|
||||
# 查询转移方式
|
||||
transfer_type = self._transferconf.get(mon_path)
|
||||
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(meta=file_meta)
|
||||
@@ -260,7 +272,7 @@ class DirMonitor(_PluginBase):
|
||||
# 新增转移成功历史记录
|
||||
self.transferhis.add_fail(
|
||||
src_path=file_path,
|
||||
mode=self._transfer_type,
|
||||
mode=transfer_type,
|
||||
meta=file_meta
|
||||
)
|
||||
return
|
||||
@@ -289,7 +301,7 @@ class DirMonitor(_PluginBase):
|
||||
# 转移
|
||||
transferinfo: TransferInfo = self.chain.transfer(mediainfo=mediainfo,
|
||||
path=file_path,
|
||||
transfer_type=self._transfer_type,
|
||||
transfer_type=transfer_type,
|
||||
target=target,
|
||||
meta=file_meta,
|
||||
episodes_info=episodes_info)
|
||||
@@ -303,7 +315,7 @@ class DirMonitor(_PluginBase):
|
||||
# 新增转移失败历史记录
|
||||
self.transferhis.add_fail(
|
||||
src_path=file_path,
|
||||
mode=self._transfer_type,
|
||||
mode=transfer_type,
|
||||
download_hash=download_hash,
|
||||
meta=file_meta,
|
||||
mediainfo=mediainfo,
|
||||
@@ -320,7 +332,7 @@ class DirMonitor(_PluginBase):
|
||||
# 新增转移成功历史记录
|
||||
self.transferhis.add_success(
|
||||
src_path=file_path,
|
||||
mode=self._transfer_type,
|
||||
mode=transfer_type,
|
||||
download_hash=download_hash,
|
||||
meta=file_meta,
|
||||
mediainfo=mediainfo,
|
||||
@@ -402,7 +414,7 @@ class DirMonitor(_PluginBase):
|
||||
})
|
||||
|
||||
# 移动模式删除空目录
|
||||
if self._transfer_type == "move":
|
||||
if transfer_type == "move":
|
||||
for file_dir in file_path.parents:
|
||||
if len(str(file_dir)) <= len(str(Path(mon_path))):
|
||||
# 重要,删除到监控目录为止
|
||||
@@ -601,9 +613,10 @@ class DirMonitor(_PluginBase):
|
||||
'model': 'monitor_dirs',
|
||||
'label': '监控目录',
|
||||
'rows': 5,
|
||||
'placeholder': '每一行一个目录,支持两种配置方式:\n'
|
||||
'placeholder': '每一行一个目录,支持三种配置方式:\n'
|
||||
'监控目录\n'
|
||||
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)'
|
||||
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)\n'
|
||||
'监控目录:转移目的目录#转移方式(move|copy|link|softlink)'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
292
app/plugins/invitessignin/__init__.py
Normal file
292
app/plugins/invitessignin/__init__.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.core.config import settings
|
||||
from app.plugins import _PluginBase
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
from app.log import logger
|
||||
from app.schemas import NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class InvitesSignin(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "药丸签到"
|
||||
# 插件描述
|
||||
plugin_desc = "药丸论坛签到。"
|
||||
# 插件图标
|
||||
plugin_icon = "invites.png"
|
||||
# 主题色
|
||||
plugin_color = "#4FB647"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/thsrite"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "invitessignin"
|
||||
# 加载顺序
|
||||
plugin_order = 24
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
# 任务执行间隔
|
||||
_cron = None
|
||||
_cookie = None
|
||||
_onlyonce = False
|
||||
_notify = False
|
||||
|
||||
# 定时器
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._cookie = config.get("cookie")
|
||||
self._notify = config.get("notify")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
|
||||
# 加载模块
|
||||
if self._enabled:
|
||||
# 定时服务
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.__signin,
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="药丸签到")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
|
||||
if self._onlyonce:
|
||||
logger.info(f"药丸签到服务启动,立即运行一次")
|
||||
self._scheduler.add_job(func=self.__signin, trigger='date',
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
|
||||
name="药丸签到")
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
self.update_config({
|
||||
"onlyonce": False,
|
||||
"cron": self._cron,
|
||||
"enabled": self._enabled,
|
||||
"cookie": self._cookie,
|
||||
"notify": self._notify,
|
||||
})
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def __signin(self):
|
||||
"""
|
||||
药丸签到
|
||||
"""
|
||||
res = RequestUtils(cookies=self._cookie).get_res(url="https://invites.fun")
|
||||
if not res or res.status_code != 200:
|
||||
logger.error("请求药丸错误")
|
||||
return
|
||||
|
||||
# 获取csrfToken
|
||||
pattern = r'"csrfToken":"(.*?)"'
|
||||
csrfToken = re.findall(pattern, res.text)
|
||||
if not csrfToken:
|
||||
logger.error("请求csrfToken失败")
|
||||
return
|
||||
|
||||
csrfToken = csrfToken[0]
|
||||
logger.info(f"获取csrfToken成功 {csrfToken}")
|
||||
|
||||
# 获取userid
|
||||
pattern = r'"userId":(\d+)'
|
||||
match = re.search(pattern, res.text)
|
||||
|
||||
if match:
|
||||
userId = match.group(1)
|
||||
logger.info(f"获取userid成功 {userId}")
|
||||
else:
|
||||
logger.error("未找到userId")
|
||||
return
|
||||
|
||||
headers = {
|
||||
"X-Csrf-Token": csrfToken,
|
||||
"X-Http-Method-Override": "PATCH",
|
||||
"Cookie": self._cookie
|
||||
}
|
||||
|
||||
data = {
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"canCheckin": False,
|
||||
"totalContinuousCheckIn": 2
|
||||
},
|
||||
"id": userId
|
||||
}
|
||||
}
|
||||
|
||||
# 开始签到
|
||||
res = RequestUtils(headers=headers).post_res(url=f"https://invites.fun/api/users/{userId}", json=data)
|
||||
|
||||
if not res or res.status_code != 200:
|
||||
logger.error("药丸签到失败")
|
||||
return
|
||||
|
||||
sign_dict = json.loads(res.text)
|
||||
money = sign_dict['data']['attributes']['money']
|
||||
totalContinuousCheckIn = sign_dict['data']['attributes']['totalContinuousCheckIn']
|
||||
|
||||
# 发送通知
|
||||
if self._notify:
|
||||
self.post_message(
|
||||
mtype=NotificationType.SiteMessage,
|
||||
title="【药丸签到任务完成】",
|
||||
text=f"累计签到 {totalContinuousCheckIn} \n"
|
||||
f"剩余药丸 {money}")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '开启通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '签到周期'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cookie',
|
||||
'label': '药丸cookie'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"onlyonce": False,
|
||||
"notify": False,
|
||||
"cookie": "",
|
||||
"cron": "0 9 * * *"
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
@@ -109,7 +109,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
self._nolabels = config.get("nolabels")
|
||||
self._nopaths = config.get("nopaths")
|
||||
self._clearcache = config.get("clearcache")
|
||||
self._permanent_error_caches = config.get("permanent_error_caches") or []
|
||||
self._permanent_error_caches = [] if self._clearcache else config.get("permanent_error_caches") or []
|
||||
self._error_caches = [] if self._clearcache else config.get("error_caches") or []
|
||||
self._success_caches = [] if self._clearcache else config.get("success_caches") or []
|
||||
|
||||
|
||||
@@ -741,7 +741,11 @@ class MediaSyncDel(_PluginBase):
|
||||
return
|
||||
|
||||
# 遍历删除
|
||||
last_del_time = None
|
||||
for del_media in del_medias:
|
||||
# 删除时间
|
||||
del_time = del_media.get("time")
|
||||
last_del_time = del_time
|
||||
# 媒体类型 Movie|Series|Season|Episode
|
||||
media_type = del_media.get("type")
|
||||
# 媒体名称 蜀山战纪
|
||||
@@ -881,7 +885,7 @@ class MediaSyncDel(_PluginBase):
|
||||
# 保存历史
|
||||
self.save_data("history", history)
|
||||
|
||||
self.save_data("last_time", datetime.datetime.now())
|
||||
self.save_data("last_time", last_del_time or datetime.datetime.now())
|
||||
|
||||
def handle_torrent(self, src: str, torrent_hash: str):
|
||||
"""
|
||||
@@ -1047,146 +1051,207 @@ class MediaSyncDel(_PluginBase):
|
||||
|
||||
@staticmethod
|
||||
def parse_emby_log(last_time):
|
||||
log_url = "[HOST]System/Logs/embyserver.txt?api_key=[APIKEY]"
|
||||
log_res = Emby().get_data(log_url)
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取emby日志失败,请检查服务器配置")
|
||||
return []
|
||||
"""
|
||||
获取emby日志列表、解析emby日志
|
||||
"""
|
||||
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}) Info App: Removing item from database, Type: (\w+), Name: (.*), Path: (.*), Id: (\d+)'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
def __parse_log(file_name: str, del_list: list):
|
||||
"""
|
||||
解析emby日志
|
||||
"""
|
||||
log_url = f"[HOST]System/Logs/{file_name}?api_key=[APIKEY]"
|
||||
log_res = Emby().get_data(log_url)
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取emby日志失败,请检查服务器配置")
|
||||
return del_list
|
||||
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}) Info App: Removing item from database, Type: (\w+), Name: (.*), Path: (.*), Id: (\d+)'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
mtime = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if last_time and mtime < last_time:
|
||||
continue
|
||||
|
||||
mtype = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if mtype == 'Episode' or mtype == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": mtime,
|
||||
"type": mtype,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_list.append(media)
|
||||
|
||||
return del_list
|
||||
|
||||
log_files = []
|
||||
try:
|
||||
# 获取所有emby日志
|
||||
log_list_url = "[HOST]System/Logs/Query?Limit=3&api_key=[APIKEY]"
|
||||
log_list_res = Emby().get_data(log_list_url)
|
||||
|
||||
if log_list_res and log_list_res.status_code == 200:
|
||||
log_files_dict = json.loads(log_list_res.text)
|
||||
for item in log_files_dict.get("Items"):
|
||||
if str(item.get('Name')).startswith("embyserver"):
|
||||
log_files.append(str(item.get('Name')))
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
if not log_files:
|
||||
log_files.append("embyserver.txt")
|
||||
|
||||
del_medias = []
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
mtime = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if last_time and mtime < last_time:
|
||||
continue
|
||||
|
||||
mtype = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if mtype == 'Episode' or mtype == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": mtime,
|
||||
"type": mtype,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_medias.append(media)
|
||||
log_files.reverse()
|
||||
for log_file in log_files:
|
||||
del_medias = __parse_log(log_file, del_medias)
|
||||
|
||||
return del_medias
|
||||
|
||||
@staticmethod
|
||||
def parse_jellyfin_log(last_time: datetime):
|
||||
# 根据加入日期 降序排序
|
||||
log_url = "[HOST]System/Logs/Log?name=log_%s.log&api_key=[APIKEY]" % datetime.date.today().strftime("%Y%m%d")
|
||||
log_res = Jellyfin().get_data(log_url)
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取jellyfin日志失败,请检查服务器配置")
|
||||
return []
|
||||
"""
|
||||
获取jellyfin日志列表、解析jellyfin日志
|
||||
"""
|
||||
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'\[(.*?)\].*?Removing item, Type: "(.*?)", Name: "(.*?)", Path: "(.*?)"'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
def __parse_log(file_name: str, del_list: list):
|
||||
"""
|
||||
解析jellyfin日志
|
||||
"""
|
||||
log_url = f"[HOST]System/Logs/Log?name={file_name}&api_key=[APIKEY]"
|
||||
log_res = Jellyfin().get_data(log_url)
|
||||
if not log_res or log_res.status_code != 200:
|
||||
logger.error("获取jellyfin日志失败,请检查服务器配置")
|
||||
return del_list
|
||||
|
||||
# 正则解析删除的媒体信息
|
||||
pattern = r'\[(.*?)\].*?Removing item, Type: "(.*?)", Name: "(.*?)", Path: "(.*?)"'
|
||||
matches = re.findall(pattern, log_res.text)
|
||||
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
mtime = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if last_time and mtime < last_time:
|
||||
continue
|
||||
|
||||
mtype = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if mtype == 'Episode' or mtype == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": mtime,
|
||||
"type": mtype,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_list.append(media)
|
||||
|
||||
return del_list
|
||||
|
||||
log_files = []
|
||||
try:
|
||||
# 获取所有jellyfin日志
|
||||
log_list_url = "[HOST]System/Logs?api_key=[APIKEY]"
|
||||
log_list_res = Jellyfin().get_data(log_list_url)
|
||||
|
||||
if log_list_res and log_list_res.status_code == 200:
|
||||
log_files_dict = json.loads(log_list_res.text)
|
||||
for item in log_files_dict:
|
||||
if str(item.get('Name')).startswith("log_"):
|
||||
log_files.append(str(item.get('Name')))
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
if not log_files:
|
||||
log_files.append("log_%s.log" % datetime.date.today().strftime("%Y%m%d"))
|
||||
|
||||
del_medias = []
|
||||
# 循环获取媒体信息
|
||||
for match in matches:
|
||||
mtime = match[0]
|
||||
# 排除已处理的媒体信息
|
||||
if last_time and mtime < last_time:
|
||||
continue
|
||||
|
||||
mtype = match[1]
|
||||
name = match[2]
|
||||
path = match[3]
|
||||
|
||||
year = None
|
||||
year_pattern = r'\(\d+\)'
|
||||
year_match = re.search(year_pattern, path)
|
||||
if year_match:
|
||||
year = year_match.group()[1:-1]
|
||||
|
||||
season = None
|
||||
episode = None
|
||||
if mtype == 'Episode' or mtype == 'Season':
|
||||
name_pattern = r"\/([\u4e00-\u9fa5]+)(?= \()"
|
||||
season_pattern = r"Season\s*(\d+)"
|
||||
episode_pattern = r"S\d+E(\d+)"
|
||||
name_match = re.search(name_pattern, path)
|
||||
season_match = re.search(season_pattern, path)
|
||||
episode_match = re.search(episode_pattern, path)
|
||||
|
||||
if name_match:
|
||||
name = name_match.group(1)
|
||||
|
||||
if season_match:
|
||||
season = season_match.group(1)
|
||||
if int(season) < 10:
|
||||
season = f'S0{season}'
|
||||
else:
|
||||
season = f'S{season}'
|
||||
else:
|
||||
season = None
|
||||
|
||||
if episode_match:
|
||||
episode = episode_match.group(1)
|
||||
episode = f'E{episode}'
|
||||
else:
|
||||
episode = None
|
||||
|
||||
media = {
|
||||
"time": mtime,
|
||||
"type": mtype,
|
||||
"name": name,
|
||||
"year": year,
|
||||
"path": path,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
}
|
||||
logger.debug(f"解析到删除媒体:{json.dumps(media)}")
|
||||
del_medias.append(media)
|
||||
log_files.reverse()
|
||||
for log_file in log_files:
|
||||
del_medias = __parse_log(log_file, del_medias)
|
||||
|
||||
return del_medias
|
||||
|
||||
|
||||
@@ -307,11 +307,57 @@ class PersonMeta(_PluginBase):
|
||||
logger.info(f"媒体库 {library.name} 的演员信息刮削完成")
|
||||
logger.info(f"服务器 {server} 的演员信息刮削完成")
|
||||
|
||||
def __update_peoples(self, server: str, itemid: str, iteminfo: dict, douban_actors):
|
||||
# 处理媒体项中的人物信息
|
||||
"""
|
||||
"People": [
|
||||
{
|
||||
"Name": "丹尼尔·克雷格",
|
||||
"Id": "33625",
|
||||
"Role": "James Bond",
|
||||
"Type": "Actor",
|
||||
"PrimaryImageTag": "bef4f764540f10577f804201d8d27918"
|
||||
}
|
||||
]
|
||||
"""
|
||||
peoples = []
|
||||
# 更新当前媒体项人物
|
||||
for people in iteminfo["People"] or []:
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
if not people.get("Name"):
|
||||
continue
|
||||
if StringUtils.is_chinese(people.get("Name")) \
|
||||
and StringUtils.is_chinese(people.get("Role")):
|
||||
peoples.append(people)
|
||||
continue
|
||||
info = self.__update_people(server=server, people=people,
|
||||
douban_actors=douban_actors)
|
||||
if info:
|
||||
peoples.append(info)
|
||||
elif not self._remove_nozh:
|
||||
peoples.append(people)
|
||||
# 保存媒体项信息
|
||||
if peoples:
|
||||
iteminfo["People"] = peoples
|
||||
self.set_iteminfo(server=server, itemid=itemid, iteminfo=iteminfo)
|
||||
|
||||
def __update_item(self, server: str, item: MediaServerItem,
|
||||
mediainfo: MediaInfo = None, season: int = None):
|
||||
"""
|
||||
更新媒体服务器中的条目
|
||||
"""
|
||||
|
||||
def __need_trans_actor(_item):
|
||||
# 是否需要处理人物信息
|
||||
_peoples = [x for x in _item.get("People", []) if
|
||||
(x.get("Name") and not StringUtils.is_chinese(x.get("Name")))
|
||||
or (x.get("Role") and not StringUtils.is_chinese(x.get("Role")))]
|
||||
if _peoples:
|
||||
return True
|
||||
return False
|
||||
|
||||
# 识别媒体信息
|
||||
if not mediainfo:
|
||||
if not item.tmdbid:
|
||||
@@ -323,49 +369,19 @@ class PersonMeta(_PluginBase):
|
||||
logger.warn(f"{item.title} 未识别到媒体信息")
|
||||
return
|
||||
|
||||
# 获取豆瓣演员信息
|
||||
douban_actors = self.__get_douban_actors(mediainfo=mediainfo, season=season)
|
||||
|
||||
# 获取媒体项
|
||||
iteminfo = self.get_iteminfo(server=server, itemid=item.item_id)
|
||||
if not iteminfo:
|
||||
logger.warn(f"{item.title} 未找到媒体项")
|
||||
return
|
||||
|
||||
# 处理媒体项中的人物信息
|
||||
if iteminfo.get("People"):
|
||||
"""
|
||||
"People": [
|
||||
{
|
||||
"Name": "丹尼尔·克雷格",
|
||||
"Id": "33625",
|
||||
"Role": "James Bond",
|
||||
"Type": "Actor",
|
||||
"PrimaryImageTag": "bef4f764540f10577f804201d8d27918"
|
||||
}
|
||||
]
|
||||
"""
|
||||
peoples = []
|
||||
# 更新当前媒体项人物
|
||||
for people in iteminfo["People"]:
|
||||
if not people.get("Name"):
|
||||
continue
|
||||
if StringUtils.is_chinese(people.get("Name")):
|
||||
peoples.append(people)
|
||||
continue
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
info = self.__update_people(server=server, people=people,
|
||||
douban_actors=douban_actors)
|
||||
if info:
|
||||
peoples.append(info)
|
||||
elif not self._remove_nozh:
|
||||
peoples.append(people)
|
||||
# 保存媒体项信息
|
||||
if peoples:
|
||||
iteminfo["People"] = peoples
|
||||
self.set_iteminfo(server=server, itemid=item.item_id, iteminfo=iteminfo)
|
||||
if __need_trans_actor(iteminfo):
|
||||
# 获取豆瓣演员信息
|
||||
logger.info(f"开始获取 {item.title} 的豆瓣演员信息 ...")
|
||||
douban_actors = self.__get_douban_actors(mediainfo=mediainfo, season=season)
|
||||
self.__update_peoples(server=server, itemid=item.item_id, iteminfo=iteminfo, douban_actors=douban_actors)
|
||||
else:
|
||||
logger.info(f"{item.title} 的人物信息已是中文,无需更新")
|
||||
|
||||
# 处理季和集人物
|
||||
if iteminfo.get("Type") and "Series" in iteminfo["Type"]:
|
||||
@@ -383,31 +399,14 @@ class PersonMeta(_PluginBase):
|
||||
if not seasoninfo:
|
||||
logger.warn(f"{item.title} 未找到季媒体项:{season.get('Id')}")
|
||||
continue
|
||||
# 更新季媒体项人物
|
||||
peoples = []
|
||||
if seasoninfo.get("People"):
|
||||
logger.info(f"开始更新季 {seasoninfo.get('Id')} 的人物信息 ...")
|
||||
for people in seasoninfo["People"]:
|
||||
if not people.get("Name"):
|
||||
continue
|
||||
if StringUtils.is_chinese(people.get("Name")):
|
||||
peoples.append(people)
|
||||
continue
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
# 更新人物信息
|
||||
info = self.__update_people(server=server, people=people,
|
||||
douban_actors=season_actors)
|
||||
if info:
|
||||
peoples.append(info)
|
||||
elif not self._remove_nozh:
|
||||
peoples.append(people)
|
||||
# 保存季媒体项信息
|
||||
if peoples:
|
||||
seasoninfo["People"] = peoples
|
||||
self.set_iteminfo(server=server, itemid=season.get("Id"), iteminfo=seasoninfo)
|
||||
|
||||
if __need_trans_actor(seasoninfo):
|
||||
# 更新季媒体项人物
|
||||
self.__update_peoples(server=server, itemid=season.get("Id"), iteminfo=seasoninfo,
|
||||
douban_actors=season_actors)
|
||||
logger.info(f"季 {seasoninfo.get('Id')} 的人物信息更新完成")
|
||||
else:
|
||||
logger.info(f"季 {seasoninfo.get('Id')} 的人物信息已是中文,无需更新")
|
||||
# 获取集媒体项
|
||||
episodes = self.get_items(server=server, parentid=season.get("Id"), mtype="Episode")
|
||||
if not episodes:
|
||||
@@ -420,31 +419,13 @@ class PersonMeta(_PluginBase):
|
||||
if not episodeinfo:
|
||||
logger.warn(f"{item.title} 未找到集媒体项:{episode.get('Id')}")
|
||||
continue
|
||||
# 更新集媒体项人物
|
||||
if episodeinfo.get("People"):
|
||||
logger.info(f"开始更新集 {episodeinfo.get('Id')} 的人物信息 ...")
|
||||
peoples = []
|
||||
for people in episodeinfo["People"]:
|
||||
if not people.get("Name"):
|
||||
continue
|
||||
if StringUtils.is_chinese(people.get("Name")):
|
||||
peoples.append(people)
|
||||
continue
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
# 更新人物信息
|
||||
info = self.__update_people(server=server, people=people,
|
||||
douban_actors=season_actors)
|
||||
if info:
|
||||
peoples.append(info)
|
||||
elif not self._remove_nozh:
|
||||
peoples.append(people)
|
||||
# 保存集媒体项信息
|
||||
if peoples:
|
||||
episodeinfo["People"] = peoples
|
||||
self.set_iteminfo(server=server, itemid=episode.get("Id"), iteminfo=episodeinfo)
|
||||
if __need_trans_actor(episodeinfo):
|
||||
# 更新集媒体项人物
|
||||
self.__update_peoples(server=server, itemid=episode.get("Id"), iteminfo=episodeinfo,
|
||||
douban_actors=season_actors)
|
||||
logger.info(f"集 {episodeinfo.get('Id')} 的人物信息更新完成")
|
||||
else:
|
||||
logger.info(f"集 {episodeinfo.get('Id')} 的人物信息已是中文,无需更新")
|
||||
|
||||
def __update_people(self, server: str, people: dict, douban_actors: list = None) -> Optional[dict]:
|
||||
"""
|
||||
@@ -506,15 +487,39 @@ class PersonMeta(_PluginBase):
|
||||
profile_path = person_tmdbinfo.get('profile_path')
|
||||
if profile_path:
|
||||
logger.info(f"{people.get('Name')} 从TMDB获取到图片:{profile_path}")
|
||||
profile_path = f"https://image.tmdb.org/t/p/original{profile_path}"
|
||||
profile_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{profile_path}"
|
||||
|
||||
# 从豆瓣信息中更新人物信息
|
||||
"""
|
||||
{
|
||||
"name": "丹尼尔·克雷格",
|
||||
"roles": [
|
||||
"演员",
|
||||
"制片人",
|
||||
"配音"
|
||||
],
|
||||
"title": "丹尼尔·克雷格(同名)英国,英格兰,柴郡,切斯特影视演员",
|
||||
"url": "https://movie.douban.com/celebrity/1025175/",
|
||||
"user": null,
|
||||
"character": "饰 詹姆斯·邦德 James Bond 007",
|
||||
"uri": "douban://douban.com/celebrity/1025175?subject_id=27230907",
|
||||
"avatar": {
|
||||
"large": "https://qnmob3.doubanio.com/view/celebrity/raw/public/p42588.jpg?imageView2/2/q/80/w/600/h/3000/format/webp",
|
||||
"normal": "https://qnmob3.doubanio.com/view/celebrity/raw/public/p42588.jpg?imageView2/2/q/80/w/200/h/300/format/webp"
|
||||
},
|
||||
"sharing_url": "https://www.douban.com/doubanapp/dispatch?uri=/celebrity/1025175/",
|
||||
"type": "celebrity",
|
||||
"id": "1025175",
|
||||
"latin_name": "Daniel Craig"
|
||||
}
|
||||
"""
|
||||
if douban_actors and (not updated_name
|
||||
or not updated_overview
|
||||
or not update_character):
|
||||
# 从豆瓣演员中匹配中文名称、角色和简介
|
||||
for douban_actor in douban_actors:
|
||||
if douban_actor.get("latin_name") == people.get("Name"):
|
||||
if douban_actor.get("latin_name") == people.get("Name") \
|
||||
or douban_actor.get("name") == people.get("Name"):
|
||||
# 名称
|
||||
if not updated_name:
|
||||
logger.info(f"{people.get('Name')} 从豆瓣中获取到中文名:{douban_actor.get('name')}")
|
||||
@@ -587,6 +592,8 @@ class PersonMeta(_PluginBase):
|
||||
if doubaninfo:
|
||||
doubanitem = self.chain.douban_info(doubaninfo.get("id")) or {}
|
||||
return (doubanitem.get("actors") or []) + (doubanitem.get("directors") or [])
|
||||
else:
|
||||
logger.warn(f"未找到豆瓣信息:{mediainfo.title_year}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -841,87 +841,88 @@ class SiteStatistic(_PluginBase):
|
||||
url = site_info.get("url")
|
||||
proxy = site_info.get("proxy")
|
||||
ua = site_info.get("ua")
|
||||
session = requests.Session()
|
||||
proxies = settings.PROXY if proxy else None
|
||||
proxy_server = settings.PROXY_SERVER if proxy else None
|
||||
render = site_info.get("render")
|
||||
# 会话管理
|
||||
with requests.Session() as session:
|
||||
proxies = settings.PROXY if proxy else None
|
||||
proxy_server = settings.PROXY_SERVER if proxy else None
|
||||
render = site_info.get("render")
|
||||
|
||||
logger.debug(f"站点 {site_name} url={url} site_cookie={site_cookie} ua={ua}")
|
||||
if render:
|
||||
# 演染模式
|
||||
html_text = PlaywrightHelper().get_page_source(url=url,
|
||||
cookies=site_cookie,
|
||||
ua=ua,
|
||||
proxies=proxy_server)
|
||||
else:
|
||||
# 普通模式
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=url)
|
||||
if res and res.status_code == 200:
|
||||
if re.search(r"charset=\"?utf-8\"?", res.text, re.IGNORECASE):
|
||||
res.encoding = "utf-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
# 第一次登录反爬
|
||||
if html_text.find("title") == -1:
|
||||
i = html_text.find("window.location")
|
||||
if i == -1:
|
||||
return None
|
||||
tmp_url = url + html_text[i:html_text.find(";")] \
|
||||
.replace("\"", "") \
|
||||
.replace("+", "") \
|
||||
.replace(" ", "") \
|
||||
.replace("window.location=", "")
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=tmp_url)
|
||||
if res and res.status_code == 200:
|
||||
if "charset=utf-8" in res.text or "charset=UTF-8" in res.text:
|
||||
res.encoding = "UTF-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return None
|
||||
else:
|
||||
logger.error("站点 %s 被反爬限制:%s, 状态码:%s" % (site_name, url, res.status_code))
|
||||
return None
|
||||
|
||||
# 兼容假首页情况,假首页通常没有 <link rel="search" 属性
|
||||
if '"search"' not in html_text and '"csrf-token"' not in html_text:
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=url + "/index.php")
|
||||
if res and res.status_code == 200:
|
||||
if re.search(r"charset=\"?utf-8\"?", res.text, re.IGNORECASE):
|
||||
res.encoding = "utf-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return None
|
||||
elif res is not None:
|
||||
logger.error(f"站点 {site_name} 连接失败,状态码:{res.status_code}")
|
||||
return None
|
||||
logger.debug(f"站点 {site_name} url={url} site_cookie={site_cookie} ua={ua}")
|
||||
if render:
|
||||
# 演染模式
|
||||
html_text = PlaywrightHelper().get_page_source(url=url,
|
||||
cookies=site_cookie,
|
||||
ua=ua,
|
||||
proxies=proxy_server)
|
||||
else:
|
||||
logger.error(f"站点 {site_name} 无法访问:{url}")
|
||||
return None
|
||||
# 解析站点类型
|
||||
if html_text:
|
||||
site_schema = self.__build_class(html_text)
|
||||
if not site_schema:
|
||||
logger.error("站点 %s 无法识别站点类型" % site_name)
|
||||
return None
|
||||
return site_schema(site_name, url, site_cookie, html_text, session=session, ua=ua, proxy=proxy)
|
||||
return None
|
||||
# 普通模式
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=url)
|
||||
if res and res.status_code == 200:
|
||||
if re.search(r"charset=\"?utf-8\"?", res.text, re.IGNORECASE):
|
||||
res.encoding = "utf-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
# 第一次登录反爬
|
||||
if html_text.find("title") == -1:
|
||||
i = html_text.find("window.location")
|
||||
if i == -1:
|
||||
return None
|
||||
tmp_url = url + html_text[i:html_text.find(";")] \
|
||||
.replace("\"", "") \
|
||||
.replace("+", "") \
|
||||
.replace(" ", "") \
|
||||
.replace("window.location=", "")
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=tmp_url)
|
||||
if res and res.status_code == 200:
|
||||
if "charset=utf-8" in res.text or "charset=UTF-8" in res.text:
|
||||
res.encoding = "UTF-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return None
|
||||
else:
|
||||
logger.error("站点 %s 被反爬限制:%s, 状态码:%s" % (site_name, url, res.status_code))
|
||||
return None
|
||||
|
||||
# 兼容假首页情况,假首页通常没有 <link rel="search" 属性
|
||||
if '"search"' not in html_text and '"csrf-token"' not in html_text:
|
||||
res = RequestUtils(cookies=site_cookie,
|
||||
session=session,
|
||||
ua=ua,
|
||||
proxies=proxies
|
||||
).get_res(url=url + "/index.php")
|
||||
if res and res.status_code == 200:
|
||||
if re.search(r"charset=\"?utf-8\"?", res.text, re.IGNORECASE):
|
||||
res.encoding = "utf-8"
|
||||
else:
|
||||
res.encoding = res.apparent_encoding
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return None
|
||||
elif res is not None:
|
||||
logger.error(f"站点 {site_name} 连接失败,状态码:{res.status_code}")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"站点 {site_name} 无法访问:{url}")
|
||||
return None
|
||||
# 解析站点类型
|
||||
if html_text:
|
||||
site_schema = self.__build_class(html_text)
|
||||
if not site_schema:
|
||||
logger.error("站点 %s 无法识别站点类型" % site_name)
|
||||
return None
|
||||
return site_schema(site_name, url, site_cookie, html_text, session=session, ua=ua, proxy=proxy)
|
||||
return None
|
||||
|
||||
def refresh_by_domain(self, domain: str) -> schemas.Response:
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,6 @@ from enum import Enum
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
|
||||
import requests
|
||||
from requests import Session
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -107,7 +106,7 @@ class ISiteUserInfo(metaclass=ABCMeta):
|
||||
self._base_url = f"{split_url.scheme}://{split_url.netloc}"
|
||||
self._site_cookie = site_cookie
|
||||
self._index_html = index_html
|
||||
self._session = session if session else requests.Session()
|
||||
self._session = session if session else None
|
||||
self._ua = ua
|
||||
|
||||
self._emulate = emulate
|
||||
|
||||
@@ -100,7 +100,7 @@ class TorrentTransfer(_PluginBase):
|
||||
return
|
||||
if self._fromdownloader == self._todownloader:
|
||||
logger.error(f"源下载器和目的下载器不能相同")
|
||||
self.systemmessage(f"源下载器和目的下载器不能相同")
|
||||
self.systemmessage.put(f"源下载器和目的下载器不能相同")
|
||||
return
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
@@ -110,7 +110,7 @@ class TorrentTransfer(_PluginBase):
|
||||
CronTrigger.from_crontab(self._cron))
|
||||
except Exception as e:
|
||||
logger.error(f"转移做种服务启动失败:{e}")
|
||||
self.systemmessage(f"转移做种服务启动失败:{e}")
|
||||
self.systemmessage.put(f"转移做种服务启动失败:{e}")
|
||||
return
|
||||
if self._onlyonce:
|
||||
logger.info(f"转移做种服务启动,立即运行一次")
|
||||
|
||||
@@ -152,12 +152,12 @@ class WebHook(_PluginBase):
|
||||
return tuple(__to_dict(list(_event)))
|
||||
elif isinstance(_event, set):
|
||||
return set(__to_dict(list(_event)))
|
||||
elif isinstance(_event, frozenset):
|
||||
return frozenset(__to_dict(list(_event)))
|
||||
elif hasattr(_event, 'to_dict'):
|
||||
return __to_dict(_event.to_dict())
|
||||
elif hasattr(_event, '__dict__'):
|
||||
return __to_dict(_event.__dict__)
|
||||
elif isinstance(_event, (int, float, str, bool, type(None))):
|
||||
return _event
|
||||
elif hasattr(_event, '__dict__'):
|
||||
return _event.__dict__
|
||||
else:
|
||||
return str(_event)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.chain import ChainBase
|
||||
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.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.db import SessionFactory
|
||||
@@ -183,6 +184,14 @@ class Scheduler(metaclass=Singleton):
|
||||
}
|
||||
)
|
||||
|
||||
# 后台刷新TMDB壁纸
|
||||
self._scheduler.add_job(
|
||||
TmdbChain(self._db).get_random_wallpager,
|
||||
"interval",
|
||||
minutes=30,
|
||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(seconds=3)
|
||||
)
|
||||
|
||||
# 公共定时服务
|
||||
self._scheduler.add_job(
|
||||
SchedulerChain(self._db).scheduler_job,
|
||||
|
||||
@@ -58,6 +58,8 @@ class SystemConfigKey(Enum):
|
||||
NotificationChannels = "NotificationChannels"
|
||||
# 自定义制作组/字幕组
|
||||
CustomReleaseGroups = "CustomReleaseGroups"
|
||||
# 自定义占位符
|
||||
Customization = "Customization"
|
||||
# 自定义识别词
|
||||
CustomIdentifiers = "CustomIdentifiers"
|
||||
# 搜索优先级规则
|
||||
|
||||
@@ -59,7 +59,8 @@ class RequestUtils:
|
||||
headers=self._headers,
|
||||
proxies=self._proxies,
|
||||
timeout=self._timeout,
|
||||
json=json)
|
||||
json=json,
|
||||
stream=False)
|
||||
else:
|
||||
return requests.post(url,
|
||||
data=data,
|
||||
@@ -67,7 +68,8 @@ class RequestUtils:
|
||||
headers=self._headers,
|
||||
proxies=self._proxies,
|
||||
timeout=self._timeout,
|
||||
json=json)
|
||||
json=json,
|
||||
stream=False)
|
||||
except requests.exceptions.RequestException:
|
||||
return None
|
||||
|
||||
@@ -91,27 +93,38 @@ class RequestUtils:
|
||||
except requests.exceptions.RequestException:
|
||||
return None
|
||||
|
||||
def get_res(self, url: str, params: dict = None,
|
||||
allow_redirects: bool = True, raise_exception: bool = False) -> Optional[Response]:
|
||||
def get_res(self, url: str,
|
||||
params: dict = None,
|
||||
data: Any = None,
|
||||
json: dict = None,
|
||||
allow_redirects: bool = True,
|
||||
raise_exception: bool = False
|
||||
) -> Optional[Response]:
|
||||
try:
|
||||
if self._session:
|
||||
return self._session.get(url,
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
verify=False,
|
||||
headers=self._headers,
|
||||
proxies=self._proxies,
|
||||
cookies=self._cookies,
|
||||
timeout=self._timeout,
|
||||
allow_redirects=allow_redirects)
|
||||
allow_redirects=allow_redirects,
|
||||
stream=False)
|
||||
else:
|
||||
return requests.get(url,
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
verify=False,
|
||||
headers=self._headers,
|
||||
proxies=self._proxies,
|
||||
cookies=self._cookies,
|
||||
timeout=self._timeout,
|
||||
allow_redirects=allow_redirects)
|
||||
allow_redirects=allow_redirects,
|
||||
stream=False)
|
||||
except requests.exceptions.RequestException:
|
||||
if raise_exception:
|
||||
raise requests.exceptions.RequestException
|
||||
@@ -120,7 +133,8 @@ class RequestUtils:
|
||||
def post_res(self, url: str, data: Any = None, params: dict = None,
|
||||
allow_redirects: bool = True,
|
||||
files: Any = None,
|
||||
json: dict = None) -> Optional[Response]:
|
||||
json: dict = None,
|
||||
raise_exception: bool = False) -> Optional[Response]:
|
||||
try:
|
||||
if self._session:
|
||||
return self._session.post(url,
|
||||
@@ -133,7 +147,8 @@ class RequestUtils:
|
||||
timeout=self._timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
files=files,
|
||||
json=json)
|
||||
json=json,
|
||||
stream=False)
|
||||
else:
|
||||
return requests.post(url,
|
||||
data=data,
|
||||
@@ -145,8 +160,11 @@ class RequestUtils:
|
||||
timeout=self._timeout,
|
||||
allow_redirects=allow_redirects,
|
||||
files=files,
|
||||
json=json)
|
||||
json=json,
|
||||
stream=False)
|
||||
except requests.exceptions.RequestException:
|
||||
if raise_exception:
|
||||
raise requests.exceptions.RequestException
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -68,6 +68,8 @@ class StringUtils:
|
||||
"""
|
||||
判断是否含有中文
|
||||
"""
|
||||
if not word:
|
||||
return False
|
||||
if isinstance(word, list):
|
||||
word = " ".join(word)
|
||||
chn = re.compile(r'[\u4e00-\u9fff]')
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Union, Tuple
|
||||
|
||||
@@ -27,20 +28,39 @@ class SystemUtils:
|
||||
|
||||
@staticmethod
|
||||
def is_docker() -> bool:
|
||||
"""
|
||||
判断是否为Docker环境
|
||||
"""
|
||||
return Path("/.dockerenv").exists()
|
||||
|
||||
@staticmethod
|
||||
def is_synology() -> bool:
|
||||
"""
|
||||
判断是否为群晖系统
|
||||
"""
|
||||
if SystemUtils.is_windows():
|
||||
return False
|
||||
return True if "synology" in SystemUtils.execute('uname -a') else False
|
||||
|
||||
@staticmethod
|
||||
def is_windows() -> bool:
|
||||
"""
|
||||
判断是否为Windows系统
|
||||
"""
|
||||
return True if os.name == "nt" else False
|
||||
|
||||
@staticmethod
|
||||
def is_frozen() -> bool:
|
||||
"""
|
||||
判断是否为冻结的二进制文件
|
||||
"""
|
||||
return True if getattr(sys, 'frozen', False) else False
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""
|
||||
判断是否为MacOS系统
|
||||
"""
|
||||
return True if platform.system() == 'Darwin' else False
|
||||
|
||||
@staticmethod
|
||||
|
||||
187
config/app.env
Normal file
187
config/app.env
Normal file
@@ -0,0 +1,187 @@
|
||||
#######################################################################
|
||||
# 【*】为必配项,其余为选配项,选配项可以删除整项配置项或者保留配置默认值 #
|
||||
#######################################################################
|
||||
|
||||
####################################
|
||||
# 基础设置 #
|
||||
####################################
|
||||
# 时区
|
||||
TZ=Asia/Shanghai
|
||||
# 【*】API监听地址
|
||||
HOST=0.0.0.0
|
||||
# 是否调试模式
|
||||
DEBUG=false
|
||||
# 是否开发模式
|
||||
DEV=false
|
||||
# 【*】超级管理员
|
||||
SUPERUSER=admin
|
||||
# 【*】超级管理员初始密码
|
||||
SUPERUSER_PASSWORD=password
|
||||
# 【*】API密钥,建议更换复杂字符串
|
||||
API_TOKEN=moviepilot
|
||||
# 网络代理 IP:PORT
|
||||
PROXY_HOST=
|
||||
# TMDB图片地址,无需修改需保留默认值
|
||||
TMDB_IMAGE_DOMAIN=image.tmdb.org
|
||||
# TMDB API地址,无需修改需保留默认值
|
||||
TMDB_API_DOMAIN=api.themoviedb.org
|
||||
# 大内存模式
|
||||
BIG_MEMORY_MODE=false
|
||||
|
||||
####################################
|
||||
# 媒体识别&刮削 #
|
||||
####################################
|
||||
# 媒体信息搜索来源 themoviedb/douban
|
||||
SEARCH_SOURCE=themoviedb
|
||||
# 刮削入库的媒体文件 true/false
|
||||
SCRAP_METADATA=true
|
||||
# 新增已入库媒体是否跟随TMDB信息变化
|
||||
SCRAP_FOLLOW_TMDB=true
|
||||
# 刮削来源 themoviedb/douban
|
||||
SCRAP_SOURCE=themoviedb
|
||||
|
||||
####################################
|
||||
# 媒体库 #
|
||||
####################################
|
||||
# 【*】转移方式 link/copy/move/softlink
|
||||
TRANSFER_TYPE=copy
|
||||
# 【*】媒体库目录,多个目录使用,分隔
|
||||
LIBRARY_PATH=
|
||||
# 电影媒体库目录名,默认电影
|
||||
LIBRARY_MOVIE_NAME=
|
||||
# 电视剧媒体库目录名,默认电视剧
|
||||
LIBRARY_TV_NAME=
|
||||
# 动漫媒体库目录名,默认电视剧/动漫
|
||||
LIBRARY_ANIME_NAME=
|
||||
# 二级分类
|
||||
LIBRARY_CATEGORY=true
|
||||
# 电影重命名格式
|
||||
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={{title}}{% if year %} ({{year}}){% endif %}/Season {{season}}/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}{{fileExt}}
|
||||
|
||||
####################################
|
||||
# 站点 #
|
||||
####################################
|
||||
# 【*】CookieCloud服务器地址,默认为公共服务器
|
||||
COOKIECLOUD_HOST=https://movie-pilot.org/cookiecloud
|
||||
# 【*】CookieCloud用户KEY
|
||||
COOKIECLOUD_KEY=
|
||||
# 【*】CookieCloud端对端加密密码
|
||||
COOKIECLOUD_PASSWORD=
|
||||
# 【*】CookieCloud同步间隔(分钟)
|
||||
COOKIECLOUD_INTERVAL=1440
|
||||
# OCR服务器地址
|
||||
OCR_HOST=https://movie-pilot.org
|
||||
# 【*】CookieCloud对应的浏览器UA
|
||||
USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57
|
||||
|
||||
####################################
|
||||
# 订阅 & 搜索 #
|
||||
####################################
|
||||
# 订阅模式 spider/rss
|
||||
SUBSCRIBE_MODE=spider
|
||||
# RSS订阅模式刷新时间间隔(分钟)
|
||||
SUBSCRIBE_RSS_INTERVAL=30
|
||||
# 订阅搜索开关
|
||||
SUBSCRIBE_SEARCH=false
|
||||
# 交互搜索自动下载用户ID,使用,分割
|
||||
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
|
||||
# Transmission地址,IP:PORT
|
||||
TR_HOST=
|
||||
# Transmission用户名
|
||||
TR_USER=
|
||||
# Transmission密码
|
||||
TR_PASSWORD=
|
||||
# 种子标签
|
||||
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
|
||||
# 入库刷新媒体库
|
||||
REFRESH_MEDIASERVER=true
|
||||
# 媒体服务器同步间隔(小时)
|
||||
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=
|
||||
@@ -12,7 +12,7 @@ for module in Path(__file__).with_name("models").glob("*.py"):
|
||||
|
||||
db_version = input("请输入版本号:")
|
||||
db_location = settings.CONFIG_PATH / 'user.db'
|
||||
script_location = settings.ROOT_PATH / 'alembic'
|
||||
script_location = settings.ROOT_PATH / 'database'
|
||||
alembic_cfg = AlembicConfig()
|
||||
alembic_cfg.set_main_option('script_location', str(script_location))
|
||||
alembic_cfg.set_main_option('sqlalchemy.url', f"sqlite:///{db_location}")
|
||||
@@ -42,6 +42,7 @@ chardet~=4.0.0
|
||||
starlette~=0.27.0
|
||||
PyVirtualDisplay~=3.0
|
||||
psutil~=5.9.4
|
||||
python_dotenv~=1.0.0
|
||||
python_hosts~=1.0.3
|
||||
watchdog~=3.0.0
|
||||
tailer~=0.4.1
|
||||
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.2.8'
|
||||
APP_VERSION = 'v1.2.9'
|
||||
|
||||
97
windows.spec
Normal file
97
windows.spec
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
def collect_pkg_data(package, include_py_files=False, subdir=None):
|
||||
"""
|
||||
Collect all data files from the given package.
|
||||
"""
|
||||
import os
|
||||
from PyInstaller.utils.hooks import get_package_paths, remove_prefix, PY_IGNORE_EXTENSIONS
|
||||
|
||||
# Accept only strings as packages.
|
||||
if type(package) is not str:
|
||||
raise ValueError
|
||||
|
||||
pkg_base, pkg_dir = get_package_paths(package)
|
||||
if subdir:
|
||||
pkg_dir = os.path.join(pkg_dir, subdir)
|
||||
# Walk through all file in the given package, looking for data files.
|
||||
data_toc = TOC()
|
||||
for dir_path, dir_names, files in os.walk(pkg_dir):
|
||||
for f in files:
|
||||
extension = os.path.splitext(f)[1]
|
||||
if include_py_files or (extension not in PY_IGNORE_EXTENSIONS):
|
||||
source_file = os.path.join(dir_path, f)
|
||||
dest_folder = remove_prefix(dir_path, os.path.dirname(pkg_base) + os.sep)
|
||||
dest_file = os.path.join(dest_folder, f)
|
||||
data_toc.append((dest_file, source_file, 'DATA'))
|
||||
return data_toc
|
||||
|
||||
|
||||
def collect_local_submodules(package):
|
||||
"""
|
||||
Collect all local submodules from the given package.
|
||||
"""
|
||||
import os
|
||||
base_dir = '..'
|
||||
package_dir = os.path.join(base_dir, package.replace('.', os.sep))
|
||||
submodules = []
|
||||
for dir_path, dir_names, files in os.walk(package_dir):
|
||||
for f in files:
|
||||
if f == '__init__.py':
|
||||
submodules.append(f"{package}.{os.path.basename(dir_path)}")
|
||||
elif f.endswith('.py'):
|
||||
submodules.append(f"{package}.{os.path.basename(dir_path)}.{os.path.splitext(f)[0]}")
|
||||
for d in dir_names:
|
||||
submodules.append(f"{package}.{os.path.basename(dir_path)}.{d}")
|
||||
return submodules
|
||||
|
||||
|
||||
hiddenimports = [
|
||||
'passlib.handlers.bcrypt',
|
||||
'app.modules',
|
||||
'app.plugins',
|
||||
] + collect_local_submodules('app.modules') \
|
||||
+ collect_local_submodules('app.plugins')
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['app/main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
collect_pkg_data('config'),
|
||||
collect_pkg_data('cf_clearance'),
|
||||
collect_pkg_data('database', include_py_files=True),
|
||||
[],
|
||||
name='MoviePilot',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon="app.ico"
|
||||
)
|
||||
Reference in New Issue
Block a user