mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-13 23:16:46 +00:00
Compare commits
42 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 |
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 \
|
||||
|
||||
119
README.md
119
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) 或执行命令:
|
||||
|
||||
@@ -42,52 +42,55 @@ docker pull jxxghp/moviepilot:latest
|
||||
## 配置
|
||||
|
||||
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
|
||||
1. 在docker环境变量部分进行参数配置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
|
||||
2. 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
|
||||
- 在docker环境变量部分进行参数配置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
|
||||
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
|
||||
|
||||
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,部分参数如路径映射、站点认证等必须通过环境变量进行配置。
|
||||
配置文件映射路径:`/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](https://github.com/jxxghp/MoviePilot/raw/main/config/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:** 动漫媒体库目录名,默认`电视剧/动漫`
|
||||
---
|
||||
- **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) 自动在媒体库目录下建立二级目录分类
|
||||
- **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,使用,分割
|
||||
---
|
||||
- **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`设置项:
|
||||
|
||||
@@ -104,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`设置项:
|
||||
|
||||
@@ -133,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`设置项:
|
||||
|
||||
@@ -160,7 +171,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过docker环境变量配置**)
|
||||
|
||||
- **AUTH_SITE:** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||
- **AUTH_SITE $\color{red}{*}$ :** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||
|
||||
| 站点 | 参数 |
|
||||
|:------------:|:-----------------------------------------------------:|
|
||||
@@ -197,6 +208,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
> `edition`: 版本(资源类型+特效)
|
||||
> `videoFormat`: 分辨率
|
||||
> `releaseGroup`: 制作组/字幕组
|
||||
> `customization`: 自定义占位符
|
||||
> `videoCodec`: 视频编码
|
||||
> `audioCodec`: 音频编码
|
||||
> `tmdbid`: TMDBID
|
||||
@@ -226,9 +238,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
```
|
||||
|
||||
|
||||
### 3. **过滤规则**
|
||||
|
||||
在`设定`-`规则`中设定,规则说明:
|
||||
### 3. **优先级规则**
|
||||
|
||||
- 仅支持使用内置规则进行排列组合,内置规则有:`蓝光原盘`、`4K`、`1080P`、`中文字幕`、`特效字幕`、`H265`、`H264`、`杜比`、`HDR`、`REMUX`、`WEB-DL`、`免费`、`国语配音` 等
|
||||
- 符合任一层级规则的资源将被标识选中,匹配成功的层级做为该资源的优先级,排越前面优先级超高
|
||||
@@ -245,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;
|
||||
@@ -258,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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
# 项目名称
|
||||
@@ -27,7 +31,7 @@ class Settings(BaseSettings):
|
||||
# 是否开发模式
|
||||
DEV: bool = False
|
||||
# 配置文件目录
|
||||
CONFIG_DIR: str = "/config"
|
||||
CONFIG_DIR: str = None
|
||||
# 超级管理员
|
||||
SUPERUSER: str = "admin"
|
||||
# 超级管理员初始密码
|
||||
@@ -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):
|
||||
@@ -273,6 +281,9 @@ class Settings(BaseSettings):
|
||||
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)
|
||||
|
||||
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]:
|
||||
|
||||
@@ -173,7 +173,7 @@ class DoubanApi(metaclass=Singleton):
|
||||
|
||||
ts = params.pop(
|
||||
'_ts',
|
||||
int(datetime.strftime(datetime.now(), '%Y%m%d'))
|
||||
datetime.strftime(datetime.now(), '%Y%m%d')
|
||||
)
|
||||
params.update({
|
||||
'os_rom': 'android',
|
||||
@@ -185,6 +185,8 @@ class DoubanApi(metaclass=Singleton):
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
@@ -509,12 +490,36 @@ class PersonMeta(_PluginBase):
|
||||
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
|
||||
|
||||
@@ -58,6 +58,8 @@ class SystemConfigKey(Enum):
|
||||
NotificationChannels = "NotificationChannels"
|
||||
# 自定义制作组/字幕组
|
||||
CustomReleaseGroups = "CustomReleaseGroups"
|
||||
# 自定义占位符
|
||||
Customization = "Customization"
|
||||
# 自定义识别词
|
||||
CustomIdentifiers = "CustomIdentifiers"
|
||||
# 搜索优先级规则
|
||||
|
||||
@@ -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
|
||||
|
||||
134
config/app.env
134
config/app.env
@@ -1,24 +1,30 @@
|
||||
#######################################################################
|
||||
# 【*】为必配项,其余为选配项,选配项可以删除整项配置项或者保留配置默认值 #
|
||||
#######################################################################
|
||||
|
||||
####################################
|
||||
# 系统设置 #
|
||||
# 基础设置 #
|
||||
####################################
|
||||
# 时区
|
||||
TZ="Asia/Shanghai"
|
||||
# API监听地址
|
||||
HOST="0.0.0.0"
|
||||
# API监听端口
|
||||
PORT=3001
|
||||
TZ=Asia/Shanghai
|
||||
# 【*】API监听地址
|
||||
HOST=0.0.0.0
|
||||
# 是否调试模式
|
||||
DEBUG=false
|
||||
# 是否开发模式
|
||||
DEV=false
|
||||
# 超级管理员
|
||||
SUPERUSER="admin"
|
||||
# 超级管理员初始密码
|
||||
SUPERUSER_PASSWORD="password"
|
||||
# API密钥,建议更换复杂字符串
|
||||
API_TOKEN="moviepilot"
|
||||
# 【*】超级管理员
|
||||
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
|
||||
|
||||
@@ -26,23 +32,55 @@ BIG_MEMORY_MODE=false
|
||||
# 媒体识别&刮削 #
|
||||
####################################
|
||||
# 媒体信息搜索来源 themoviedb/douban
|
||||
SEARCH_SOURCE="themoviedb"
|
||||
SEARCH_SOURCE=themoviedb
|
||||
# 刮削入库的媒体文件 true/false
|
||||
SCRAP_METADATA=true
|
||||
# 新增已入库媒体是否跟随TMDB信息变化
|
||||
SCRAP_FOLLOW_TMDB=true
|
||||
# 刮削来源 themoviedb/douban
|
||||
SCRAP_SOURCE="themoviedb"
|
||||
# TMDB图片地址,无需修改需保留默认值
|
||||
TMDB_IMAGE_DOMAIN="image.tmdb.org"
|
||||
# TMDB API地址,无需修改需保留默认值
|
||||
TMDB_API_DOMAIN="api.themoviedb.org"
|
||||
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"
|
||||
SUBSCRIBE_MODE=spider
|
||||
# RSS订阅模式刷新时间间隔(分钟)
|
||||
SUBSCRIBE_RSS_INTERVAL=30
|
||||
# 订阅搜索开关
|
||||
@@ -53,8 +91,8 @@ AUTO_DOWNLOAD_USER=
|
||||
####################################
|
||||
# 消息通知 #
|
||||
####################################
|
||||
# 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
|
||||
MESSAGER="telegram"
|
||||
# 【*】消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
|
||||
MESSAGER=telegram
|
||||
# WeChat企业ID
|
||||
WECHAT_CORPID=
|
||||
# WeChat应用Secret
|
||||
@@ -62,7 +100,7 @@ WECHAT_APP_SECRET=
|
||||
# WeChat应用ID
|
||||
WECHAT_APP_ID=
|
||||
# WeChat代理服务器,无需代理需保留默认值
|
||||
WECHAT_PROXY="https://qyapi.weixin.qq.com"
|
||||
WECHAT_PROXY=https://qyapi.weixin.qq.com
|
||||
# WeChat Token
|
||||
WECHAT_TOKEN=
|
||||
# WeChat EncodingAESKey
|
||||
@@ -91,8 +129,8 @@ SYNOLOGYCHAT_TOKEN=
|
||||
####################################
|
||||
# 下载 #
|
||||
####################################
|
||||
# 下载器 qbittorrent/transmission
|
||||
DOWNLOADER="qbittorrent"
|
||||
# 【*】下载器 qbittorrent/transmission
|
||||
DOWNLOADER=qbittorrent
|
||||
# 下载器监控开关
|
||||
DOWNLOADER_MONITOR=true
|
||||
# Qbittorrent地址,IP:PORT
|
||||
@@ -110,9 +148,9 @@ TR_USER=
|
||||
# Transmission密码
|
||||
TR_PASSWORD=
|
||||
# 种子标签
|
||||
TORRENT_TAG="MOVIEPILOT"
|
||||
# 下载保存目录,容器内映射路径需要一致
|
||||
DOWNLOAD_PATH="/downloads"
|
||||
TORRENT_TAG=MOVIEPILOT
|
||||
# 【*】下载保存目录,容器内映射路径需要一致
|
||||
DOWNLOAD_PATH=/downloads
|
||||
# 电影下载保存目录,容器内映射路径需要一致
|
||||
DOWNLOAD_MOVIE_PATH=
|
||||
# 电视剧下载保存目录,容器内映射路径需要一致
|
||||
@@ -127,8 +165,8 @@ DOWNLOAD_SUBTITLE=true
|
||||
####################################
|
||||
# 媒体服务器 #
|
||||
####################################
|
||||
# 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
|
||||
MEDIASERVER="emby"
|
||||
# 【*】媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
|
||||
MEDIASERVER=emby
|
||||
# 入库刷新媒体库
|
||||
REFRESH_MEDIASERVER=true
|
||||
# 媒体服务器同步间隔(小时)
|
||||
@@ -147,39 +185,3 @@ JELLYFIN_API_KEY=
|
||||
PLEX_HOST=
|
||||
# Plex Token
|
||||
PLEX_TOKEN=
|
||||
|
||||
####################################
|
||||
# 站点 #
|
||||
####################################
|
||||
# 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"
|
||||
|
||||
####################################
|
||||
# 媒体库 #
|
||||
####################################
|
||||
# 转移方式 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}}"
|
||||
|
||||
@@ -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}")
|
||||
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