mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-11 09:59:51 +08:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd391d14f9 | ||
|
|
d7844968ab | ||
|
|
70ea398f14 | ||
|
|
860d55a0e2 | ||
|
|
0e35cec6e2 | ||
|
|
5778e86260 | ||
|
|
967d0b1205 | ||
|
|
0b2d419000 | ||
|
|
149104063c | ||
|
|
498168a2d3 | ||
|
|
88e307416d | ||
|
|
3bb2eedb33 | ||
|
|
36c046ad6a | ||
|
|
85396df221 | ||
|
|
2f0f58783e | ||
|
|
2d989d4229 | ||
|
|
ecc8b6b385 | ||
|
|
aa90c5d5c0 | ||
|
|
5f7d93f170 | ||
|
|
0fbe51f257 | ||
|
|
be941ebdd1 | ||
|
|
4d900c2eb0 | ||
|
|
93c473afe7 | ||
|
|
4c9a66f586 | ||
|
|
375e16e0dc | ||
|
|
91085d13a3 | ||
|
|
3f83894dc6 | ||
|
|
5946684ee6 | ||
|
|
7e3f25879f | ||
|
|
48dcc3ee1b | ||
|
|
fca0a4b511 | ||
|
|
d6831a8881 | ||
|
|
39a646ed92 | ||
|
|
595965c5d0 | ||
|
|
3bb6f8a0c0 | ||
|
|
1924a2017e | ||
|
|
60140fd2e6 | ||
|
|
65b5219e45 | ||
|
|
ae2f649aee | ||
|
|
bf3e860a18 | ||
|
|
0b44a91493 | ||
|
|
16077b3341 | ||
|
|
a7cedde721 | ||
|
|
ecd53192dc | ||
|
|
a03c76e211 | ||
|
|
de427fd7a9 |
65
.github/workflows/build-windows.yml
vendored
65
.github/workflows/build-windows.yml
vendored
@@ -1,65 +0,0 @@
|
|||||||
name: MoviePilot Windows Builder
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- version.py
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Windows-build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Release Version
|
|
||||||
id: release_version
|
|
||||||
run: |
|
|
||||||
$app_version = Select-String -Path "version.py" -Pattern "APP_VERSION\s=\s'v(.*)'" | ForEach-Object { $_.Matches.Groups[1].Value }
|
|
||||||
$env:GITHUB_ENV += "app_version=$app_version"
|
|
||||||
|
|
||||||
- name: Init Python 3.11.4
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.11.4'
|
|
||||||
|
|
||||||
- name: Install Dependent Packages
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install wheel pyinstaller
|
|
||||||
pip install -r requirements.txt
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Pyinstaller
|
|
||||||
run: |
|
|
||||||
pyinstaller windows.spec
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Upload Windows File
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: windows
|
|
||||||
path: dist/MoviePilot.exe
|
|
||||||
|
|
||||||
- name: Generate Release
|
|
||||||
id: generate_release
|
|
||||||
uses: actions/create-release@latest
|
|
||||||
with:
|
|
||||||
tag_name: v${{ env.app_version }}
|
|
||||||
release_name: v${{ env.app_version }}
|
|
||||||
body: ${{ github.event.commits[0].message }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
|
||||||
uses: dwenegar/upload-release-assets@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
release_id: ${{ steps.generate_release.outputs.id }}
|
|
||||||
assets_path: |
|
|
||||||
dist/MoviePilot.exe
|
|
||||||
99
.github/workflows/build.yml
vendored
99
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: MoviePilot Docker Builder
|
name: MoviePilot Builder
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
||||||
echo "app_version=$app_version" >> $GITHUB_ENV
|
echo "app_version=$app_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker Meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -56,4 +56,97 @@ jobs:
|
|||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha, scope=${{ github.workflow }}
|
cache-from: type=gha, scope=${{ github.workflow }}
|
||||||
cache-to: type=gha, scope=${{ github.workflow }}
|
cache-to: type=gha, scope=${{ github.workflow }}
|
||||||
|
|
||||||
|
Windows-build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
name: Build Windows Binary
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Init Python 3.11.4
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11.4'
|
||||||
|
|
||||||
|
- name: Install Dependent Packages
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install wheel pyinstaller
|
||||||
|
pip install -r requirements.txt
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Prepare Frontend
|
||||||
|
run: |
|
||||||
|
Invoke-WebRequest -Uri "http://nginx.org/download/nginx-1.25.2.zip" -OutFile "nginx.zip"
|
||||||
|
Expand-Archive -Path "nginx.zip" -DestinationPath "nginx-1.25.2"
|
||||||
|
Move-Item -Path "nginx-1.25.2/nginx-1.25.2" -Destination "nginx"
|
||||||
|
Remove-Item -Path "nginx.zip"
|
||||||
|
Remove-Item -Path "nginx-1.25.2" -Recurse -Force
|
||||||
|
$FRONTEND_VERSION = (Invoke-WebRequest -Uri "https://api.github.com/repos/jxxghp/MoviePilot-Frontend/releases/latest" | ConvertFrom-Json).tag_name
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/jxxghp/MoviePilot-Frontend/releases/download/$FRONTEND_VERSION/dist.zip" -OutFile "dist.zip"
|
||||||
|
Expand-Archive -Path "dist.zip" -DestinationPath "dist"
|
||||||
|
Move-Item -Path "dist/dist/*" -Destination "nginx/html" -Force
|
||||||
|
Remove-Item -Path "dist.zip"
|
||||||
|
Remove-Item -Path "dist" -Recurse -Force
|
||||||
|
Move-Item -Path "nginx/html/nginx.conf" -Destination "nginx/conf/nginx.conf" -Force
|
||||||
|
New-Item -Path "nginx/temp" -ItemType Directory -Force
|
||||||
|
New-Item -Path "nginx/temp/__keep__.txt" -ItemType File -Force
|
||||||
|
New-Item -Path "nginx/logs" -ItemType Directory -Force
|
||||||
|
New-Item -Path "nginx/logs/__keep__.txt" -ItemType File -Force
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Pyinstaller
|
||||||
|
run: |
|
||||||
|
pyinstaller windows.spec
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Upload Windows File
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: dist/MoviePilot.exe
|
||||||
|
|
||||||
|
Create-release:
|
||||||
|
permissions: write-all
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ Windows-build, Docker-build ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Release Version
|
||||||
|
id: release_version
|
||||||
|
run: |
|
||||||
|
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
||||||
|
echo "app_version=$app_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: get release_informations
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir releases
|
||||||
|
mv ./windows/MoviePilot.exe ./releases/MoviePilot_v${{ env.app_version }}.exe
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@latest
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: v${{ env.app_version }}
|
||||||
|
release_name: v${{ env.app_version }}
|
||||||
|
body: ${{ github.event.commits[0].message }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset
|
||||||
|
uses: dwenegar/upload-release-assets@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
release_id: ${{ steps.create_release.outputs.id }}
|
||||||
|
assets_path: |
|
||||||
|
./releases/
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*.c
|
*.c
|
||||||
build/
|
build/
|
||||||
|
dist/
|
||||||
|
nginx/
|
||||||
test.py
|
test.py
|
||||||
app/helper/sites.py
|
app/helper/sites.py
|
||||||
config/user.db
|
config/user.db
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
FROM python:3.11.4-slim-bullseye
|
FROM python:3.11.4-slim-bullseye
|
||||||
ARG MOVIEPILOT_VERSION
|
ARG MOVIEPILOT_VERSION
|
||||||
ENV LANG="C.UTF-8" \
|
ENV LANG="C.UTF-8" \
|
||||||
|
TZ="Asia/Shanghai" \
|
||||||
HOME="/moviepilot" \
|
HOME="/moviepilot" \
|
||||||
|
CONFIG_DIR="/config" \
|
||||||
TERM="xterm" \
|
TERM="xterm" \
|
||||||
PUID=0 \
|
PUID=0 \
|
||||||
PGID=0 \
|
PGID=0 \
|
||||||
UMASK=000 \
|
UMASK=000 \
|
||||||
PORT=3001 \
|
PORT=3001 \
|
||||||
NGINX_PORT=3000 \
|
NGINX_PORT=3000 \
|
||||||
|
PROXY_HOST="" \
|
||||||
MOVIEPILOT_AUTO_UPDATE=true \
|
MOVIEPILOT_AUTO_UPDATE=true \
|
||||||
MOVIEPILOT_AUTO_UPDATE_DEV=false \
|
MOVIEPILOT_AUTO_UPDATE_DEV=false \
|
||||||
CONFIG_DIR="/config"
|
AUTH_SITE="iyuu" \
|
||||||
|
IYUU_SIGN=""
|
||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt-get -y install \
|
&& apt-get -y install \
|
||||||
@@ -27,6 +31,7 @@ RUN apt-get update -y \
|
|||||||
dumb-init \
|
dumb-init \
|
||||||
jq \
|
jq \
|
||||||
haproxy \
|
haproxy \
|
||||||
|
rclone \
|
||||||
&& \
|
&& \
|
||||||
if [ "$(uname -m)" = "x86_64" ]; \
|
if [ "$(uname -m)" = "x86_64" ]; \
|
||||||
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \
|
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
# 仅用于学习交流使用,请勿在任何国内平台宣传该项目!
|
# 仅用于学习交流使用,请勿在任何国内平台宣传该项目!
|
||||||
|
|
||||||
Docker:https://hub.docker.com/r/jxxghp/moviepilot
|
|
||||||
|
|
||||||
发布频道:https://t.me/moviepilot_channel
|
发布频道:https://t.me/moviepilot_channel
|
||||||
|
|
||||||
## 主要特性
|
## 主要特性
|
||||||
@@ -33,19 +31,25 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
|||||||
|
|
||||||
### 4. **安装MoviePilot**
|
### 4. **安装MoviePilot**
|
||||||
|
|
||||||
目前仅提供docker镜像,点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令:
|
- Docker镜像
|
||||||
|
|
||||||
```shell
|
点击 [这里](https://hub.docker.com/r/jxxghp/moviepilot) 或执行命令:
|
||||||
docker pull jxxghp/moviepilot:latest
|
|
||||||
```
|
```shell
|
||||||
|
docker pull jxxghp/moviepilot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Windows
|
||||||
|
|
||||||
|
下载 [MoviePilot.exe](https://github.com/jxxghp/MoviePilot/releases),双击运行后自动生成配置文件目录。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
|
项目的所有配置均通过环境变量进行设置,支持两种配置方式:
|
||||||
- 在docker环境变量部分进行参数配置,部分环境建立容器后会自动显示待配置项,如未自动显示配置项则需要手动增加对应环境变量。
|
- 在Docker环境变量部分或Wdinows系统环境变量中进行参数配置,如未自动显示配置项则需要手动增加对应环境变量。
|
||||||
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
|
- 下载 [app.env](https://github.com/jxxghp/MoviePilot/raw/main/config/app.env) 配置文件,修改好配置后放置到配置文件映射路径根目录,配置项可根据说明自主增减。
|
||||||
|
|
||||||
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,部分参数如路径映射、站点认证、权限端口等必须通过环境变量进行配置。
|
配置文件映射路径:`/config`,配置项生效优先级:环境变量 > env文件 > 默认值,**部分参数如路径映射、站点认证、权限端口、时区等必须通过环境变量进行配置**。
|
||||||
|
|
||||||
> $\color{red}{*}$ 号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
|
> $\color{red}{*}$ 号标识的为必填项,其它为可选项,可选项可删除配置变量从而使用默认值。
|
||||||
|
|
||||||
@@ -56,13 +60,13 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
|
- **PUID**:运行程序用户的`uid`,默认`0`(仅支持环境变量配置)
|
||||||
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
|
- **PGID**:运行程序用户的`gid`,默认`0`(仅支持环境变量配置)
|
||||||
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
|
- **UMASK**:掩码权限,默认`000`,可以考虑设置为`022`(仅支持环境变量配置)
|
||||||
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`,具体看下方`PROXY_HOST`解释**(仅支持环境变量配置)
|
- **PROXY_HOST:** 网络代理,访问themoviedb或者重启更新需要使用代理访问,格式为`http(s)://ip:port`、`socks5://user:pass@host:port`(仅支持环境变量配置)
|
||||||
|
- **MOVIEPILOT_AUTO_UPDATE**:重启更新,`true`/`false`,默认`true` **注意:如果出现网络问题可以配置`PROXY_HOST`**(仅支持环境变量配置)
|
||||||
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`(仅支持环境变量配置)
|
- **MOVIEPILOT_AUTO_UPDATE_DEV**:重启时更新到未发布的开发版本代码,`true`/`false`,默认`false`(仅支持环境变量配置)
|
||||||
---
|
---
|
||||||
- **SUPERUSER $\color{red}{*}$ :** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
- **SUPERUSER $\color{red}{*}$ :** 超级管理员用户名,默认`admin`,安装后使用该用户登录后台管理界面
|
||||||
- **SUPERUSER_PASSWORD $\color{red}{*}$ :** 超级管理员初始密码,默认`password`,建议修改为复杂密码
|
- **SUPERUSER_PASSWORD $\color{red}{*}$ :** 超级管理员初始密码,默认`password`,建议修改为复杂密码
|
||||||
- **API_TOKEN $\color{red}{*}$ :** API密钥,默认`moviepilot`,在媒体服务器Webhook、微信回调等地址配置中需要加上`?token=`该值,建议修改为复杂字符串
|
- **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`或其它中转代理服务地址,能连通即可
|
- **TMDB_API_DOMAIN:** TMDB API地址,默认`api.themoviedb.org`,也可配置为`api.tmdb.org`或其它中转代理服务地址,能连通即可
|
||||||
- **TMDB_IMAGE_DOMAIN:** TMDB图片地址,默认`image.tmdb.org`,可配置为其它中转代理以加速TMDB图片显示,如:`static-mdb.v.geilijiasu.com`
|
- **TMDB_IMAGE_DOMAIN:** TMDB图片地址,默认`image.tmdb.org`,可配置为其它中转代理以加速TMDB图片显示,如:`static-mdb.v.geilijiasu.com`
|
||||||
---
|
---
|
||||||
@@ -70,7 +74,7 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
- **SCRAP_SOURCE:** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
|
- **SCRAP_SOURCE:** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
|
||||||
- **SCRAP_FOLLOW_TMDB:** 新增已入库媒体是否跟随TMDB信息变化,`true`/`false`,默认`true`
|
- **SCRAP_FOLLOW_TMDB:** 新增已入库媒体是否跟随TMDB信息变化,`true`/`false`,默认`true`
|
||||||
---
|
---
|
||||||
- **TRANSFER_TYPE $\color{red}{*}$ :** 整理转移方式,支持`link`/`copy`/`move`/`softlink` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响**
|
- **TRANSFER_TYPE $\color{red}{*}$ :** 整理转移方式,支持`link`/`copy`/`move`/`softlink`/`rclone_copy`/`rclone_move` **注意:在`link`和`softlink`转移方式下,转移后的文件会继承源文件的权限掩码,不受`UMASK`影响;rclone需要自行映射rclone配置目录到容器中或在容器内完成rclone配置,节点名称必须为:`MP`**
|
||||||
- **LIBRARY_PATH $\color{red}{*}$ :** 媒体库目录,多个目录使用`,`分隔
|
- **LIBRARY_PATH $\color{red}{*}$ :** 媒体库目录,多个目录使用`,`分隔
|
||||||
- **LIBRARY_MOVIE_NAME:** 电影媒体库目录名称(不是完整路径),默认`电影`
|
- **LIBRARY_MOVIE_NAME:** 电影媒体库目录名称(不是完整路径),默认`电影`
|
||||||
- **LIBRARY_TV_NAME:** 电视剧媒体库目录称(不是完整路径),默认`电视剧`
|
- **LIBRARY_TV_NAME:** 电视剧媒体库目录称(不是完整路径),默认`电视剧`
|
||||||
@@ -169,7 +173,7 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
|
|
||||||
### 2. **用户认证**
|
### 2. **用户认证**
|
||||||
|
|
||||||
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过docker环境变量配置**)
|
`MoviePilot`需要认证后才能使用,配置`AUTH_SITE`后,需要根据下表配置对应站点的认证参数(**仅能通过环境变量配置**)
|
||||||
|
|
||||||
- **AUTH_SITE $\color{red}{*}$ :** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
- **AUTH_SITE $\color{red}{*}$ :** 认证站点,支持`iyuu`/`hhclub`/`audiences`/`hddolby`/`zmpt`/`freefarm`/`hdfans`/`wintersakura`/`leaves`/`1ptba`/`icc2022`/`ptlsp`/`xingtan`
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ def tv_weekly_global(page: int = 1,
|
|||||||
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def tv_animation(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
热门动画剧集
|
||||||
|
"""
|
||||||
|
tvs = DoubanChain(db).tv_animation(page=page, count=count)
|
||||||
|
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{doubanid}", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
|
@router.get("/{doubanid}", summary="查询豆瓣详情", response_model=schemas.MediaInfo)
|
||||||
def douban_info(doubanid: str,
|
def douban_info(doubanid: str,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ async def login_access_token(
|
|||||||
user.create(db)
|
user.create(db)
|
||||||
elif not user.is_active:
|
elif not user.is_active:
|
||||||
raise HTTPException(status_code=403, detail="用户未启用")
|
raise HTTPException(status_code=403, detail="用户未启用")
|
||||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
||||||
return schemas.Token(
|
return schemas.Token(
|
||||||
access_token=security.create_access_token(
|
access_token=security.create_access_token(
|
||||||
user.id, expires_delta=access_token_expires
|
user.id,
|
||||||
|
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
),
|
),
|
||||||
token_type="bearer",
|
token_type="bearer",
|
||||||
super_user=user.is_superuser,
|
super_user=user.is_superuser,
|
||||||
|
|||||||
@@ -29,18 +29,33 @@ class DoubanChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
根据豆瓣信息识别媒体信息
|
根据豆瓣信息识别媒体信息
|
||||||
"""
|
"""
|
||||||
# 使用原标题匹配
|
# 优先使用原标题匹配
|
||||||
meta = MetaInfo(title=doubaninfo.get("original_title") or doubaninfo.get("title"))
|
season_meta = None
|
||||||
|
if doubaninfo.get("original_title"):
|
||||||
|
meta = MetaInfo(title=doubaninfo.get("original_title"))
|
||||||
|
season_meta = MetaInfo(title=doubaninfo.get("title"))
|
||||||
|
# 合并季
|
||||||
|
meta.begin_season = season_meta.begin_season
|
||||||
|
else:
|
||||||
|
meta = MetaInfo(title=doubaninfo.get("title"))
|
||||||
|
# 年份
|
||||||
|
if doubaninfo.get("year"):
|
||||||
|
meta.year = doubaninfo.get("year")
|
||||||
# 处理类型
|
# 处理类型
|
||||||
if isinstance(doubaninfo.get('media_type'), MediaType):
|
if isinstance(doubaninfo.get('media_type'), MediaType):
|
||||||
meta.type = doubaninfo.get('media_type')
|
meta.type = doubaninfo.get('media_type')
|
||||||
else:
|
else:
|
||||||
meta.type = MediaType.MOVIE if doubaninfo.get("type") == "movie" else MediaType.TV
|
meta.type = MediaType.MOVIE if doubaninfo.get("type") == "movie" else MediaType.TV
|
||||||
# 识别媒体信息
|
# 使用原标题识别媒体信息
|
||||||
mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type)
|
mediainfo = self.recognize_media(meta=meta, mtype=meta.type)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
|
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
|
||||||
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
|
if season_meta and season_meta.name != meta.name:
|
||||||
|
# 使用主标题识别媒体信息
|
||||||
|
mediainfo = self.recognize_media(meta=season_meta, mtype=season_meta.type)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f'{season_meta.name} 未识别到TMDB媒体信息')
|
||||||
|
return Context(meta_info=season_meta, media_info=MediaInfo(douban_info=doubaninfo))
|
||||||
logger.info(f'识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year} {meta.season}')
|
logger.info(f'识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year} {meta.season}')
|
||||||
mediainfo.set_douban_info(doubaninfo)
|
mediainfo.set_douban_info(doubaninfo)
|
||||||
return Context(meta_info=meta, media_info=mediainfo)
|
return Context(meta_info=meta, media_info=mediainfo)
|
||||||
@@ -84,3 +99,9 @@ class DoubanChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
|
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
|
||||||
page=page, count=count)
|
page=page, count=count)
|
||||||
|
|
||||||
|
def tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
获取动画剧集
|
||||||
|
"""
|
||||||
|
return self.run_module("tv_animation", page=page, count=count)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -26,6 +25,8 @@ class Settings(BaseSettings):
|
|||||||
HOST: str = "0.0.0.0"
|
HOST: str = "0.0.0.0"
|
||||||
# API监听端口
|
# API监听端口
|
||||||
PORT: int = 3001
|
PORT: int = 3001
|
||||||
|
# 前端监听端口
|
||||||
|
NGINX_PORT: int = 3000
|
||||||
# 是否调试模式
|
# 是否调试模式
|
||||||
DEBUG: bool = False
|
DEBUG: bool = False
|
||||||
# 是否开发模式
|
# 是否开发模式
|
||||||
|
|||||||
98
app/main.py
98
app/main.py
@@ -1,10 +1,22 @@
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import uvicorn as uvicorn
|
import uvicorn as uvicorn
|
||||||
|
from PIL import Image
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from uvicorn import Config
|
from uvicorn import Config
|
||||||
|
|
||||||
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
|
# 禁用输出
|
||||||
|
if SystemUtils.is_frozen():
|
||||||
|
sys.stdout = open(os.devnull, 'w')
|
||||||
|
sys.stderr = open(os.devnull, 'w')
|
||||||
|
|
||||||
from app.command import Command
|
from app.command import Command
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.module import ModuleManager
|
from app.core.module import ModuleManager
|
||||||
@@ -44,6 +56,82 @@ def init_routers():
|
|||||||
App.include_router(arr_router, prefix="/api/v3")
|
App.include_router(arr_router, prefix="/api/v3")
|
||||||
|
|
||||||
|
|
||||||
|
def start_frontend():
|
||||||
|
"""
|
||||||
|
启动前端服务
|
||||||
|
"""
|
||||||
|
if not SystemUtils.is_frozen():
|
||||||
|
return
|
||||||
|
nginx_path = settings.ROOT_PATH / 'nginx'
|
||||||
|
if not nginx_path.exists():
|
||||||
|
return
|
||||||
|
import subprocess
|
||||||
|
if SystemUtils.is_windows():
|
||||||
|
subprocess.Popen("start nginx.exe",
|
||||||
|
cwd=nginx_path,
|
||||||
|
shell=True)
|
||||||
|
else:
|
||||||
|
subprocess.Popen("nohup ./nginx &",
|
||||||
|
cwd=nginx_path,
|
||||||
|
shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_frontend():
|
||||||
|
"""
|
||||||
|
停止前端服务
|
||||||
|
"""
|
||||||
|
if not SystemUtils.is_frozen():
|
||||||
|
return
|
||||||
|
import subprocess
|
||||||
|
if SystemUtils.is_windows():
|
||||||
|
subprocess.Popen(f"taskkill /f /im nginx.exe", shell=True)
|
||||||
|
else:
|
||||||
|
subprocess.Popen(f"killall nginx", shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def start_tray():
|
||||||
|
"""
|
||||||
|
启动托盘图标
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not SystemUtils.is_frozen():
|
||||||
|
return
|
||||||
|
|
||||||
|
def open_web():
|
||||||
|
"""
|
||||||
|
调用浏览器打开前端页面
|
||||||
|
"""
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(f"http://localhost:{settings.NGINX_PORT}")
|
||||||
|
|
||||||
|
def quit_app():
|
||||||
|
"""
|
||||||
|
退出程序
|
||||||
|
"""
|
||||||
|
TrayIcon.stop()
|
||||||
|
Server.should_exit = True
|
||||||
|
|
||||||
|
import pystray
|
||||||
|
|
||||||
|
# 托盘图标
|
||||||
|
TrayIcon = pystray.Icon(
|
||||||
|
settings.PROJECT_NAME,
|
||||||
|
icon=Image.open(settings.ROOT_PATH / 'app.ico'),
|
||||||
|
menu=pystray.Menu(
|
||||||
|
pystray.MenuItem(
|
||||||
|
'打开',
|
||||||
|
open_web,
|
||||||
|
),
|
||||||
|
pystray.MenuItem(
|
||||||
|
'退出',
|
||||||
|
quit_app,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 启动托盘图标
|
||||||
|
threading.Thread(target=TrayIcon.run, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
@App.on_event("shutdown")
|
@App.on_event("shutdown")
|
||||||
def shutdown_server():
|
def shutdown_server():
|
||||||
"""
|
"""
|
||||||
@@ -59,6 +147,8 @@ def shutdown_server():
|
|||||||
DisplayHelper().stop()
|
DisplayHelper().stop()
|
||||||
# 停止定时服务
|
# 停止定时服务
|
||||||
Scheduler().stop()
|
Scheduler().stop()
|
||||||
|
# 停止前端服务
|
||||||
|
stop_frontend()
|
||||||
|
|
||||||
|
|
||||||
@App.on_event("startup")
|
@App.on_event("startup")
|
||||||
@@ -66,7 +156,7 @@ def start_module():
|
|||||||
"""
|
"""
|
||||||
启动模块
|
启动模块
|
||||||
"""
|
"""
|
||||||
# 虚伪显示
|
# 虚拟显示
|
||||||
DisplayHelper()
|
DisplayHelper()
|
||||||
# 站点管理
|
# 站点管理
|
||||||
SitesHelper()
|
SitesHelper()
|
||||||
@@ -80,12 +170,16 @@ def start_module():
|
|||||||
Command()
|
Command()
|
||||||
# 初始化路由
|
# 初始化路由
|
||||||
init_routers()
|
init_routers()
|
||||||
|
# 启动前端服务
|
||||||
|
start_frontend()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# 启动托盘
|
||||||
|
start_tray()
|
||||||
# 初始化数据库
|
# 初始化数据库
|
||||||
init_db()
|
init_db()
|
||||||
# 更新数据库
|
# 更新数据库
|
||||||
update_db()
|
update_db()
|
||||||
# 启动服务
|
# 启动API服务
|
||||||
Server.run()
|
Server.run()
|
||||||
|
|||||||
@@ -369,6 +369,16 @@ class DoubanModule(_ModuleBase):
|
|||||||
return []
|
return []
|
||||||
return infos.get("subject_collection_items")
|
return infos.get("subject_collection_items")
|
||||||
|
|
||||||
|
def tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
获取豆瓣动画剧
|
||||||
|
"""
|
||||||
|
infos = self.doubanapi.tv_animation(start=(page - 1) * count,
|
||||||
|
count=count)
|
||||||
|
if not infos:
|
||||||
|
return []
|
||||||
|
return infos.get("subject_collection_items")
|
||||||
|
|
||||||
def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
|
def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
|
||||||
"""
|
"""
|
||||||
搜索媒体信息
|
搜索媒体信息
|
||||||
|
|||||||
@@ -161,7 +161,13 @@ class DoubanScraper:
|
|||||||
"""
|
"""
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
return
|
return
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
|
# 没有后缀时,处理URL转化为jpg格式
|
||||||
|
if not file_path.suffix:
|
||||||
|
url = url.replace("/format/webp", "/format/jpg")
|
||||||
|
file_path.with_suffix(".jpg")
|
||||||
logger.info(f"正在下载{file_path.stem}图片:{url} ...")
|
logger.info(f"正在下载{file_path.stem}图片:{url} ...")
|
||||||
r = RequestUtils().get_res(url=url)
|
r = RequestUtils().get_res(url=url)
|
||||||
if r:
|
if r:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Emby(metaclass=Singleton):
|
|||||||
if not self._host.startswith("http"):
|
if not self._host.startswith("http"):
|
||||||
self._host = "http://" + self._host
|
self._host = "http://" + self._host
|
||||||
self._apikey = settings.EMBY_API_KEY
|
self._apikey = settings.EMBY_API_KEY
|
||||||
self.user = self.get_user()
|
self.user = self.get_user(settings.SUPERUSER)
|
||||||
self.folders = self.get_emby_folders()
|
self.folders = self.get_emby_folders()
|
||||||
|
|
||||||
def is_inactive(self) -> bool:
|
def is_inactive(self) -> bool:
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ class FileTransferModule(_ModuleBase):
|
|||||||
elif transfer_type == 'move':
|
elif transfer_type == 'move':
|
||||||
# 移动
|
# 移动
|
||||||
retcode, retmsg = SystemUtils.move(file_item, target_file)
|
retcode, retmsg = SystemUtils.move(file_item, target_file)
|
||||||
|
elif transfer_type == 'rclone_move':
|
||||||
|
# Rclone 移动
|
||||||
|
retcode, retmsg = SystemUtils.rclone_move(file_item, target_file)
|
||||||
|
elif transfer_type == 'rclone_copy':
|
||||||
|
# Rclone 复制
|
||||||
|
retcode, retmsg = SystemUtils.rclone_copy(file_item, target_file)
|
||||||
else:
|
else:
|
||||||
# 复制
|
# 复制
|
||||||
retcode, retmsg = SystemUtils.copy(file_item, target_file)
|
retcode, retmsg = SystemUtils.copy(file_item, target_file)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
if not self._host.startswith("http"):
|
if not self._host.startswith("http"):
|
||||||
self._host = "http://" + self._host
|
self._host = "http://" + self._host
|
||||||
self._apikey = settings.JELLYFIN_API_KEY
|
self._apikey = settings.JELLYFIN_API_KEY
|
||||||
self.user = self.get_user()
|
self.user = self.get_user(settings.SUPERUSER)
|
||||||
self.serverid = self.get_server_id()
|
self.serverid = self.get_server_id()
|
||||||
|
|
||||||
def is_inactive(self) -> bool:
|
def is_inactive(self) -> bool:
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
# 直接查询详情
|
# 直接查询详情
|
||||||
info = self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
|
info = self.tmdb.get_info(mtype=mtype, tmdbid=tmdbid)
|
||||||
elif meta:
|
elif meta:
|
||||||
logger.info(f"正在识别 {meta.name} ...")
|
if meta.begin_season:
|
||||||
|
logger.info(f"正在识别 {meta.name} 第{meta.begin_season}季 ...")
|
||||||
|
else:
|
||||||
|
logger.info(f"正在识别 {meta.name} ...")
|
||||||
if meta.type == MediaType.UNKNOWN and not meta.year:
|
if meta.type == MediaType.UNKNOWN and not meta.year:
|
||||||
info = self.tmdb.match_multi(meta.name)
|
info = self.tmdb.match_multi(meta.name)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -858,7 +858,7 @@ class BrushFlow(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/upload.png'
|
'src': '/plugin_icon/upload.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -928,7 +928,7 @@ class BrushFlow(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/download.png'
|
'src': '/plugin_icon/download.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -998,7 +998,7 @@ class BrushFlow(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/seed.png'
|
'src': '/plugin_icon/seed.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1068,7 +1068,7 @@ class BrushFlow(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/delete.png'
|
'src': '/plugin_icon/delete.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class CustomHosts(_PluginBase):
|
|||||||
# 添加新的Hosts
|
# 添加新的Hosts
|
||||||
system_hosts.add(new_entrys)
|
system_hosts.add(new_entrys)
|
||||||
system_hosts.write()
|
system_hosts.write()
|
||||||
logger.info("更新系统hosts文件成功")
|
logger.info("更新系统hosts文件成功(注:容器运行则更新容器hosts!)")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
err_flag = True
|
err_flag = True
|
||||||
logger.error(f"更新系统hosts文件失败:{str(err) or '请检查权限'}")
|
logger.error(f"更新系统hosts文件失败:{str(err) or '请检查权限'}")
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ class DirMonitor(_PluginBase):
|
|||||||
if not mon_path:
|
if not mon_path:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 自定义转移方式
|
||||||
|
_transfer_type = self._transfer_type
|
||||||
|
if mon_path.count("#") == 1:
|
||||||
|
_transfer_type = mon_path.split("#")[1]
|
||||||
|
mon_path = mon_path.split("#")[0]
|
||||||
|
|
||||||
# 存储目的目录
|
# 存储目的目录
|
||||||
if SystemUtils.is_windows():
|
if SystemUtils.is_windows():
|
||||||
if mon_path.count(":") > 1:
|
if mon_path.count(":") > 1:
|
||||||
@@ -136,21 +142,19 @@ class DirMonitor(_PluginBase):
|
|||||||
else:
|
else:
|
||||||
paths = mon_path.split(":")
|
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
|
target_path = None
|
||||||
if len(paths) > 1:
|
if len(paths) > 1:
|
||||||
mon_path = paths[0]
|
mon_path = paths[0]
|
||||||
target_path = Path(paths[1])
|
target_path = Path(paths[1])
|
||||||
self._dirconf[mon_path] = target_path
|
self._dirconf[mon_path] = target_path
|
||||||
|
|
||||||
|
# 转移方式
|
||||||
|
self._transferconf[mon_path] = _transfer_type
|
||||||
|
|
||||||
# 检查媒体库目录是不是下载目录的子目录
|
# 检查媒体库目录是不是下载目录的子目录
|
||||||
try:
|
try:
|
||||||
if target_path.is_relative_to(Path(mon_path)):
|
if target_path and target_path.is_relative_to(Path(mon_path)):
|
||||||
logger.warn(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控")
|
logger.warn(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控")
|
||||||
self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控")
|
self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控")
|
||||||
continue
|
continue
|
||||||
@@ -615,8 +619,9 @@ class DirMonitor(_PluginBase):
|
|||||||
'rows': 5,
|
'rows': 5,
|
||||||
'placeholder': '每一行一个目录,支持三种配置方式:\n'
|
'placeholder': '每一行一个目录,支持三种配置方式:\n'
|
||||||
'监控目录\n'
|
'监控目录\n'
|
||||||
|
'监控目录#转移方式(move|copy|link|softlink|rclone_copy|rclone_move)\n'
|
||||||
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)\n'
|
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)\n'
|
||||||
'监控目录:转移目的目录#转移方式(move|copy|link|softlink)'
|
'监控目录:转移目的目录#转移方式(move|copy|link|softlink|rclone_copy|rclone_move)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -449,15 +449,19 @@ class DoubanSync(_PluginBase):
|
|||||||
results = self.rsshelper.parse(url)
|
results = self.rsshelper.parse(url)
|
||||||
if not results:
|
if not results:
|
||||||
logger.error(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}")
|
logger.error(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}")
|
||||||
return
|
continue
|
||||||
|
else:
|
||||||
|
logger.info(f"获取到用户 {user_id} 豆瓣RSS数据:{len(results)}")
|
||||||
# 解析数据
|
# 解析数据
|
||||||
for result in results:
|
for result in results:
|
||||||
try:
|
try:
|
||||||
dtype = result.get("title", "")[:2]
|
dtype = result.get("title", "")[:2]
|
||||||
title = result.get("title", "")[2:]
|
title = result.get("title", "")[2:]
|
||||||
if dtype not in ["想看"]:
|
if dtype not in ["想看"]:
|
||||||
|
logger.info(f'标题:{title},非想看数据,跳过')
|
||||||
continue
|
continue
|
||||||
if not result.get("link"):
|
if not result.get("link"):
|
||||||
|
logger.warn(f'标题:{title},未获取到链接,跳过')
|
||||||
continue
|
continue
|
||||||
# 判断是否在天数范围
|
# 判断是否在天数范围
|
||||||
pubdate: Optional[datetime.datetime] = result.get("pubdate")
|
pubdate: Optional[datetime.datetime] = result.get("pubdate")
|
||||||
@@ -468,6 +472,7 @@ class DoubanSync(_PluginBase):
|
|||||||
douban_id = result.get("link", "").split("/")[-2]
|
douban_id = result.get("link", "").split("/")[-2]
|
||||||
# 检查是否处理过
|
# 检查是否处理过
|
||||||
if not douban_id or douban_id in [h.get("doubanid") for h in history]:
|
if not douban_id or douban_id in [h.get("doubanid") for h in history]:
|
||||||
|
logger.info(f'标题:{title},豆瓣ID:{douban_id} 已处理过')
|
||||||
continue
|
continue
|
||||||
# 根据豆瓣ID获取豆瓣数据
|
# 根据豆瓣ID获取豆瓣数据
|
||||||
doubaninfo: Optional[dict] = self.chain.douban_info(doubanid=douban_id)
|
doubaninfo: Optional[dict] = self.chain.douban_info(doubanid=douban_id)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class InvitesSignin(_PluginBase):
|
|||||||
# 插件图标
|
# 插件图标
|
||||||
plugin_icon = "invites.png"
|
plugin_icon = "invites.png"
|
||||||
# 主题色
|
# 主题色
|
||||||
plugin_color = "#4FB647"
|
plugin_color = "#FFFFFF"
|
||||||
# 插件版本
|
# 插件版本
|
||||||
plugin_version = "1.0"
|
plugin_version = "1.0"
|
||||||
# 插件作者
|
# 插件作者
|
||||||
|
|||||||
@@ -581,8 +581,10 @@ class PersonMeta(_PluginBase):
|
|||||||
"""
|
"""
|
||||||
获取豆瓣演员信息
|
获取豆瓣演员信息
|
||||||
"""
|
"""
|
||||||
# 随机休眠1-5秒
|
# 随机休眠 3-10 秒
|
||||||
time.sleep(1 + int(time.time()) % 5)
|
sleep_time = 3 + int(time.time()) % 7
|
||||||
|
logger.info(f"随机休眠 {sleep_time}秒 ...")
|
||||||
|
time.sleep(sleep_time)
|
||||||
# 匹配豆瓣信息
|
# 匹配豆瓣信息
|
||||||
doubaninfo = self.chain.match_doubaninfo(name=mediainfo.title,
|
doubaninfo = self.chain.match_doubaninfo(name=mediainfo.title,
|
||||||
mtype=mediainfo.type.value,
|
mtype=mediainfo.type.value,
|
||||||
@@ -712,7 +714,7 @@ class PersonMeta(_PluginBase):
|
|||||||
logger.error(f"获取Jellyfin媒体的所有子媒体项失败:{err}")
|
logger.error(f"获取Jellyfin媒体的所有子媒体项失败:{err}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def __get_plex_items(t: str) -> dict:
|
def __get_plex_items() -> dict:
|
||||||
"""
|
"""
|
||||||
获得Plex媒体的所有子媒体项
|
获得Plex媒体的所有子媒体项
|
||||||
"""
|
"""
|
||||||
@@ -721,7 +723,7 @@ class PersonMeta(_PluginBase):
|
|||||||
plex = Plex().get_plex()
|
plex = Plex().get_plex()
|
||||||
items['Items'] = []
|
items['Items'] = []
|
||||||
if parentid:
|
if parentid:
|
||||||
if mtype and 'Season' in t:
|
if mtype and 'Season' in mtype:
|
||||||
plexitem = plex.library.fetchItem(ekey=parentid)
|
plexitem = plex.library.fetchItem(ekey=parentid)
|
||||||
items['Items'] = []
|
items['Items'] = []
|
||||||
for season in plexitem.seasons():
|
for season in plexitem.seasons():
|
||||||
@@ -732,7 +734,7 @@ class PersonMeta(_PluginBase):
|
|||||||
'Overview': season.summary
|
'Overview': season.summary
|
||||||
}
|
}
|
||||||
items['Items'].append(item)
|
items['Items'].append(item)
|
||||||
elif mtype and 'Episode' in t:
|
elif mtype and 'Episode' in mtype:
|
||||||
plexitem = plex.library.fetchItem(ekey=parentid)
|
plexitem = plex.library.fetchItem(ekey=parentid)
|
||||||
items['Items'] = []
|
items['Items'] = []
|
||||||
for episode in plexitem.episodes():
|
for episode in plexitem.episodes():
|
||||||
@@ -783,7 +785,7 @@ class PersonMeta(_PluginBase):
|
|||||||
elif server == "jellyfin":
|
elif server == "jellyfin":
|
||||||
return __get_jellyfin_items()
|
return __get_jellyfin_items()
|
||||||
else:
|
else:
|
||||||
return __get_plex_items(mtype)
|
return __get_plex_items()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_iteminfo(server: str, itemid: str, iteminfo: dict):
|
def set_iteminfo(server: str, itemid: str, iteminfo: dict):
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ class SiteStatistic(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/upload.png'
|
'src': '/plugin_icon/upload.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -537,7 +537,7 @@ class SiteStatistic(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/download.png'
|
'src': '/plugin_icon/download.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -607,7 +607,7 @@ class SiteStatistic(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/seed.png'
|
'src': '/plugin_icon/seed.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -677,7 +677,7 @@ class SiteStatistic(_PluginBase):
|
|||||||
{
|
{
|
||||||
'component': 'VImg',
|
'component': 'VImg',
|
||||||
'props': {
|
'props': {
|
||||||
'src': '/plugin/database.png'
|
'src': '/plugin_icon/database.png'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class TransferHistory(BaseModel):
|
|||||||
src: Optional[str] = None
|
src: Optional[str] = None
|
||||||
# 目的目录
|
# 目的目录
|
||||||
dest: Optional[str] = None
|
dest: Optional[str] = None
|
||||||
# 转移模式link/copy/move/softlink
|
# 转移模式
|
||||||
mode: Optional[str] = None
|
mode: Optional[str] = None
|
||||||
# 类型:电影、电视剧
|
# 类型:电影、电视剧
|
||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Union, Tuple
|
from typing import List, Union, Tuple
|
||||||
@@ -97,7 +98,7 @@ class SystemUtils:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# link到当前目录并改名
|
# link到当前目录并改名
|
||||||
tmp_path = (src.parent / dest.name).with_suffix(".mp")
|
tmp_path = src.parent / (dest.name + ".mp")
|
||||||
tmp_path.hardlink_to(src)
|
tmp_path.hardlink_to(src)
|
||||||
# 移动到目标目录
|
# 移动到目标目录
|
||||||
shutil.move(tmp_path, dest)
|
shutil.move(tmp_path, dest)
|
||||||
@@ -118,6 +119,54 @@ class SystemUtils:
|
|||||||
print(str(err))
|
print(str(err))
|
||||||
return -1, str(err)
|
return -1, str(err)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rclone_move(src: Path, dest: Path):
|
||||||
|
"""
|
||||||
|
Rclone移动
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'moveto',
|
||||||
|
str(src),
|
||||||
|
f'MP:{dest}'
|
||||||
|
],
|
||||||
|
startupinfo=SystemUtils.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
return retcode, ""
|
||||||
|
except Exception as err:
|
||||||
|
print(str(err))
|
||||||
|
return -1, str(err)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rclone_copy(src: Path, dest: Path):
|
||||||
|
"""
|
||||||
|
Rclone复制
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'copyto',
|
||||||
|
str(src),
|
||||||
|
f'MP:{dest}'
|
||||||
|
],
|
||||||
|
startupinfo=SystemUtils.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
return retcode, ""
|
||||||
|
except Exception as err:
|
||||||
|
print(str(err))
|
||||||
|
return -1, str(err)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_hidden_shell():
|
||||||
|
if SystemUtils.is_windows():
|
||||||
|
st = subprocess.STARTUPINFO()
|
||||||
|
st.dwFlags = subprocess.STARTF_USESHOWWINDOW
|
||||||
|
st.wShowWindow = subprocess.SW_HIDE
|
||||||
|
return st
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_files(directory: Path, extensions: list, min_filesize: int = 0) -> List[Path]:
|
def list_files(directory: Path, extensions: list, min_filesize: int = 0) -> List[Path]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
####################################
|
####################################
|
||||||
# 基础设置 #
|
# 基础设置 #
|
||||||
####################################
|
####################################
|
||||||
# 时区
|
|
||||||
TZ=Asia/Shanghai
|
|
||||||
# 【*】API监听地址
|
# 【*】API监听地址
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
# 是否调试模式
|
# 是否调试模式
|
||||||
@@ -19,8 +17,6 @@ SUPERUSER=admin
|
|||||||
SUPERUSER_PASSWORD=password
|
SUPERUSER_PASSWORD=password
|
||||||
# 【*】API密钥,建议更换复杂字符串
|
# 【*】API密钥,建议更换复杂字符串
|
||||||
API_TOKEN=moviepilot
|
API_TOKEN=moviepilot
|
||||||
# 网络代理 IP:PORT
|
|
||||||
PROXY_HOST=
|
|
||||||
# TMDB图片地址,无需修改需保留默认值
|
# TMDB图片地址,无需修改需保留默认值
|
||||||
TMDB_IMAGE_DOMAIN=image.tmdb.org
|
TMDB_IMAGE_DOMAIN=image.tmdb.org
|
||||||
# TMDB API地址,无需修改需保留默认值
|
# TMDB API地址,无需修改需保留默认值
|
||||||
@@ -43,7 +39,7 @@ SCRAP_SOURCE=themoviedb
|
|||||||
####################################
|
####################################
|
||||||
# 媒体库 #
|
# 媒体库 #
|
||||||
####################################
|
####################################
|
||||||
# 【*】转移方式 link/copy/move/softlink
|
# 【*】转移方式 link/copy/move/softlink/rclone_copy/rclone_move
|
||||||
TRANSFER_TYPE=copy
|
TRANSFER_TYPE=copy
|
||||||
# 【*】媒体库目录,多个目录使用,分隔
|
# 【*】媒体库目录,多个目录使用,分隔
|
||||||
LIBRARY_PATH=
|
LIBRARY_PATH=
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ requests_cache~=0.5.2
|
|||||||
parse~=1.19.0
|
parse~=1.19.0
|
||||||
docker~=6.1.3
|
docker~=6.1.3
|
||||||
cachetools~=5.3.1
|
cachetools~=5.3.1
|
||||||
fast-bencode==1.1.3
|
fast-bencode~=1.1.3
|
||||||
|
pystray~=0.19.5
|
||||||
@@ -1 +1 @@
|
|||||||
APP_VERSION = 'v1.2.9'
|
APP_VERSION = 'v1.3.1'
|
||||||
|
|||||||
57
windows.spec
57
windows.spec
@@ -1,11 +1,12 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
def collect_pkg_data(package, include_py_files=False, subdir=None):
|
def collect_pkg_data(package: str, include_py_files: bool = False, subdir: str = None):
|
||||||
"""
|
"""
|
||||||
Collect all data files from the given package.
|
Collect all data files from the given package.
|
||||||
"""
|
"""
|
||||||
import os
|
from pathlib import Path
|
||||||
from PyInstaller.utils.hooks import get_package_paths, remove_prefix, PY_IGNORE_EXTENSIONS
|
from PyInstaller.utils.hooks import get_package_paths, PY_IGNORE_EXTENSIONS
|
||||||
|
from PyInstaller.building.datastruct import TOC
|
||||||
|
|
||||||
# Accept only strings as packages.
|
# Accept only strings as packages.
|
||||||
if type(package) is not str:
|
if type(package) is not str:
|
||||||
@@ -13,36 +14,36 @@ def collect_pkg_data(package, include_py_files=False, subdir=None):
|
|||||||
|
|
||||||
pkg_base, pkg_dir = get_package_paths(package)
|
pkg_base, pkg_dir = get_package_paths(package)
|
||||||
if subdir:
|
if subdir:
|
||||||
pkg_dir = os.path.join(pkg_dir, subdir)
|
pkg_path = Path(pkg_dir) / subdir
|
||||||
|
else:
|
||||||
|
pkg_path = Path(pkg_dir)
|
||||||
# Walk through all file in the given package, looking for data files.
|
# Walk through all file in the given package, looking for data files.
|
||||||
data_toc = TOC()
|
data_toc = TOC()
|
||||||
for dir_path, dir_names, files in os.walk(pkg_dir):
|
for file in pkg_path.rglob('*'):
|
||||||
for f in files:
|
if file.is_file():
|
||||||
extension = os.path.splitext(f)[1]
|
extension = file.suffix
|
||||||
if include_py_files or (extension not in PY_IGNORE_EXTENSIONS):
|
if not include_py_files and (extension in PY_IGNORE_EXTENSIONS):
|
||||||
source_file = os.path.join(dir_path, f)
|
continue
|
||||||
dest_folder = remove_prefix(dir_path, os.path.dirname(pkg_base) + os.sep)
|
data_toc.append((str(file.relative_to(pkg_base)), str(file), 'DATA'))
|
||||||
dest_file = os.path.join(dest_folder, f)
|
|
||||||
data_toc.append((dest_file, source_file, 'DATA'))
|
|
||||||
return data_toc
|
return data_toc
|
||||||
|
|
||||||
|
|
||||||
def collect_local_submodules(package):
|
def collect_local_submodules(package: str):
|
||||||
"""
|
"""
|
||||||
Collect all local submodules from the given package.
|
Collect all local submodules from the given package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
base_dir = '..'
|
from pathlib import Path
|
||||||
package_dir = os.path.join(base_dir, package.replace('.', os.sep))
|
package_dir = Path(package.replace('.', os.sep))
|
||||||
submodules = []
|
submodules = [package]
|
||||||
for dir_path, dir_names, files in os.walk(package_dir):
|
# Walk through all file in the given package, looking for data files.
|
||||||
for f in files:
|
for file in package_dir.rglob('*.py'):
|
||||||
if f == '__init__.py':
|
if file.name == '__init__.py':
|
||||||
submodules.append(f"{package}.{os.path.basename(dir_path)}")
|
module = f"{file.parent}".replace(os.sep, '.')
|
||||||
elif f.endswith('.py'):
|
else:
|
||||||
submodules.append(f"{package}.{os.path.basename(dir_path)}.{os.path.splitext(f)[0]}")
|
module = f"{file.parent}.{file.stem}".replace(os.sep, '.')
|
||||||
for d in dir_names:
|
if module not in submodules:
|
||||||
submodules.append(f"{package}.{os.path.basename(dir_path)}.{d}")
|
submodules.append(module)
|
||||||
return submodules
|
return submodules
|
||||||
|
|
||||||
|
|
||||||
@@ -50,8 +51,7 @@ hiddenimports = [
|
|||||||
'passlib.handlers.bcrypt',
|
'passlib.handlers.bcrypt',
|
||||||
'app.modules',
|
'app.modules',
|
||||||
'app.plugins',
|
'app.plugins',
|
||||||
] + collect_local_submodules('app.modules') \
|
] + collect_local_submodules('app.modules') + collect_local_submodules('app.plugins')
|
||||||
+ collect_local_submodules('app.plugins')
|
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
|
|
||||||
@@ -75,8 +75,9 @@ exe = EXE(
|
|||||||
a.scripts,
|
a.scripts,
|
||||||
a.binaries,
|
a.binaries,
|
||||||
a.zipfiles,
|
a.zipfiles,
|
||||||
a.datas,
|
a.datas + [('./app.ico', './app.ico', 'DATA')],
|
||||||
collect_pkg_data('config'),
|
collect_pkg_data('config'),
|
||||||
|
collect_pkg_data('nginx'),
|
||||||
collect_pkg_data('cf_clearance'),
|
collect_pkg_data('cf_clearance'),
|
||||||
collect_pkg_data('database', include_py_files=True),
|
collect_pkg_data('database', include_py_files=True),
|
||||||
[],
|
[],
|
||||||
@@ -87,7 +88,7 @@ exe = EXE(
|
|||||||
upx=True,
|
upx=True,
|
||||||
upx_exclude=[],
|
upx_exclude=[],
|
||||||
runtime_tmpdir=None,
|
runtime_tmpdir=None,
|
||||||
console=True,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
argv_emulation=False,
|
argv_emulation=False,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
|
|||||||
Reference in New Issue
Block a user