mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-12 07:39:40 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f96a562d4 | ||
|
|
cefbd70469 | ||
|
|
30c9c66087 | ||
|
|
1ecbc2f0be | ||
|
|
884a0feb62 | ||
|
|
5f44f07515 | ||
|
|
a902b79684 | ||
|
|
4e13f59b36 | ||
|
|
cbccac87f0 | ||
|
|
eb3c09a3d3 | ||
|
|
2a9a36ac88 | ||
|
|
af2f52a050 | ||
|
|
7a61fa1ee2 | ||
|
|
ac3009d58f | ||
|
|
e835feb056 | ||
|
|
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 |
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -77,6 +77,26 @@ jobs:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
shell: pwsh
|
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
|
- name: Pyinstaller
|
||||||
run: |
|
run: |
|
||||||
pyinstaller windows.spec
|
pyinstaller windows.spec
|
||||||
|
|||||||
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`
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Response
|
from fastapi import APIRouter, Depends, Response
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.douban import DoubanChain
|
from app.chain.douban import DoubanChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.db import get_db
|
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
|
||||||
@@ -32,13 +30,12 @@ def douban_img(imgurl: str) -> Any:
|
|||||||
|
|
||||||
@router.get("/recognize/{doubanid}", summary="豆瓣ID识别", response_model=schemas.Context)
|
@router.get("/recognize/{doubanid}", summary="豆瓣ID识别", response_model=schemas.Context)
|
||||||
def recognize_doubanid(doubanid: str,
|
def recognize_doubanid(doubanid: str,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据豆瓣ID识别媒体信息
|
根据豆瓣ID识别媒体信息
|
||||||
"""
|
"""
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
context = DoubanChain(db).recognize_by_doubanid(doubanid=doubanid)
|
context = DoubanChain().recognize_by_doubanid(doubanid=doubanid)
|
||||||
if context:
|
if context:
|
||||||
return context.to_dict()
|
return context.to_dict()
|
||||||
else:
|
else:
|
||||||
@@ -48,12 +45,11 @@ def recognize_doubanid(doubanid: str,
|
|||||||
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
||||||
def movie_showing(page: int = 1,
|
def movie_showing(page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣正在热映
|
浏览豆瓣正在热映
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain(db).movie_showing(page=page, count=count)
|
movies = DoubanChain().movie_showing(page=page, count=count)
|
||||||
if not movies:
|
if not movies:
|
||||||
return []
|
return []
|
||||||
medias = [MediaInfo(douban_info=movie) for movie in movies]
|
medias = [MediaInfo(douban_info=movie) for movie in movies]
|
||||||
@@ -65,13 +61,12 @@ def douban_movies(sort: str = "R",
|
|||||||
tags: str = "",
|
tags: str = "",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣电影信息
|
浏览豆瓣电影信息
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain(db).douban_discover(mtype=MediaType.MOVIE,
|
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
|
||||||
sort=sort, tags=tags, page=page, count=count)
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
if not movies:
|
if not movies:
|
||||||
return []
|
return []
|
||||||
medias = [MediaInfo(douban_info=movie) for movie in movies]
|
medias = [MediaInfo(douban_info=movie) for movie in movies]
|
||||||
@@ -86,13 +81,12 @@ def douban_tvs(sort: str = "R",
|
|||||||
tags: str = "",
|
tags: str = "",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain(db).douban_discover(mtype=MediaType.TV,
|
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
|
||||||
sort=sort, tags=tags, page=page, count=count)
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
if not tvs:
|
if not tvs:
|
||||||
return []
|
return []
|
||||||
medias = [MediaInfo(douban_info=tv) for tv in tvs]
|
medias = [MediaInfo(douban_info=tv) for tv in tvs]
|
||||||
@@ -106,47 +100,54 @@ def douban_tvs(sort: str = "R",
|
|||||||
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
||||||
def movie_top250(page: int = 1,
|
def movie_top250(page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
movies = DoubanChain(db).movie_top250(page=page, count=count)
|
movies = DoubanChain().movie_top250(page=page, count=count)
|
||||||
return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
|
return [MediaInfo(douban_info=movie).to_dict() for movie in movies]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
def tv_weekly_chinese(page: int = 1,
|
def tv_weekly_chinese(page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
中国每周剧集口碑榜
|
中国每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain(db).tv_weekly_chinese(page=page, count=count)
|
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count)
|
||||||
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
def tv_weekly_global(page: int = 1,
|
def tv_weekly_global(page: int = 1,
|
||||||
count: int = 30,
|
count: int = 30,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
全球每周剧集口碑榜
|
全球每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
tvs = DoubanChain(db).tv_weekly_global(page=page, count=count)
|
tvs = DoubanChain().tv_weekly_global(page=page, count=count)
|
||||||
|
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def tv_animation(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
热门动画剧集
|
||||||
|
"""
|
||||||
|
tvs = DoubanChain().tv_animation(page=page, count=count)
|
||||||
return [MediaInfo(douban_info=tv).to_dict() for tv in tvs]
|
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),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据豆瓣ID查询豆瓣媒体信息
|
根据豆瓣ID查询豆瓣媒体信息
|
||||||
"""
|
"""
|
||||||
doubaninfo = DoubanChain(db).douban_info(doubanid=doubanid)
|
doubaninfo = DoubanChain().douban_info(doubanid=doubanid)
|
||||||
if doubaninfo:
|
if doubaninfo:
|
||||||
return MediaInfo(douban_info=doubaninfo).to_dict()
|
return MediaInfo(douban_info=doubaninfo).to_dict()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ def exists(media_in: schemas.MediaInfo,
|
|||||||
if media_in.tmdb_id:
|
if media_in.tmdb_id:
|
||||||
mediainfo.from_dict(media_in.dict())
|
mediainfo.from_dict(media_in.dict())
|
||||||
elif media_in.douban_id:
|
elif media_in.douban_id:
|
||||||
context = DoubanChain(db).recognize_by_doubanid(doubanid=media_in.douban_id)
|
context = DoubanChain().recognize_by_doubanid(doubanid=media_in.douban_id)
|
||||||
if context:
|
if context:
|
||||||
mediainfo = context.media_info
|
mediainfo = context.media_info
|
||||||
meta = context.meta_info
|
meta = context.meta_info
|
||||||
else:
|
else:
|
||||||
context = MediaChain(db).recognize_by_title(title=f"{media_in.title} {media_in.year}")
|
context = MediaChain().recognize_by_title(title=f"{media_in.title} {media_in.year}")
|
||||||
if context:
|
if context:
|
||||||
mediainfo = context.media_info
|
mediainfo = context.media_info
|
||||||
meta = context.meta_info
|
meta = context.meta_info
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -74,11 +74,11 @@ def bing_wallpaper() -> Any:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/tmdb", summary="TMDB电影海报", response_model=schemas.Response)
|
@router.get("/tmdb", summary="TMDB电影海报", response_model=schemas.Response)
|
||||||
def tmdb_wallpaper(db: Session = Depends(get_db)) -> Any:
|
def tmdb_wallpaper() -> Any:
|
||||||
"""
|
"""
|
||||||
获取TMDB电影海报
|
获取TMDB电影海报
|
||||||
"""
|
"""
|
||||||
wallpager = TmdbChain(db).get_random_wallpager()
|
wallpager = TmdbChain().get_random_wallpager()
|
||||||
if wallpager:
|
if wallpager:
|
||||||
return schemas.Response(
|
return schemas.Response(
|
||||||
success=True,
|
success=True,
|
||||||
|
|||||||
@@ -20,13 +20,12 @@ router = APIRouter()
|
|||||||
@router.get("/recognize", summary="识别媒体信息(种子)", response_model=schemas.Context)
|
@router.get("/recognize", summary="识别媒体信息(种子)", response_model=schemas.Context)
|
||||||
def recognize(title: str,
|
def recognize(title: str,
|
||||||
subtitle: str = None,
|
subtitle: str = None,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据标题、副标题识别媒体信息
|
根据标题、副标题识别媒体信息
|
||||||
"""
|
"""
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
context = MediaChain(db).recognize_by_title(title=title, subtitle=subtitle)
|
context = MediaChain().recognize_by_title(title=title, subtitle=subtitle)
|
||||||
if context:
|
if context:
|
||||||
return context.to_dict()
|
return context.to_dict()
|
||||||
return schemas.Context()
|
return schemas.Context()
|
||||||
@@ -34,13 +33,12 @@ def recognize(title: str,
|
|||||||
|
|
||||||
@router.get("/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context)
|
@router.get("/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context)
|
||||||
def recognize(path: str,
|
def recognize(path: str,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据文件路径识别媒体信息
|
根据文件路径识别媒体信息
|
||||||
"""
|
"""
|
||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
context = MediaChain(db).recognize_by_path(path)
|
context = MediaChain().recognize_by_path(path)
|
||||||
if context:
|
if context:
|
||||||
return context.to_dict()
|
return context.to_dict()
|
||||||
return schemas.Context()
|
return schemas.Context()
|
||||||
@@ -50,12 +48,11 @@ def recognize(path: str,
|
|||||||
def search_by_title(title: str,
|
def search_by_title(title: str,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
count: int = 8,
|
count: int = 8,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
模糊搜索媒体信息列表
|
模糊搜索媒体信息列表
|
||||||
"""
|
"""
|
||||||
_, medias = MediaChain(db).search(title=title)
|
_, medias = MediaChain().search(title=title)
|
||||||
if medias:
|
if medias:
|
||||||
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
||||||
return []
|
return []
|
||||||
@@ -85,21 +82,20 @@ def exists(title: str = None,
|
|||||||
|
|
||||||
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
|
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
|
||||||
def tmdb_info(mediaid: str, type_name: str,
|
def tmdb_info(mediaid: str, type_name: str,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据媒体ID查询themoviedb或豆瓣媒体信息,type_name: 电影/电视剧
|
根据媒体ID查询themoviedb或豆瓣媒体信息,type_name: 电影/电视剧
|
||||||
"""
|
"""
|
||||||
mtype = MediaType(type_name)
|
mtype = MediaType(type_name)
|
||||||
if mediaid.startswith("tmdb:"):
|
if mediaid.startswith("tmdb:"):
|
||||||
result = TmdbChain(db).tmdb_info(int(mediaid[5:]), mtype)
|
result = TmdbChain().tmdb_info(int(mediaid[5:]), mtype)
|
||||||
return MediaInfo(tmdb_info=result).to_dict()
|
return MediaInfo(tmdb_info=result).to_dict()
|
||||||
elif mediaid.startswith("douban:"):
|
elif mediaid.startswith("douban:"):
|
||||||
# 查询豆瓣信息
|
# 查询豆瓣信息
|
||||||
doubaninfo = DoubanChain(db).douban_info(doubanid=mediaid[7:])
|
doubaninfo = DoubanChain().douban_info(doubanid=mediaid[7:])
|
||||||
if not doubaninfo:
|
if not doubaninfo:
|
||||||
return schemas.MediaInfo()
|
return schemas.MediaInfo()
|
||||||
result = DoubanChain(db).recognize_by_doubaninfo(doubaninfo)
|
result = DoubanChain().recognize_by_doubaninfo(doubaninfo)
|
||||||
if result:
|
if result:
|
||||||
# TMDB
|
# TMDB
|
||||||
return result.media_info.to_dict()
|
return result.media_info.to_dict()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def search_by_tmdbid(mediaid: str,
|
|||||||
elif mediaid.startswith("douban:"):
|
elif mediaid.startswith("douban:"):
|
||||||
doubanid = mediaid.replace("douban:", "")
|
doubanid = mediaid.replace("douban:", "")
|
||||||
# 识别豆瓣信息
|
# 识别豆瓣信息
|
||||||
context = DoubanChain(db).recognize_by_doubanid(doubanid)
|
context = DoubanChain().recognize_by_doubanid(doubanid)
|
||||||
if not context or not context.media_info or not context.media_info.tmdb_id:
|
if not context or not context.media_info or not context.media_info.tmdb_id:
|
||||||
return []
|
return []
|
||||||
torrents = SearchChain(db).search_by_tmdbid(tmdbid=context.media_info.tmdb_id,
|
torrents = SearchChain(db).search_by_tmdbid(tmdbid=context.media_info.tmdb_id,
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ def execute_command(jobid: str,
|
|||||||
if not jobid:
|
if not jobid:
|
||||||
return schemas.Response(success=False, message="命令不能为空!")
|
return schemas.Response(success=False, message="命令不能为空!")
|
||||||
if jobid == "subscribe_search":
|
if jobid == "subscribe_search":
|
||||||
Scheduler().start(jobid, state = 'R')
|
Scheduler().start(jobid, state='R')
|
||||||
else:
|
else:
|
||||||
Scheduler().start(jobid)
|
Scheduler().start(jobid)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.tmdb import TmdbChain
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.db import get_db
|
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/seasons/{tmdbid}", summary="TMDB所有季", response_model=List[schemas.TmdbSeason])
|
@router.get("/seasons/{tmdbid}", summary="TMDB所有季", response_model=List[schemas.TmdbSeason])
|
||||||
def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
|
def tmdb_seasons(tmdbid: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
"""
|
||||||
根据TMDBID查询themoviedb所有季信息
|
根据TMDBID查询themoviedb所有季信息
|
||||||
"""
|
"""
|
||||||
seasons_info = TmdbChain(db).tmdb_seasons(tmdbid=tmdbid)
|
seasons_info = TmdbChain().tmdb_seasons(tmdbid=tmdbid)
|
||||||
if not seasons_info:
|
if not seasons_info:
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
@@ -29,16 +26,15 @@ def tmdb_seasons(tmdbid: int, db: Session = Depends(get_db),
|
|||||||
@router.get("/similar/{tmdbid}/{type_name}", summary="类似电影/电视剧", response_model=List[schemas.MediaInfo])
|
@router.get("/similar/{tmdbid}/{type_name}", summary="类似电影/电视剧", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_similar(tmdbid: int,
|
def tmdb_similar(tmdbid: int,
|
||||||
type_name: str,
|
type_name: str,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据TMDBID查询类似电影/电视剧,type_name: 电影/电视剧
|
根据TMDBID查询类似电影/电视剧,type_name: 电影/电视剧
|
||||||
"""
|
"""
|
||||||
mediatype = MediaType(type_name)
|
mediatype = MediaType(type_name)
|
||||||
if mediatype == MediaType.MOVIE:
|
if mediatype == MediaType.MOVIE:
|
||||||
tmdbinfos = TmdbChain(db).movie_similar(tmdbid=tmdbid)
|
tmdbinfos = TmdbChain().movie_similar(tmdbid=tmdbid)
|
||||||
elif mediatype == MediaType.TV:
|
elif mediatype == MediaType.TV:
|
||||||
tmdbinfos = TmdbChain(db).tv_similar(tmdbid=tmdbid)
|
tmdbinfos = TmdbChain().tv_similar(tmdbid=tmdbid)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
if not tmdbinfos:
|
if not tmdbinfos:
|
||||||
@@ -50,16 +46,15 @@ def tmdb_similar(tmdbid: int,
|
|||||||
@router.get("/recommend/{tmdbid}/{type_name}", summary="推荐电影/电视剧", response_model=List[schemas.MediaInfo])
|
@router.get("/recommend/{tmdbid}/{type_name}", summary="推荐电影/电视剧", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_recommend(tmdbid: int,
|
def tmdb_recommend(tmdbid: int,
|
||||||
type_name: str,
|
type_name: str,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据TMDBID查询推荐电影/电视剧,type_name: 电影/电视剧
|
根据TMDBID查询推荐电影/电视剧,type_name: 电影/电视剧
|
||||||
"""
|
"""
|
||||||
mediatype = MediaType(type_name)
|
mediatype = MediaType(type_name)
|
||||||
if mediatype == MediaType.MOVIE:
|
if mediatype == MediaType.MOVIE:
|
||||||
tmdbinfos = TmdbChain(db).movie_recommend(tmdbid=tmdbid)
|
tmdbinfos = TmdbChain().movie_recommend(tmdbid=tmdbid)
|
||||||
elif mediatype == MediaType.TV:
|
elif mediatype == MediaType.TV:
|
||||||
tmdbinfos = TmdbChain(db).tv_recommend(tmdbid=tmdbid)
|
tmdbinfos = TmdbChain().tv_recommend(tmdbid=tmdbid)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
if not tmdbinfos:
|
if not tmdbinfos:
|
||||||
@@ -72,16 +67,15 @@ def tmdb_recommend(tmdbid: int,
|
|||||||
def tmdb_credits(tmdbid: int,
|
def tmdb_credits(tmdbid: int,
|
||||||
type_name: str,
|
type_name: str,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据TMDBID查询演员阵容,type_name: 电影/电视剧
|
根据TMDBID查询演员阵容,type_name: 电影/电视剧
|
||||||
"""
|
"""
|
||||||
mediatype = MediaType(type_name)
|
mediatype = MediaType(type_name)
|
||||||
if mediatype == MediaType.MOVIE:
|
if mediatype == MediaType.MOVIE:
|
||||||
tmdbinfos = TmdbChain(db).movie_credits(tmdbid=tmdbid, page=page)
|
tmdbinfos = TmdbChain().movie_credits(tmdbid=tmdbid, page=page)
|
||||||
elif mediatype == MediaType.TV:
|
elif mediatype == MediaType.TV:
|
||||||
tmdbinfos = TmdbChain(db).tv_credits(tmdbid=tmdbid, page=page)
|
tmdbinfos = TmdbChain().tv_credits(tmdbid=tmdbid, page=page)
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
if not tmdbinfos:
|
if not tmdbinfos:
|
||||||
@@ -92,12 +86,11 @@ def tmdb_credits(tmdbid: int,
|
|||||||
|
|
||||||
@router.get("/person/{person_id}", summary="人物详情", response_model=schemas.TmdbPerson)
|
@router.get("/person/{person_id}", summary="人物详情", response_model=schemas.TmdbPerson)
|
||||||
def tmdb_person(person_id: int,
|
def tmdb_person(person_id: int,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据人物ID查询人物详情
|
根据人物ID查询人物详情
|
||||||
"""
|
"""
|
||||||
tmdbinfo = TmdbChain(db).person_detail(person_id=person_id)
|
tmdbinfo = TmdbChain().person_detail(person_id=person_id)
|
||||||
if not tmdbinfo:
|
if not tmdbinfo:
|
||||||
return schemas.TmdbPerson()
|
return schemas.TmdbPerson()
|
||||||
else:
|
else:
|
||||||
@@ -107,12 +100,11 @@ def tmdb_person(person_id: int,
|
|||||||
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
|
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_person_credits(person_id: int,
|
def tmdb_person_credits(person_id: int,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据人物ID查询人物参演作品
|
根据人物ID查询人物参演作品
|
||||||
"""
|
"""
|
||||||
tmdbinfo = TmdbChain(db).person_credits(person_id=person_id, page=page)
|
tmdbinfo = TmdbChain().person_credits(person_id=person_id, page=page)
|
||||||
if not tmdbinfo:
|
if not tmdbinfo:
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
@@ -124,16 +116,15 @@ def tmdb_movies(sort_by: str = "popularity.desc",
|
|||||||
with_genres: str = "",
|
with_genres: str = "",
|
||||||
with_original_language: str = "",
|
with_original_language: str = "",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览TMDB电影信息
|
浏览TMDB电影信息
|
||||||
"""
|
"""
|
||||||
movies = TmdbChain(db).tmdb_discover(mtype=MediaType.MOVIE,
|
movies = TmdbChain().tmdb_discover(mtype=MediaType.MOVIE,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
page=page)
|
page=page)
|
||||||
if not movies:
|
if not movies:
|
||||||
return []
|
return []
|
||||||
return [MediaInfo(tmdb_info=movie).to_dict() for movie in movies]
|
return [MediaInfo(tmdb_info=movie).to_dict() for movie in movies]
|
||||||
@@ -144,16 +135,15 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
|
|||||||
with_genres: str = "",
|
with_genres: str = "",
|
||||||
with_original_language: str = "",
|
with_original_language: str = "",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
浏览TMDB剧集信息
|
||||||
"""
|
"""
|
||||||
tvs = TmdbChain(db).tmdb_discover(mtype=MediaType.TV,
|
tvs = TmdbChain().tmdb_discover(mtype=MediaType.TV,
|
||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
page=page)
|
page=page)
|
||||||
if not tvs:
|
if not tvs:
|
||||||
return []
|
return []
|
||||||
return [MediaInfo(tmdb_info=tv).to_dict() for tv in tvs]
|
return [MediaInfo(tmdb_info=tv).to_dict() for tv in tvs]
|
||||||
@@ -161,12 +151,11 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
|
|||||||
|
|
||||||
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_trending(page: int = 1,
|
def tmdb_trending(page: int = 1,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
浏览TMDB剧集信息
|
||||||
"""
|
"""
|
||||||
infos = TmdbChain(db).tmdb_trending(page=page)
|
infos = TmdbChain().tmdb_trending(page=page)
|
||||||
if not infos:
|
if not infos:
|
||||||
return []
|
return []
|
||||||
return [MediaInfo(tmdb_info=info).to_dict() for info in infos]
|
return [MediaInfo(tmdb_info=info).to_dict() for info in infos]
|
||||||
@@ -174,12 +163,11 @@ def tmdb_trending(page: int = 1,
|
|||||||
|
|
||||||
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
||||||
def tmdb_season_episodes(tmdbid: int, season: int,
|
def tmdb_season_episodes(tmdbid: int, season: int,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据TMDBID查询某季的所有信信息
|
根据TMDBID查询某季的所有信信息
|
||||||
"""
|
"""
|
||||||
episodes_info = TmdbChain(db).tmdb_episodes(tmdbid=tmdbid, season=season)
|
episodes_info = TmdbChain().tmdb_episodes(tmdbid=tmdbid, season=season)
|
||||||
if not episodes_info:
|
if not episodes_info:
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -301,11 +301,11 @@ def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)) -> A
|
|||||||
)
|
)
|
||||||
tmdbid = term.replace("tmdb:", "")
|
tmdbid = term.replace("tmdb:", "")
|
||||||
# 查询媒体信息
|
# 查询媒体信息
|
||||||
mediainfo = MediaChain(db).recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
|
mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
return [RadarrMovie()]
|
return [RadarrMovie()]
|
||||||
# 查询是否已存在
|
# 查询是否已存在
|
||||||
exists = MediaChain(db).media_exists(mediainfo=mediainfo)
|
exists = MediaChain().media_exists(mediainfo=mediainfo)
|
||||||
if not exists:
|
if not exists:
|
||||||
# 文件不存在
|
# 文件不存在
|
||||||
hasfile = False
|
hasfile = False
|
||||||
@@ -581,7 +581,7 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
|
|||||||
|
|
||||||
# 获取TVDBID
|
# 获取TVDBID
|
||||||
if not term.startswith("tvdb:"):
|
if not term.startswith("tvdb:"):
|
||||||
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(term),
|
mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
|
||||||
mtype=MediaType.TV)
|
mtype=MediaType.TV)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
return [SonarrSeries()]
|
return [SonarrSeries()]
|
||||||
@@ -593,7 +593,7 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
|
|||||||
tvdbid = int(term.replace("tvdb:", ""))
|
tvdbid = int(term.replace("tvdb:", ""))
|
||||||
|
|
||||||
# 查询TVDB信息
|
# 查询TVDB信息
|
||||||
tvdbinfo = MediaChain(db).tvdb_info(tvdbid=tvdbid)
|
tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
|
||||||
if not tvdbinfo:
|
if not tvdbinfo:
|
||||||
return [SonarrSeries()]
|
return [SonarrSeries()]
|
||||||
|
|
||||||
@@ -605,11 +605,11 @@ def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db)) ->
|
|||||||
|
|
||||||
# 根据TVDB查询媒体信息
|
# 根据TVDB查询媒体信息
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
mediainfo = MediaChain(db).recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
|
mediainfo = MediaChain().recognize_media(meta=MetaInfo(tvdbinfo.get('seriesName')),
|
||||||
mtype=MediaType.TV)
|
mtype=MediaType.TV)
|
||||||
|
|
||||||
# 查询是否存在
|
# 查询是否存在
|
||||||
exists = MediaChain(db).media_exists(mediainfo)
|
exists = MediaChain().media_exists(mediainfo)
|
||||||
if exists:
|
if exists:
|
||||||
hasfile = True
|
hasfile = True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -115,16 +115,18 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
|
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
|
||||||
|
|
||||||
def match_doubaninfo(self, name: str, mtype: str = None,
|
def match_doubaninfo(self, name: str, imdbid: str = None,
|
||||||
year: str = None, season: int = None) -> Optional[dict]:
|
mtype: str = None, year: str = None, season: int = None) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
搜索和匹配豆瓣信息
|
搜索和匹配豆瓣信息
|
||||||
:param name: 标题
|
:param name: 标题
|
||||||
|
:param imdbid: imdbid
|
||||||
:param mtype: 类型
|
:param mtype: 类型
|
||||||
:param year: 年份
|
:param year: 年份
|
||||||
:param season: 季
|
:param season: 季
|
||||||
"""
|
"""
|
||||||
return self.run_module("match_doubaninfo", name=name, mtype=mtype, year=year, season=season)
|
return self.run_module("match_doubaninfo", name=name, imdbid=imdbid,
|
||||||
|
mtype=mtype, year=year, season=season)
|
||||||
|
|
||||||
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ from app.core.context import MediaInfo
|
|||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class DoubanChain(ChainBase):
|
class DoubanChain(ChainBase, metaclass=Singleton):
|
||||||
"""
|
"""
|
||||||
豆瓣处理链
|
豆瓣处理链,单例运行
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
|
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
|
||||||
@@ -29,18 +30,32 @@ class DoubanChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
根据豆瓣信息识别媒体信息
|
根据豆瓣信息识别媒体信息
|
||||||
"""
|
"""
|
||||||
# 使用原标题匹配
|
# 优先使用原标题匹配
|
||||||
meta = MetaInfo(title=doubaninfo.get("original_title") or doubaninfo.get("title"))
|
season_meta = None
|
||||||
|
if doubaninfo.get("original_title"):
|
||||||
|
meta = MetaInfo(title=doubaninfo.get("original_title"))
|
||||||
|
season_meta = MetaInfo(title=doubaninfo.get("title"))
|
||||||
|
# 合并季
|
||||||
|
meta.begin_season = season_meta.begin_season
|
||||||
|
else:
|
||||||
|
meta = MetaInfo(title=doubaninfo.get("title"))
|
||||||
|
# 年份
|
||||||
|
if doubaninfo.get("year"):
|
||||||
|
meta.year = doubaninfo.get("year")
|
||||||
# 处理类型
|
# 处理类型
|
||||||
if isinstance(doubaninfo.get('media_type'), MediaType):
|
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媒体信息')
|
if season_meta and season_meta.name != meta.name:
|
||||||
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
|
# 使用主标题识别媒体信息
|
||||||
|
mediainfo = self.recognize_media(meta=season_meta, mtype=season_meta.type)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f'{meta.name} 未识别到TMDB媒体信息')
|
||||||
|
return Context(meta_info=meta, media_info=MediaInfo(douban_info=doubaninfo))
|
||||||
logger.info(f'识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year} {meta.season}')
|
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,18 +1,31 @@
|
|||||||
|
import copy
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from threading import Lock
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.core.context import Context, MediaInfo
|
from app.core.context import Context, MediaInfo
|
||||||
|
from app.core.event import eventmanager, Event
|
||||||
from app.core.meta import MetaBase
|
from app.core.meta import MetaBase
|
||||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
from app.schemas.types import EventType, MediaType
|
||||||
|
from app.utils.singleton import Singleton
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
class MediaChain(ChainBase):
|
recognize_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
class MediaChain(ChainBase, metaclass=Singleton):
|
||||||
"""
|
"""
|
||||||
媒体信息处理链
|
媒体信息处理链,单例运行
|
||||||
"""
|
"""
|
||||||
|
# 临时识别标题
|
||||||
|
recognize_title: Optional[str] = None
|
||||||
|
# 临时识别结果 {title, name, year, season, episode}
|
||||||
|
recognize_temp: Optional[dict] = None
|
||||||
|
|
||||||
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
|
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
|
||||||
"""
|
"""
|
||||||
@@ -24,14 +37,104 @@ class MediaChain(ChainBase):
|
|||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
|
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f'{title} 未识别到媒体信息')
|
# 偿试使用辅助识别,如果有注册响应事件的话
|
||||||
return Context(meta_info=metainfo)
|
if eventmanager.check(EventType.NameRecognize):
|
||||||
|
logger.info(f'请求辅助识别,标题:{title} ...')
|
||||||
|
mediainfo = self.recognize_help(title=title, org_meta=metainfo)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f'{title} 未识别到媒体信息')
|
||||||
|
return Context(meta_info=metainfo)
|
||||||
|
# 识别成功
|
||||||
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
|
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
|
||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
# 返回上下文
|
# 返回上下文
|
||||||
return Context(meta_info=metainfo, media_info=mediainfo)
|
return Context(meta_info=metainfo, media_info=mediainfo)
|
||||||
|
|
||||||
|
def recognize_help(self, title: str, org_meta: MetaBase) -> Optional[MediaInfo]:
|
||||||
|
"""
|
||||||
|
请求辅助识别,返回媒体信息
|
||||||
|
:param title: 标题
|
||||||
|
:param org_meta: 原始元数据
|
||||||
|
"""
|
||||||
|
with recognize_lock:
|
||||||
|
self.recognize_temp = None
|
||||||
|
self.recognize_title = title
|
||||||
|
|
||||||
|
# 发送请求事件
|
||||||
|
eventmanager.send_event(
|
||||||
|
EventType.NameRecognize,
|
||||||
|
{
|
||||||
|
'title': title,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 每0.5秒循环一次,等待结果,直到10秒后超时
|
||||||
|
for i in range(10):
|
||||||
|
if self.recognize_temp is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
# 加锁
|
||||||
|
with recognize_lock:
|
||||||
|
mediainfo = None
|
||||||
|
if not self.recognize_temp or self.recognize_title != title:
|
||||||
|
# 没有识别结果或者识别标题已改变
|
||||||
|
return None
|
||||||
|
# 有识别结果
|
||||||
|
meta_dict = copy.deepcopy(self.recognize_temp)
|
||||||
|
logger.info(f'获取到辅助识别结果:{meta_dict}')
|
||||||
|
if meta_dict.get("name") == org_meta.name and meta_dict.get("year") == org_meta.year:
|
||||||
|
logger.info(f'辅助识别结果与原始识别结果一致')
|
||||||
|
else:
|
||||||
|
logger.info(f'辅助识别结果与原始识别结果不一致,重新匹配媒体信息 ...')
|
||||||
|
org_meta.name = meta_dict.get("name")
|
||||||
|
org_meta.year = meta_dict.get("year")
|
||||||
|
org_meta.begin_season = meta_dict.get("season")
|
||||||
|
org_meta.begin_episode = meta_dict.get("episode")
|
||||||
|
if org_meta.begin_season or org_meta.begin_episode:
|
||||||
|
org_meta.type = MediaType.TV
|
||||||
|
# 重新识别
|
||||||
|
mediainfo = self.recognize_media(meta=org_meta)
|
||||||
|
return mediainfo
|
||||||
|
|
||||||
|
@eventmanager.register(EventType.NameRecognizeResult)
|
||||||
|
def recognize_result(self, event: Event):
|
||||||
|
"""
|
||||||
|
监控识别结果事件,获取辅助识别结果,结果格式:{title, name, year, season, episode}
|
||||||
|
"""
|
||||||
|
if not event:
|
||||||
|
return
|
||||||
|
event_data = event.event_data or {}
|
||||||
|
# 加锁
|
||||||
|
with recognize_lock:
|
||||||
|
# 不是原标题的结果不要
|
||||||
|
if event_data.get("title") != self.recognize_title:
|
||||||
|
return
|
||||||
|
# 标志收到返回
|
||||||
|
self.recognize_temp = {}
|
||||||
|
# 处理数据格式
|
||||||
|
file_title, file_year, season_number, episode_number = None, None, None, None
|
||||||
|
if event_data.get("name"):
|
||||||
|
file_title = str(event_data["name"]).split("/")[0].strip().replace(".", " ")
|
||||||
|
if event_data.get("year"):
|
||||||
|
file_year = str(event_data["year"]).split("/")[0].strip()
|
||||||
|
if event_data.get("season") and str(event_data["season"]).isdigit():
|
||||||
|
season_number = int(event_data["season"])
|
||||||
|
if event_data.get("episode") and str(event_data["episode"]).isdigit():
|
||||||
|
episode_number = int(event_data["episode"])
|
||||||
|
if not file_title:
|
||||||
|
return
|
||||||
|
if file_title == 'Unknown':
|
||||||
|
return
|
||||||
|
if not str(file_year).isdigit():
|
||||||
|
file_year = None
|
||||||
|
# 结果赋值
|
||||||
|
self.recognize_temp = {
|
||||||
|
"name": file_title,
|
||||||
|
"year": file_year,
|
||||||
|
"season": season_number,
|
||||||
|
"episode": episode_number
|
||||||
|
}
|
||||||
|
|
||||||
def recognize_by_path(self, path: str) -> Optional[Context]:
|
def recognize_by_path(self, path: str) -> Optional[Context]:
|
||||||
"""
|
"""
|
||||||
根据文件路径识别媒体信息
|
根据文件路径识别媒体信息
|
||||||
@@ -43,8 +146,13 @@ class MediaChain(ChainBase):
|
|||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
mediainfo = self.recognize_media(meta=file_meta)
|
mediainfo = self.recognize_media(meta=file_meta)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f'{path} 未识别到媒体信息')
|
# 偿试使用辅助识别,如果有注册响应事件的话
|
||||||
return Context(meta_info=file_meta)
|
if eventmanager.check(EventType.NameRecognize):
|
||||||
|
logger.info(f'请求辅助识别,标题:{file_path.name} ...')
|
||||||
|
mediainfo = self.recognize_help(title=path, org_meta=file_meta)
|
||||||
|
if not mediainfo:
|
||||||
|
logger.warn(f'{path} 未识别到媒体信息')
|
||||||
|
return Context(meta_info=file_meta)
|
||||||
logger.info(f'{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
|
logger.info(f'{path} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
|
||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class MessageChain(ChainBase):
|
|||||||
self.downloadchain = DownloadChain(self._db)
|
self.downloadchain = DownloadChain(self._db)
|
||||||
self.subscribechain = SubscribeChain(self._db)
|
self.subscribechain = SubscribeChain(self._db)
|
||||||
self.searchchain = SearchChain(self._db)
|
self.searchchain = SearchChain(self._db)
|
||||||
self.medtachain = MediaChain(self._db)
|
self.medtachain = MediaChain()
|
||||||
self.torrent = TorrentHelper()
|
self.torrent = TorrentHelper()
|
||||||
self.eventmanager = EventManager()
|
self.eventmanager = EventManager()
|
||||||
self.torrenthelper = TorrentHelper()
|
self.torrenthelper = TorrentHelper()
|
||||||
|
|||||||
@@ -342,6 +342,12 @@ class SearchChain(ChainBase):
|
|||||||
include = filter_rule.get("include")
|
include = filter_rule.get("include")
|
||||||
# 排除
|
# 排除
|
||||||
exclude = filter_rule.get("exclude")
|
exclude = filter_rule.get("exclude")
|
||||||
|
# 质量
|
||||||
|
quality = filter_rule.get("quality")
|
||||||
|
# 分辨率
|
||||||
|
resolution = filter_rule.get("resolution")
|
||||||
|
# 特效
|
||||||
|
effect = filter_rule.get("effect")
|
||||||
|
|
||||||
def __filter_torrent(t: TorrentInfo) -> bool:
|
def __filter_torrent(t: TorrentInfo) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -359,6 +365,24 @@ class SearchChain(ChainBase):
|
|||||||
f"{t.title} {t.description}", re.I):
|
f"{t.title} {t.description}", re.I):
|
||||||
logger.info(f"{t.title} 匹配排除规则 {exclude}")
|
logger.info(f"{t.title} 匹配排除规则 {exclude}")
|
||||||
return False
|
return False
|
||||||
|
# 质量
|
||||||
|
if quality:
|
||||||
|
if not re.search(r"%s" % quality, t.title, re.I):
|
||||||
|
logger.info(f"{t.title} 不匹配质量规则 {quality}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 分辨率
|
||||||
|
if resolution:
|
||||||
|
if not re.search(r"%s" % resolution, t.title, re.I):
|
||||||
|
logger.info(f"{t.title} 不匹配分辨率规则 {resolution}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 特效
|
||||||
|
if effect:
|
||||||
|
if not re.search(r"%s" % effect, t.title, re.I):
|
||||||
|
logger.info(f"{t.title} 不匹配特效规则 {effect}")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 使用默认过滤规则再次过滤
|
# 使用默认过滤规则再次过滤
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Dict, List, Optional, Union, Tuple
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
|
from app.chain.douban import DoubanChain
|
||||||
from app.chain.download import DownloadChain
|
from app.chain.download import DownloadChain
|
||||||
from app.chain.search import SearchChain
|
from app.chain.search import SearchChain
|
||||||
from app.chain.torrents import TorrentsChain
|
from app.chain.torrents import TorrentsChain
|
||||||
@@ -50,18 +51,28 @@ class SubscribeChain(ChainBase):
|
|||||||
识别媒体信息并添加订阅
|
识别媒体信息并添加订阅
|
||||||
"""
|
"""
|
||||||
logger.info(f'开始添加订阅,标题:{title} ...')
|
logger.info(f'开始添加订阅,标题:{title} ...')
|
||||||
# 识别元数据
|
metainfo = None
|
||||||
metainfo = MetaInfo(title)
|
mediainfo = None
|
||||||
if year:
|
if not tmdbid and doubanid:
|
||||||
metainfo.year = year
|
# 将豆瓣信息转换为TMDB信息
|
||||||
if mtype:
|
context = DoubanChain().recognize_by_doubanid(doubanid)
|
||||||
metainfo.type = mtype
|
if context:
|
||||||
if season:
|
metainfo = context.meta_info
|
||||||
metainfo.type = MediaType.TV
|
mediainfo = context.media_info
|
||||||
metainfo.begin_season = season
|
else:
|
||||||
# 识别媒体信息
|
# 识别元数据
|
||||||
mediainfo: MediaInfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
|
metainfo = MetaInfo(title)
|
||||||
if not mediainfo:
|
if year:
|
||||||
|
metainfo.year = year
|
||||||
|
if mtype:
|
||||||
|
metainfo.type = mtype
|
||||||
|
if season:
|
||||||
|
metainfo.type = MediaType.TV
|
||||||
|
metainfo.begin_season = season
|
||||||
|
# 识别媒体信息
|
||||||
|
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid)
|
||||||
|
# 识别失败
|
||||||
|
if not mediainfo or not metainfo or not mediainfo.tmdb_id:
|
||||||
logger.warn(f'未识别到媒体信息,标题:{title},tmdbid:{tmdbid}')
|
logger.warn(f'未识别到媒体信息,标题:{title},tmdbid:{tmdbid}')
|
||||||
return None, "未识别到媒体信息"
|
return None, "未识别到媒体信息"
|
||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
@@ -74,8 +85,8 @@ class SubscribeChain(ChainBase):
|
|||||||
if not kwargs.get('total_episode'):
|
if not kwargs.get('total_episode'):
|
||||||
if not mediainfo.seasons:
|
if not mediainfo.seasons:
|
||||||
# 补充媒体信息
|
# 补充媒体信息
|
||||||
mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
|
mediainfo = self.recognize_media(mtype=mediainfo.type,
|
||||||
tmdbid=mediainfo.tmdb_id)
|
tmdbid=mediainfo.tmdb_id)
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.error(f"媒体信息识别失败!")
|
logger.error(f"媒体信息识别失败!")
|
||||||
return None, "媒体信息识别失败"
|
return None, "媒体信息识别失败"
|
||||||
@@ -85,7 +96,7 @@ class SubscribeChain(ChainBase):
|
|||||||
total_episode = len(mediainfo.seasons.get(season) or [])
|
total_episode = len(mediainfo.seasons.get(season) or [])
|
||||||
if not total_episode:
|
if not total_episode:
|
||||||
logger.error(f'未获取到总集数,标题:{title},tmdbid:{tmdbid}')
|
logger.error(f'未获取到总集数,标题:{title},tmdbid:{tmdbid}')
|
||||||
return None, "未获取到总集数"
|
return None, f"未获取到第 {season} 季的总集数"
|
||||||
kwargs.update({
|
kwargs.update({
|
||||||
'total_episode': total_episode
|
'total_episode': total_episode
|
||||||
})
|
})
|
||||||
@@ -176,66 +187,75 @@ class SubscribeChain(ChainBase):
|
|||||||
totals = {
|
totals = {
|
||||||
subscribe.season: subscribe.total_episode
|
subscribe.season: subscribe.total_episode
|
||||||
}
|
}
|
||||||
# 查询缺失的媒体信息
|
# 查询媒体库缺失的媒体信息
|
||||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
|
||||||
meta=meta,
|
meta=meta,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
totals=totals
|
totals=totals
|
||||||
)
|
)
|
||||||
if exist_flag:
|
|
||||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
|
|
||||||
self.subscribeoper.delete(subscribe.id)
|
|
||||||
# 发送通知
|
|
||||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
|
||||||
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
|
||||||
image=mediainfo.get_message_image()))
|
|
||||||
continue
|
|
||||||
# 电视剧订阅
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
# 使用订阅的总集数和开始集数替换no_exists
|
|
||||||
no_exists = self.__get_subscribe_no_exits(
|
|
||||||
no_exists=no_exists,
|
|
||||||
tmdb_id=mediainfo.tmdb_id,
|
|
||||||
begin_season=meta.begin_season,
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode,
|
|
||||||
|
|
||||||
)
|
|
||||||
# 打印缺失集信息
|
|
||||||
if no_exists and no_exists.get(subscribe.tmdbid):
|
|
||||||
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
|
|
||||||
if no_exists_info:
|
|
||||||
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
|
|
||||||
else:
|
else:
|
||||||
# 洗版状态
|
# 洗版状态
|
||||||
|
exist_flag = False
|
||||||
if meta.type == MediaType.TV:
|
if meta.type == MediaType.TV:
|
||||||
no_exists = {
|
no_exists = {
|
||||||
subscribe.season: NotExistMediaInfo(
|
subscribe.tmdbid: {
|
||||||
season=subscribe.season,
|
subscribe.season: NotExistMediaInfo(
|
||||||
episodes=[],
|
season=subscribe.season,
|
||||||
total_episode=subscribe.total_episode,
|
episodes=[],
|
||||||
start_episode=subscribe.start_episode or 1)
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode or 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
no_exists = {}
|
no_exists = {}
|
||||||
|
|
||||||
|
# 已存在
|
||||||
|
if exist_flag:
|
||||||
|
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||||
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 电视剧订阅处理缺失集
|
||||||
|
if meta.type == MediaType.TV:
|
||||||
|
# 使用订阅的总集数和开始集数替换no_exists
|
||||||
|
no_exists = self.__get_subscribe_no_exits(
|
||||||
|
no_exists=no_exists,
|
||||||
|
tmdb_id=mediainfo.tmdb_id,
|
||||||
|
begin_season=meta.begin_season,
|
||||||
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode,
|
||||||
|
|
||||||
|
)
|
||||||
|
# 打印缺失集信息
|
||||||
|
if no_exists and no_exists.get(subscribe.tmdbid):
|
||||||
|
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
|
||||||
|
if no_exists_info:
|
||||||
|
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
|
||||||
|
|
||||||
# 站点范围
|
# 站点范围
|
||||||
if subscribe.sites:
|
if subscribe.sites:
|
||||||
sites = json.loads(subscribe.sites)
|
sites = json.loads(subscribe.sites)
|
||||||
else:
|
else:
|
||||||
sites = None
|
sites = None
|
||||||
|
|
||||||
# 优先级过滤规则
|
# 优先级过滤规则
|
||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||||
else:
|
else:
|
||||||
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
||||||
|
|
||||||
# 默认过滤规则
|
# 默认过滤规则
|
||||||
if subscribe.include or subscribe.exclude:
|
if subscribe.include or subscribe.exclude:
|
||||||
filter_rule = {
|
filter_rule = {
|
||||||
"include": subscribe.include,
|
"include": subscribe.include,
|
||||||
"exclude": subscribe.exclude
|
"exclude": subscribe.exclude,
|
||||||
|
"quality": subscribe.quality,
|
||||||
|
"resolution": subscribe.resolution,
|
||||||
|
"effect": subscribe.effect,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
|
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
|
||||||
|
|
||||||
# 搜索,同时电视剧会过滤掉不需要的剧集
|
# 搜索,同时电视剧会过滤掉不需要的剧集
|
||||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||||
keyword=subscribe.keyword,
|
keyword=subscribe.keyword,
|
||||||
@@ -247,8 +267,10 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
||||||
if meta.type == MediaType.TV:
|
if meta.type == MediaType.TV:
|
||||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||||
|
meta=meta, mediainfo=mediainfo)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 过滤
|
# 过滤
|
||||||
matched_contexts = []
|
matched_contexts = []
|
||||||
for context in contexts:
|
for context in contexts:
|
||||||
@@ -278,8 +300,10 @@ class SubscribeChain(ChainBase):
|
|||||||
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
|
logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源')
|
||||||
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
# 非洗版未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||||
|
meta=meta, mediainfo=mediainfo)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 自动下载
|
# 自动下载
|
||||||
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
downloads, lefts = self.downloadchain.batch_download(contexts=matched_contexts,
|
||||||
no_exists=no_exists)
|
no_exists=no_exists)
|
||||||
@@ -299,8 +323,9 @@ class SubscribeChain(ChainBase):
|
|||||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||||
# 更新订阅剩余集数和时间
|
# 更新订阅剩余集数和时间
|
||||||
update_date = True if downloads else False
|
update_date = True if downloads else False
|
||||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
|
||||||
mediainfo=mediainfo, update_date=update_date)
|
mediainfo=mediainfo, update_date=update_date)
|
||||||
|
|
||||||
# 手动触发时发送系统消息
|
# 手动触发时发送系统消息
|
||||||
if manual:
|
if manual:
|
||||||
if sid:
|
if sid:
|
||||||
@@ -309,19 +334,19 @@ class SubscribeChain(ChainBase):
|
|||||||
self.message.put('所有订阅搜索完成!')
|
self.message.put('所有订阅搜索完成!')
|
||||||
|
|
||||||
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
|
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo,
|
||||||
mediainfo: MediaInfo, downloads: List[Context]):
|
mediainfo: MediaInfo, downloads: List[Context] = None):
|
||||||
"""
|
"""
|
||||||
判断是否应完成订阅
|
判断是否应完成订阅
|
||||||
"""
|
"""
|
||||||
if not subscribe.best_version:
|
if not subscribe.best_version:
|
||||||
# 全部下载完成
|
# 全部下载完成
|
||||||
logger.info(f'{mediainfo.title_year} 下载完成,完成订阅')
|
logger.info(f'{mediainfo.title_year} 完成订阅')
|
||||||
self.subscribeoper.delete(subscribe.id)
|
self.subscribeoper.delete(subscribe.id)
|
||||||
# 发送通知
|
# 发送通知
|
||||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||||
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
||||||
image=mediainfo.get_message_image()))
|
image=mediainfo.get_message_image()))
|
||||||
else:
|
elif downloads:
|
||||||
# 当前下载资源的优先级
|
# 当前下载资源的优先级
|
||||||
priority = max([item.torrent_info.pri_order for item in downloads])
|
priority = max([item.torrent_info.pri_order for item in downloads])
|
||||||
if priority == 100:
|
if priority == 100:
|
||||||
@@ -411,46 +436,50 @@ class SubscribeChain(ChainBase):
|
|||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
totals=totals
|
totals=totals
|
||||||
)
|
)
|
||||||
if exist_flag:
|
|
||||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在,完成订阅')
|
|
||||||
self.subscribeoper.delete(subscribe.id)
|
|
||||||
# 发送通知
|
|
||||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
|
||||||
title=f'{mediainfo.title_year} {meta.season} 已完成订阅',
|
|
||||||
image=mediainfo.get_message_image()))
|
|
||||||
continue
|
|
||||||
# 电视剧订阅
|
|
||||||
if meta.type == MediaType.TV:
|
|
||||||
# 使用订阅的总集数和开始集数替换no_exists
|
|
||||||
no_exists = self.__get_subscribe_no_exits(
|
|
||||||
no_exists=no_exists,
|
|
||||||
tmdb_id=mediainfo.tmdb_id,
|
|
||||||
begin_season=meta.begin_season,
|
|
||||||
total_episode=subscribe.total_episode,
|
|
||||||
start_episode=subscribe.start_episode,
|
|
||||||
|
|
||||||
)
|
|
||||||
# 打印缺失集信息
|
|
||||||
if no_exists and no_exists.get(subscribe.tmdbid):
|
|
||||||
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
|
|
||||||
if no_exists_info:
|
|
||||||
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
|
|
||||||
else:
|
else:
|
||||||
# 洗版
|
# 洗版
|
||||||
|
exist_flag = False
|
||||||
if meta.type == MediaType.TV:
|
if meta.type == MediaType.TV:
|
||||||
no_exists = {
|
no_exists = {
|
||||||
subscribe.season: NotExistMediaInfo(
|
subscribe.tmdbid: {
|
||||||
season=subscribe.season,
|
subscribe.season: NotExistMediaInfo(
|
||||||
episodes=[],
|
season=subscribe.season,
|
||||||
total_episode=subscribe.total_episode,
|
episodes=[],
|
||||||
start_episode=subscribe.start_episode or 1)
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode or 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
no_exists = {}
|
no_exists = {}
|
||||||
|
|
||||||
|
# 已存在
|
||||||
|
if exist_flag:
|
||||||
|
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||||
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 电视剧订阅
|
||||||
|
if meta.type == MediaType.TV:
|
||||||
|
# 使用订阅的总集数和开始集数替换no_exists
|
||||||
|
no_exists = self.__get_subscribe_no_exits(
|
||||||
|
no_exists=no_exists,
|
||||||
|
tmdb_id=mediainfo.tmdb_id,
|
||||||
|
begin_season=meta.begin_season,
|
||||||
|
total_episode=subscribe.total_episode,
|
||||||
|
start_episode=subscribe.start_episode,
|
||||||
|
|
||||||
|
)
|
||||||
|
# 打印缺失集信息
|
||||||
|
if no_exists and no_exists.get(subscribe.tmdbid):
|
||||||
|
no_exists_info = no_exists.get(subscribe.tmdbid).get(subscribe.season)
|
||||||
|
if no_exists_info:
|
||||||
|
logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}')
|
||||||
|
|
||||||
# 默认过滤规则
|
# 默认过滤规则
|
||||||
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
|
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
|
||||||
include = subscribe.include or default_filter.get("include")
|
include = subscribe.include or default_filter.get("include")
|
||||||
exclude = subscribe.exclude or default_filter.get("exclude")
|
exclude = subscribe.exclude or default_filter.get("exclude")
|
||||||
|
|
||||||
# 遍历缓存种子
|
# 遍历缓存种子
|
||||||
_match_context = []
|
_match_context = []
|
||||||
for domain, contexts in torrents.items():
|
for domain, contexts in torrents.items():
|
||||||
@@ -537,6 +566,7 @@ class SubscribeChain(ChainBase):
|
|||||||
# 匹配成功
|
# 匹配成功
|
||||||
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
|
logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}')
|
||||||
_match_context.append(context)
|
_match_context.append(context)
|
||||||
|
|
||||||
# 开始下载
|
# 开始下载
|
||||||
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源')
|
||||||
if _match_context:
|
if _match_context:
|
||||||
@@ -554,12 +584,13 @@ class SubscribeChain(ChainBase):
|
|||||||
if meta.type == MediaType.TV and not subscribe.best_version:
|
if meta.type == MediaType.TV and not subscribe.best_version:
|
||||||
update_date = True if downloads else False
|
update_date = True if downloads else False
|
||||||
# 未完成下载,计算剩余集数
|
# 未完成下载,计算剩余集数
|
||||||
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
|
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, meta=meta,
|
||||||
mediainfo=mediainfo, update_date=update_date)
|
mediainfo=mediainfo, update_date=update_date)
|
||||||
else:
|
else:
|
||||||
if meta.type == MediaType.TV:
|
if meta.type == MediaType.TV:
|
||||||
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
# 未搜索到资源,但本地缺失可能有变化,更新订阅剩余集数
|
||||||
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe, mediainfo=mediainfo)
|
self.__update_lack_episodes(lefts=no_exists, subscribe=subscribe,
|
||||||
|
meta=meta, mediainfo=mediainfo)
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
"""
|
"""
|
||||||
@@ -651,31 +682,36 @@ class SubscribeChain(ChainBase):
|
|||||||
|
|
||||||
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
def __update_lack_episodes(self, lefts: Dict[int, Dict[int, NotExistMediaInfo]],
|
||||||
subscribe: Subscribe,
|
subscribe: Subscribe,
|
||||||
|
meta: MetaBase,
|
||||||
mediainfo: MediaInfo,
|
mediainfo: MediaInfo,
|
||||||
update_date: bool = False):
|
update_date: bool = False):
|
||||||
"""
|
"""
|
||||||
更新订阅剩余集数
|
更新订阅剩余集数
|
||||||
"""
|
"""
|
||||||
left_seasons = lefts.get(mediainfo.tmdb_id) or {}
|
left_seasons = lefts.get(mediainfo.tmdb_id)
|
||||||
for season_info in left_seasons.values():
|
if left_seasons:
|
||||||
season = season_info.season
|
for season_info in left_seasons.values():
|
||||||
if season == subscribe.season:
|
season = season_info.season
|
||||||
left_episodes = season_info.episodes
|
if season == subscribe.season:
|
||||||
if not left_episodes:
|
left_episodes = season_info.episodes
|
||||||
lack_episode = season_info.total_episode
|
if not left_episodes:
|
||||||
else:
|
lack_episode = season_info.total_episode
|
||||||
lack_episode = len(left_episodes)
|
else:
|
||||||
logger.info(f'{mediainfo.title_year} 季 {season} 更新缺失集数为{lack_episode} ...')
|
lack_episode = len(left_episodes)
|
||||||
if update_date:
|
logger.info(f'{mediainfo.title_year} 季 {season} 更新缺失集数为{lack_episode} ...')
|
||||||
# 同时更新最后时间
|
if update_date:
|
||||||
self.subscribeoper.update(subscribe.id, {
|
# 同时更新最后时间
|
||||||
"lack_episode": lack_episode,
|
self.subscribeoper.update(subscribe.id, {
|
||||||
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
"lack_episode": lack_episode,
|
||||||
})
|
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
else:
|
})
|
||||||
self.subscribeoper.update(subscribe.id, {
|
else:
|
||||||
"lack_episode": lack_episode
|
self.subscribeoper.update(subscribe.id, {
|
||||||
})
|
"lack_episode": lack_episode
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# 判断是否应完成订阅
|
||||||
|
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
|
||||||
|
|
||||||
def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
|
def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from app.utils.singleton import Singleton
|
|||||||
|
|
||||||
class TmdbChain(ChainBase, metaclass=Singleton):
|
class TmdbChain(ChainBase, metaclass=Singleton):
|
||||||
"""
|
"""
|
||||||
TheMovieDB处理链
|
TheMovieDB处理链,单例运行
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,
|
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ class TransferChain(ChainBase):
|
|||||||
self.downloadhis = DownloadHistoryOper(self._db)
|
self.downloadhis = DownloadHistoryOper(self._db)
|
||||||
self.transferhis = TransferHistoryOper(self._db)
|
self.transferhis = TransferHistoryOper(self._db)
|
||||||
self.progress = ProgressHelper()
|
self.progress = ProgressHelper()
|
||||||
self.mediachain = MediaChain(self._db)
|
self.mediachain = MediaChain()
|
||||||
self.tmdbchain = TmdbChain(self._db)
|
self.tmdbchain = TmdbChain()
|
||||||
self.systemconfig = SystemConfigOper()
|
self.systemconfig = SystemConfigOper()
|
||||||
|
|
||||||
def process(self) -> bool:
|
def process(self) -> bool:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import importlib
|
||||||
import traceback
|
import traceback
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from typing import Any, Union, Dict
|
from typing import Any, Union, Dict
|
||||||
@@ -175,10 +176,24 @@ class Command(metaclass=Singleton):
|
|||||||
for handler in handlers:
|
for handler in handlers:
|
||||||
try:
|
try:
|
||||||
names = handler.__qualname__.split(".")
|
names = handler.__qualname__.split(".")
|
||||||
if names[0] == "Command":
|
[class_name, method_name] = names
|
||||||
self.command_event(event)
|
if class_name in self.pluginmanager.get_plugin_ids():
|
||||||
|
# 插件事件
|
||||||
|
self.pluginmanager.run_plugin_method(class_name, method_name, event)
|
||||||
else:
|
else:
|
||||||
self.pluginmanager.run_plugin_method(names[0], names[1], event)
|
# 检查全局变量中是否存在
|
||||||
|
if class_name not in globals():
|
||||||
|
# 导入模块,除了插件和Command本身,只有chain能响应事件
|
||||||
|
module = importlib.import_module(
|
||||||
|
f"app.chain.{class_name[:-5].lower()}"
|
||||||
|
)
|
||||||
|
class_obj = getattr(module, class_name)()
|
||||||
|
else:
|
||||||
|
# 通过类名创建类实例
|
||||||
|
class_obj = globals()[class_name]()
|
||||||
|
# 检查类是否存在并调用方法
|
||||||
|
if hasattr(class_obj, method_name):
|
||||||
|
getattr(class_obj, method_name)(event)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
|
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
# 是否开发模式
|
# 是否开发模式
|
||||||
|
|||||||
@@ -10,16 +10,13 @@ class EventManager(metaclass=Singleton):
|
|||||||
事件管理器
|
事件管理器
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 事件队列
|
|
||||||
_eventQueue: Queue = None
|
|
||||||
# 事件响应函数字典
|
|
||||||
_handlers: dict = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 事件队列
|
# 事件队列
|
||||||
self._eventQueue = Queue()
|
self._eventQueue = Queue()
|
||||||
# 事件响应函数字典
|
# 事件响应函数字典
|
||||||
self._handlers = {}
|
self._handlers = {}
|
||||||
|
# 已禁用的事件响应
|
||||||
|
self._disabled_handlers = []
|
||||||
|
|
||||||
def get_event(self):
|
def get_event(self):
|
||||||
"""
|
"""
|
||||||
@@ -27,11 +24,21 @@ class EventManager(metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
event = self._eventQueue.get(block=True, timeout=1)
|
event = self._eventQueue.get(block=True, timeout=1)
|
||||||
handlerList = self._handlers.get(event.event_type)
|
handlerList = self._handlers.get(event.event_type) or []
|
||||||
return event, handlerList or []
|
if handlerList:
|
||||||
|
# 去除掉被禁用的事件响应
|
||||||
|
handlerList = [handler for handler in handlerList
|
||||||
|
if handler.__qualname__.split(".")[0] not in self._disabled_handlers]
|
||||||
|
return event, handlerList
|
||||||
except Empty:
|
except Empty:
|
||||||
return None, []
|
return None, []
|
||||||
|
|
||||||
|
def check(self, etype: EventType):
|
||||||
|
"""
|
||||||
|
检查事件是否存在响应
|
||||||
|
"""
|
||||||
|
return etype.value in self._handlers
|
||||||
|
|
||||||
def add_event_listener(self, etype: EventType, handler: type):
|
def add_event_listener(self, etype: EventType, handler: type):
|
||||||
"""
|
"""
|
||||||
注册事件处理
|
注册事件处理
|
||||||
@@ -45,18 +52,21 @@ class EventManager(metaclass=Singleton):
|
|||||||
handlerList.append(handler)
|
handlerList.append(handler)
|
||||||
logger.debug(f"Event Registed:{etype.value} - {handler}")
|
logger.debug(f"Event Registed:{etype.value} - {handler}")
|
||||||
|
|
||||||
def remove_event_listener(self, etype: EventType, handler: type):
|
def disable_events_hander(self, class_name: str):
|
||||||
"""
|
"""
|
||||||
移除监听器的处理函数
|
标记对应类事件处理为不可用
|
||||||
"""
|
"""
|
||||||
try:
|
if class_name not in self._disabled_handlers:
|
||||||
handlerList = self._handlers[etype.value]
|
self._disabled_handlers.append(class_name)
|
||||||
if handler in handlerList[:]:
|
logger.debug(f"Event Disabled:{class_name}")
|
||||||
handlerList.remove(handler)
|
|
||||||
if not handlerList:
|
def enable_events_hander(self, class_name: str):
|
||||||
del self._handlers[etype.value]
|
"""
|
||||||
except KeyError:
|
标记对应类事件处理为可用
|
||||||
pass
|
"""
|
||||||
|
if class_name in self._disabled_handlers:
|
||||||
|
self._disabled_handlers.remove(class_name)
|
||||||
|
logger.debug(f"Event Enabled:{class_name}")
|
||||||
|
|
||||||
def send_event(self, etype: EventType, data: dict = None):
|
def send_event(self, etype: EventType, data: dict = None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ class MetaBase(object):
|
|||||||
return self.cn_name
|
return self.cn_name
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, name: str):
|
||||||
|
"""
|
||||||
|
设置名称
|
||||||
|
"""
|
||||||
|
if StringUtils.is_all_chinese(name):
|
||||||
|
self.cn_name = name
|
||||||
|
else:
|
||||||
|
self.en_name = name
|
||||||
|
self.cn_name = None
|
||||||
|
|
||||||
def init_subtitle(self, title_text: str):
|
def init_subtitle(self, title_text: str):
|
||||||
"""
|
"""
|
||||||
副标题识别
|
副标题识别
|
||||||
|
|||||||
@@ -61,8 +61,7 @@ class WordsMatcher(metaclass=Singleton):
|
|||||||
|
|
||||||
if state:
|
if state:
|
||||||
appley_words.append(word)
|
appley_words.append(word)
|
||||||
else:
|
|
||||||
logger.debug(f"自定义识别词替换失败:{message}")
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(str(err))
|
print(str(err))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import List, Any, Dict, Tuple
|
from typing import List, Any, Dict, Tuple
|
||||||
|
|
||||||
|
from app.core.event import eventmanager
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.helper.module import ModuleHelper
|
from app.helper.module import ModuleHelper
|
||||||
from app.helper.sites import SitesHelper
|
from app.helper.sites import SitesHelper
|
||||||
@@ -58,6 +59,8 @@ class PluginManager(metaclass=Singleton):
|
|||||||
self._plugins[plugin_id] = plugin
|
self._plugins[plugin_id] = plugin
|
||||||
# 未安装的不加载
|
# 未安装的不加载
|
||||||
if plugin_id not in installed_plugins:
|
if plugin_id not in installed_plugins:
|
||||||
|
# 设置事件状态为不可用
|
||||||
|
eventmanager.disable_events_hander(plugin_id)
|
||||||
continue
|
continue
|
||||||
# 生成实例
|
# 生成实例
|
||||||
plugin_obj = plugin()
|
plugin_obj = plugin()
|
||||||
@@ -66,6 +69,8 @@ class PluginManager(metaclass=Singleton):
|
|||||||
# 存储运行实例
|
# 存储运行实例
|
||||||
self._running_plugins[plugin_id] = plugin_obj
|
self._running_plugins[plugin_id] = plugin_obj
|
||||||
logger.info(f"Plugin Loaded:{plugin_id}")
|
logger.info(f"Plugin Loaded:{plugin_id}")
|
||||||
|
# 设置事件注册状态可用
|
||||||
|
eventmanager.enable_events_hander(plugin_id)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"加载插件 {plugin_id} 出错:{err} - {traceback.format_exc()}")
|
logger.error(f"加载插件 {plugin_id} 出错:{err} - {traceback.format_exc()}")
|
||||||
|
|
||||||
@@ -177,6 +182,12 @@ class PluginManager(metaclass=Singleton):
|
|||||||
return None
|
return None
|
||||||
return getattr(self._running_plugins[pid], method)(*args, **kwargs)
|
return getattr(self._running_plugins[pid], method)(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_plugin_ids(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
获取所有插件ID
|
||||||
|
"""
|
||||||
|
return list(self._plugins.keys())
|
||||||
|
|
||||||
def get_plugin_apps(self) -> List[dict]:
|
def get_plugin_apps(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
获取所有插件信息
|
获取所有插件信息
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ class Subscribe(Base):
|
|||||||
include = Column(String)
|
include = Column(String)
|
||||||
# 排除
|
# 排除
|
||||||
exclude = Column(String)
|
exclude = Column(String)
|
||||||
|
# 质量
|
||||||
|
quality = Column(String)
|
||||||
|
# 分辨率
|
||||||
|
resolution = Column(String)
|
||||||
|
# 特效
|
||||||
|
effect = Column(String)
|
||||||
# 总集数
|
# 总集数
|
||||||
total_episode = Column(Integer)
|
total_episode = Column(Integer)
|
||||||
# 开始集数
|
# 开始集数
|
||||||
|
|||||||
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()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Optional, Tuple, Union
|
||||||
|
|
||||||
@@ -369,6 +368,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]]:
|
||||||
"""
|
"""
|
||||||
搜索媒体信息
|
搜索媒体信息
|
||||||
@@ -396,17 +405,25 @@ class DoubanModule(_ModuleBase):
|
|||||||
return ret_medias
|
return ret_medias
|
||||||
|
|
||||||
@retry(Exception, 5, 3, 3, logger=logger)
|
@retry(Exception, 5, 3, 3, logger=logger)
|
||||||
def match_doubaninfo(self, name: str, mtype: str = None,
|
def match_doubaninfo(self, name: str, imdbid: str = None,
|
||||||
year: str = None, season: int = None) -> dict:
|
mtype: str = None, year: str = None, season: int = None) -> dict:
|
||||||
"""
|
"""
|
||||||
搜索和匹配豆瓣信息
|
搜索和匹配豆瓣信息
|
||||||
:param name: 名称
|
:param name: 名称
|
||||||
|
:param imdbid: IMDB ID
|
||||||
:param mtype: 类型 电影/电视剧
|
:param mtype: 类型 电影/电视剧
|
||||||
:param year: 年份
|
:param year: 年份
|
||||||
:param season: 季号
|
:param season: 季号
|
||||||
"""
|
"""
|
||||||
result = self.doubanapi.search(f"{name} {year or ''}".strip(),
|
if imdbid:
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d%H%M%S'))
|
# 优先使用IMDBID查询
|
||||||
|
logger.info(f"开始使用IMDBID {imdbid} 查询豆瓣信息 ...")
|
||||||
|
result = self.doubanapi.imdbid(imdbid)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
# 搜索
|
||||||
|
logger.info(f"开始使用名称 {name} 查询豆瓣信息 ...")
|
||||||
|
result = self.doubanapi.search(f"{name} {year or ''}".strip())
|
||||||
if not result:
|
if not result:
|
||||||
logger.warn(f"未找到 {name} 的豆瓣信息")
|
logger.warn(f"未找到 {name} 的豆瓣信息")
|
||||||
return {}
|
return {}
|
||||||
@@ -463,6 +480,7 @@ class DoubanModule(_ModuleBase):
|
|||||||
return
|
return
|
||||||
# 根据名称查询豆瓣数据
|
# 根据名称查询豆瓣数据
|
||||||
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
|
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
|
||||||
|
imdbid=mediainfo.imdb_id,
|
||||||
mtype=mediainfo.type.value,
|
mtype=mediainfo.type.value,
|
||||||
year=mediainfo.year,
|
year=mediainfo.year,
|
||||||
season=meta.begin_season)
|
season=meta.begin_season)
|
||||||
@@ -485,6 +503,7 @@ class DoubanModule(_ModuleBase):
|
|||||||
continue
|
continue
|
||||||
# 根据名称查询豆瓣数据
|
# 根据名称查询豆瓣数据
|
||||||
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
|
doubaninfo = self.match_doubaninfo(name=mediainfo.title,
|
||||||
|
imdbid=mediainfo.imdb_id,
|
||||||
mtype=mediainfo.type.value,
|
mtype=mediainfo.type.value,
|
||||||
year=mediainfo.year,
|
year=mediainfo.year,
|
||||||
season=meta.begin_season)
|
season=meta.begin_season)
|
||||||
|
|||||||
@@ -18,28 +18,29 @@ class DoubanApi(metaclass=Singleton):
|
|||||||
_urls = {
|
_urls = {
|
||||||
# 搜索类
|
# 搜索类
|
||||||
# sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映
|
# sort=U:近期热门 T:标记最多 S:评分最高 R:最新上映
|
||||||
# q=search_word&start=0&count=20&sort=U
|
# q=search_word&start: int = 0&count: int = 20&sort=U
|
||||||
# 聚合搜索
|
# 聚合搜索
|
||||||
"search": "/search/weixin",
|
"search": "/search/weixin",
|
||||||
"search_agg": "/search",
|
"search_agg": "/search",
|
||||||
|
"imdbid": "/movie/imdb/%s",
|
||||||
|
|
||||||
# 电影探索
|
# 电影探索
|
||||||
# sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间
|
# sort=U:综合排序 T:近期热度 S:高分优先 R:首播时间
|
||||||
# tags='日本,动画,2022'&start=0&count=20&sort=U
|
# tags='日本,动画,2022'&start: int = 0&count: int = 20&sort=U
|
||||||
"movie_recommend": "/movie/recommend",
|
"movie_recommend": "/movie/recommend",
|
||||||
# 电视剧探索
|
# 电视剧探索
|
||||||
"tv_recommend": "/tv/recommend",
|
"tv_recommend": "/tv/recommend",
|
||||||
# 搜索
|
# 搜索
|
||||||
"movie_tag": "/movie/tag",
|
"movie_tag": "/movie/tag",
|
||||||
"tv_tag": "/tv/tag",
|
"tv_tag": "/tv/tag",
|
||||||
# q=search_word&start=0&count=20
|
# q=search_word&start: int = 0&count: int = 20
|
||||||
"movie_search": "/search/movie",
|
"movie_search": "/search/movie",
|
||||||
"tv_search": "/search/movie",
|
"tv_search": "/search/movie",
|
||||||
"book_search": "/search/book",
|
"book_search": "/search/book",
|
||||||
"group_search": "/search/group",
|
"group_search": "/search/group",
|
||||||
|
|
||||||
# 各类主题合集
|
# 各类主题合集
|
||||||
# start=0&count=20
|
# start: int = 0&count: int = 20
|
||||||
# 正在上映
|
# 正在上映
|
||||||
"movie_showing": "/subject_collection/movie_showing/items",
|
"movie_showing": "/subject_collection/movie_showing/items",
|
||||||
# 热门电影
|
# 热门电影
|
||||||
@@ -145,7 +146,9 @@ class DoubanApi(metaclass=Singleton):
|
|||||||
"api-client/1 com.douban.frodo/7.3.0(207) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1"]
|
"api-client/1 com.douban.frodo/7.3.0(207) Android/22 product/MI 9 vendor/Xiaomi model/MI 9 brand/Android rom/miui6 network/wifi platform/mobile nd/1"]
|
||||||
_api_secret_key = "bf7dddc7c9cfe6f7"
|
_api_secret_key = "bf7dddc7c9cfe6f7"
|
||||||
_api_key = "0dad551ec0f84ed02907ff5c42e8ec70"
|
_api_key = "0dad551ec0f84ed02907ff5c42e8ec70"
|
||||||
|
_api_key2 = "0ab215a8b1977939201640fa14c66bab"
|
||||||
_base_url = "https://frodo.douban.com/api/v2"
|
_base_url = "https://frodo.douban.com/api/v2"
|
||||||
|
_api_url = "https://api.douban.com/v2"
|
||||||
_session = None
|
_session = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -153,6 +156,9 @@ class DoubanApi(metaclass=Singleton):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __sign(cls, url: str, ts: int, method='GET') -> str:
|
def __sign(cls, url: str, ts: int, method='GET') -> str:
|
||||||
|
"""
|
||||||
|
签名
|
||||||
|
"""
|
||||||
url_path = parse.urlparse(url).path
|
url_path = parse.urlparse(url).path
|
||||||
raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)])
|
raw_sign = '&'.join([method.upper(), parse.quote(url_path, safe=''), str(ts)])
|
||||||
return base64.b64encode(
|
return base64.b64encode(
|
||||||
@@ -164,7 +170,10 @@ class DoubanApi(metaclass=Singleton):
|
|||||||
).decode()
|
).decode()
|
||||||
|
|
||||||
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
|
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
|
||||||
def __invoke(self, url, **kwargs):
|
def __invoke(self, url: str, **kwargs) -> dict:
|
||||||
|
"""
|
||||||
|
GET请求
|
||||||
|
"""
|
||||||
req_url = self._base_url + url
|
req_url = self._base_url + url
|
||||||
|
|
||||||
params = {'apiKey': self._api_key}
|
params = {'apiKey': self._api_key}
|
||||||
@@ -189,119 +198,224 @@ class DoubanApi(metaclass=Singleton):
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
return resp.json() if resp else {}
|
return resp.json() if resp else {}
|
||||||
|
|
||||||
def search(self, keyword, start=0, count=20,
|
@lru_cache(maxsize=settings.CACHE_CONF.get('douban'))
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
def __post(self, url: str, **kwargs) -> dict:
|
||||||
|
"""
|
||||||
|
POST请求
|
||||||
|
esponse = requests.post(
|
||||||
|
url="https://api.douban.com/v2/movie/imdb/tt29139455",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||||
|
"Cookie": "bid=J9zb1zA5sJc",
|
||||||
|
},
|
||||||
|
data={
|
||||||
|
"apikey": "0ab215a8b1977939201640fa14c66bab",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
req_url = self._api_url + url
|
||||||
|
params = {'apikey': self._api_key2}
|
||||||
|
if kwargs:
|
||||||
|
params.update(kwargs)
|
||||||
|
if '_ts' in params:
|
||||||
|
params.pop('_ts')
|
||||||
|
resp = RequestUtils(
|
||||||
|
ua=settings.USER_AGENT,
|
||||||
|
session=self._session,
|
||||||
|
).post_res(url=req_url, data=params)
|
||||||
|
if resp.status_code == 400 and "rate_limit" in resp.text:
|
||||||
|
return resp.json()
|
||||||
|
return resp.json() if resp else {}
|
||||||
|
|
||||||
|
def search(self, keyword: str, start: int = 0, count: int = 20,
|
||||||
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')) -> dict:
|
||||||
|
"""
|
||||||
|
关键字搜索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["search"], q=keyword,
|
return self.__invoke(self._urls["search"], q=keyword,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_search(self, keyword, start=0, count=20,
|
def imdbid(self, imdbid: str,
|
||||||
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
IMDBID搜索
|
||||||
|
"""
|
||||||
|
return self.__post(self._urls["imdbid"] % imdbid, _ts=ts)
|
||||||
|
|
||||||
|
def movie_search(self, keyword: str, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电影搜索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_search"], q=keyword,
|
return self.__invoke(self._urls["movie_search"], q=keyword,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_search(self, keyword, start=0, count=20,
|
def tv_search(self, keyword: str, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电视搜索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_search"], q=keyword,
|
return self.__invoke(self._urls["tv_search"], q=keyword,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def book_search(self, keyword, start=0, count=20,
|
def book_search(self, keyword: str, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
书籍搜索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["book_search"], q=keyword,
|
return self.__invoke(self._urls["book_search"], q=keyword,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def group_search(self, keyword, start=0, count=20,
|
def group_search(self, keyword: str, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
小组搜索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["group_search"], q=keyword,
|
return self.__invoke(self._urls["group_search"], q=keyword,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_showing(self, start=0, count=20,
|
def movie_showing(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
正在热映
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_showing"],
|
return self.__invoke(self._urls["movie_showing"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_soon(self, start=0, count=20,
|
def movie_soon(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
即将上映
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_soon"],
|
return self.__invoke(self._urls["movie_soon"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_hot_gaia(self, start=0, count=20,
|
def movie_hot_gaia(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
热门电影
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_hot_gaia"],
|
return self.__invoke(self._urls["movie_hot_gaia"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_hot(self, start=0, count=20,
|
def tv_hot(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
热门剧集
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_hot"],
|
return self.__invoke(self._urls["tv_hot"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_animation(self, start=0, count=20,
|
def tv_animation(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
动画
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_animation"],
|
return self.__invoke(self._urls["tv_animation"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_variety_show(self, start=0, count=20,
|
def tv_variety_show(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
综艺
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_variety_show"],
|
return self.__invoke(self._urls["tv_variety_show"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_rank_list(self, start=0, count=20,
|
def tv_rank_list(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电视剧排行榜
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_rank_list"],
|
return self.__invoke(self._urls["tv_rank_list"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def show_hot(self, start=0, count=20,
|
def show_hot(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
综艺热门
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["show_hot"],
|
return self.__invoke(self._urls["show_hot"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_detail(self, subject_id):
|
def movie_detail(self, subject_id: str):
|
||||||
|
"""
|
||||||
|
电影详情
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_detail"] + subject_id)
|
return self.__invoke(self._urls["movie_detail"] + subject_id)
|
||||||
|
|
||||||
def movie_celebrities(self, subject_id):
|
def movie_celebrities(self, subject_id: str):
|
||||||
|
"""
|
||||||
|
电影演职员
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_celebrities"] % subject_id)
|
return self.__invoke(self._urls["movie_celebrities"] % subject_id)
|
||||||
|
|
||||||
def tv_detail(self, subject_id):
|
def tv_detail(self, subject_id: str):
|
||||||
|
"""
|
||||||
|
电视剧详情
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_detail"] + subject_id)
|
return self.__invoke(self._urls["tv_detail"] + subject_id)
|
||||||
|
|
||||||
def tv_celebrities(self, subject_id):
|
def tv_celebrities(self, subject_id: str):
|
||||||
|
"""
|
||||||
|
电视剧演职员
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_celebrities"] % subject_id)
|
return self.__invoke(self._urls["tv_celebrities"] % subject_id)
|
||||||
|
|
||||||
def book_detail(self, subject_id):
|
def book_detail(self, subject_id: str):
|
||||||
|
"""
|
||||||
|
书籍详情
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["book_detail"] + subject_id)
|
return self.__invoke(self._urls["book_detail"] + subject_id)
|
||||||
|
|
||||||
def movie_top250(self, start=0, count=20,
|
def movie_top250(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电影TOP250
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_top250"],
|
return self.__invoke(self._urls["movie_top250"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def movie_recommend(self, tags='', sort='R', start=0, count=20,
|
def movie_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电影探索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort,
|
return self.__invoke(self._urls["movie_recommend"], tags=tags, sort=sort,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_recommend(self, tags='', sort='R', start=0, count=20,
|
def tv_recommend(self, tags='', sort='R', start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
电视剧探索
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort,
|
return self.__invoke(self._urls["tv_recommend"], tags=tags, sort=sort,
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_chinese_best_weekly(self, start=0, count=20,
|
def tv_chinese_best_weekly(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
华语口碑周榜
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_chinese_best_weekly"],
|
return self.__invoke(self._urls["tv_chinese_best_weekly"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def tv_global_best_weekly(self, start=0, count=20,
|
def tv_global_best_weekly(self, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
|
"""
|
||||||
|
全球口碑周榜
|
||||||
|
"""
|
||||||
return self.__invoke(self._urls["tv_global_best_weekly"],
|
return self.__invoke(self._urls["tv_global_best_weekly"],
|
||||||
start=start, count=count, _ts=ts)
|
start=start, count=count, _ts=ts)
|
||||||
|
|
||||||
def doulist_detail(self, subject_id):
|
def doulist_detail(self, subject_id: str):
|
||||||
"""
|
"""
|
||||||
豆列详情
|
豆列详情
|
||||||
:param subject_id: 豆列id
|
:param subject_id: 豆列id
|
||||||
"""
|
"""
|
||||||
return self.__invoke(self._urls["doulist"] + subject_id)
|
return self.__invoke(self._urls["doulist"] + subject_id)
|
||||||
|
|
||||||
def doulist_items(self, subject_id, start=0, count=20,
|
def doulist_items(self, subject_id: str, start: int = 0, count: int = 20,
|
||||||
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
ts=datetime.strftime(datetime.now(), '%Y%m%d')):
|
||||||
"""
|
"""
|
||||||
豆列列表
|
豆列列表
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -376,13 +382,14 @@ class FileTransferModule(_ModuleBase):
|
|||||||
path=in_path,
|
path=in_path,
|
||||||
message=f"{in_path} 路径不存在")
|
message=f"{in_path} 路径不存在")
|
||||||
|
|
||||||
if not target_dir.exists():
|
if transfer_type not in ['rclone_copy', 'rclone_move']:
|
||||||
return TransferInfo(success=False,
|
# 检查目标路径
|
||||||
path=in_path,
|
if not target_dir.exists():
|
||||||
message=f"{target_dir} 目标路径不存在")
|
return TransferInfo(success=False,
|
||||||
|
path=in_path,
|
||||||
# 媒体库目的目录
|
message=f"{target_dir} 目标路径不存在")
|
||||||
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
|
# 媒体库目的目录
|
||||||
|
target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=target_dir)
|
||||||
|
|
||||||
# 重命名格式
|
# 重命名格式
|
||||||
rename_format = settings.TV_RENAME_FORMAT \
|
rename_format = settings.TV_RENAME_FORMAT \
|
||||||
|
|||||||
@@ -70,11 +70,16 @@ class FilterModule(_ModuleBase):
|
|||||||
"include": [r'[Hx].?264|AVC'],
|
"include": [r'[Hx].?264|AVC'],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
},
|
},
|
||||||
# 杜比
|
# 杜比视界
|
||||||
"DOLBY": {
|
"DOLBY": {
|
||||||
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
|
"include": [r"Dolby[\s.]+Vision|DOVI|[\s.]+DV[\s.]+|杜比视界"],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
},
|
},
|
||||||
|
# 杜比全景声
|
||||||
|
"ATMOS": {
|
||||||
|
"include": [r"Dolby[\s.+]+Atmos|Atmos|杜比全景[声聲]"],
|
||||||
|
"exclude": []
|
||||||
|
},
|
||||||
# HDR
|
# HDR
|
||||||
"HDR": {
|
"HDR": {
|
||||||
"include": [r"[\s.]+HDR[\s.]+|HDR10|HDR10\+"],
|
"include": [r"[\s.]+HDR[\s.]+|HDR10|HDR10\+"],
|
||||||
@@ -98,7 +103,12 @@ class FilterModule(_ModuleBase):
|
|||||||
"CNVOI": {
|
"CNVOI": {
|
||||||
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
|
"include": [r'[国國][语語]配音|[国國]配|[国國][语語]'],
|
||||||
"exclude": []
|
"exclude": []
|
||||||
}
|
},
|
||||||
|
# 60FPS
|
||||||
|
"60FPS": {
|
||||||
|
"include": [r'60fps'],
|
||||||
|
"exclude": []
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def init_module(self) -> None:
|
def init_module(self) -> None:
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -280,6 +283,8 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:return: 更新后的媒体信息
|
:return: 更新后的媒体信息
|
||||||
"""
|
"""
|
||||||
|
if not mediainfo.tmdb_id:
|
||||||
|
return mediainfo
|
||||||
if mediainfo.logo_path \
|
if mediainfo.logo_path \
|
||||||
and mediainfo.poster_path \
|
and mediainfo.poster_path \
|
||||||
and mediainfo.backdrop_path:
|
and mediainfo.backdrop_path:
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ class TMDb(object):
|
|||||||
else:
|
else:
|
||||||
req = self.request(method, url, data, json)
|
req = self.request(method, url, data, json)
|
||||||
|
|
||||||
|
if req is None:
|
||||||
|
raise TMDbException("Failed to establish a new connection: no response from the server.")
|
||||||
|
|
||||||
headers = req.headers
|
headers = req.headers
|
||||||
|
|
||||||
if "X-RateLimit-Remaining" in headers:
|
if "X-RateLimit-Remaining" in headers:
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from typing import Any, List, Dict, Tuple
|
from typing import Any, List, Dict, Tuple
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager, Event
|
||||||
|
from app.log import logger
|
||||||
from app.plugins import _PluginBase
|
from app.plugins import _PluginBase
|
||||||
from app.plugins.chatgpt.openai import OpenAi
|
from app.plugins.chatgpt.openai import OpenAi
|
||||||
from app.schemas.types import EventType
|
from app.schemas.types import EventType
|
||||||
@@ -33,6 +34,7 @@ class ChatGPT(_PluginBase):
|
|||||||
openai = None
|
openai = None
|
||||||
_enabled = False
|
_enabled = False
|
||||||
_proxy = False
|
_proxy = False
|
||||||
|
_recognize = False
|
||||||
_openai_url = None
|
_openai_url = None
|
||||||
_openai_key = None
|
_openai_key = None
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ class ChatGPT(_PluginBase):
|
|||||||
if config:
|
if config:
|
||||||
self._enabled = config.get("enabled")
|
self._enabled = config.get("enabled")
|
||||||
self._proxy = config.get("proxy")
|
self._proxy = config.get("proxy")
|
||||||
|
self._recognize = config.get("recognize")
|
||||||
self._openai_url = config.get("openai_url")
|
self._openai_url = config.get("openai_url")
|
||||||
self._openai_key = config.get("openai_key")
|
self._openai_key = config.get("openai_key")
|
||||||
self.openai = OpenAi(api_key=self._openai_key, api_url=self._openai_url,
|
self.openai = OpenAi(api_key=self._openai_key, api_url=self._openai_url,
|
||||||
@@ -70,7 +73,7 @@ class ChatGPT(_PluginBase):
|
|||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 6
|
'md': 4
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
@@ -86,7 +89,7 @@ class ChatGPT(_PluginBase):
|
|||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 6
|
'md': 4
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
@@ -97,6 +100,22 @@ class ChatGPT(_PluginBase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'component': 'VCol',
|
||||||
|
'props': {
|
||||||
|
'cols': 12,
|
||||||
|
'md': 4
|
||||||
|
},
|
||||||
|
'content': [
|
||||||
|
{
|
||||||
|
'component': 'VSwitch',
|
||||||
|
'props': {
|
||||||
|
'model': 'recognize',
|
||||||
|
'label': '辅助识别',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -143,6 +162,7 @@ class ChatGPT(_PluginBase):
|
|||||||
], {
|
], {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"proxy": False,
|
"proxy": False,
|
||||||
|
"recognize": False,
|
||||||
"openai_url": "https://api.openai.com",
|
"openai_url": "https://api.openai.com",
|
||||||
"openai_key": ""
|
"openai_key": ""
|
||||||
}
|
}
|
||||||
@@ -151,10 +171,12 @@ class ChatGPT(_PluginBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@eventmanager.register(EventType.UserMessage)
|
@eventmanager.register(EventType.UserMessage)
|
||||||
def talk(self, event):
|
def talk(self, event: Event):
|
||||||
"""
|
"""
|
||||||
监听用户消息,获取ChatGPT回复
|
监听用户消息,获取ChatGPT回复
|
||||||
"""
|
"""
|
||||||
|
if not self._enabled:
|
||||||
|
return
|
||||||
if not self.openai:
|
if not self.openai:
|
||||||
return
|
return
|
||||||
text = event.event_data.get("text")
|
text = event.event_data.get("text")
|
||||||
@@ -166,6 +188,42 @@ class ChatGPT(_PluginBase):
|
|||||||
if response:
|
if response:
|
||||||
self.post_message(channel=channel, title=response, userid=userid)
|
self.post_message(channel=channel, title=response, userid=userid)
|
||||||
|
|
||||||
|
@eventmanager.register(EventType.NameRecognize)
|
||||||
|
def recognize(self, event: Event):
|
||||||
|
"""
|
||||||
|
监听识别事件,使用ChatGPT辅助识别名称
|
||||||
|
"""
|
||||||
|
if not event.event_data:
|
||||||
|
return
|
||||||
|
title = event.event_data.get("title")
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
# 收到事件后需要立码返回,避免主程序等待
|
||||||
|
if not self._enabled \
|
||||||
|
or not self.openai \
|
||||||
|
or not self._recognize:
|
||||||
|
eventmanager.send_event(
|
||||||
|
EventType.NameRecognizeResult,
|
||||||
|
{
|
||||||
|
'title': title
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# 调用ChatGPT
|
||||||
|
response = self.openai.get_media_name(filename=title)
|
||||||
|
logger.info(f"ChatGPT辅助识别结果:{response}")
|
||||||
|
if response:
|
||||||
|
eventmanager.send_event(
|
||||||
|
EventType.NameRecognizeResult,
|
||||||
|
{
|
||||||
|
'title': title,
|
||||||
|
'name': response.get("title"),
|
||||||
|
'year': response.get("year"),
|
||||||
|
'season': response.get("season"),
|
||||||
|
'episode': response.get("episode")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def stop_service(self):
|
def stop_service(self):
|
||||||
"""
|
"""
|
||||||
退出插件
|
退出插件
|
||||||
|
|||||||
@@ -251,6 +251,6 @@ class ChineseSubFinder(_PluginBase):
|
|||||||
else:
|
else:
|
||||||
logger.info("ChineseSubFinder任务添加成功:%s" % job_id)
|
logger.info("ChineseSubFinder任务添加成功:%s" % job_id)
|
||||||
else:
|
else:
|
||||||
logger.error("%s 目录缺失nfo元数据" % file_path)
|
logger.warn(f"ChineseSubFinder调用出错:{res.status_code} - {res.reason}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("连接ChineseSubFinder出错:" + str(e))
|
logger.error("连接ChineseSubFinder出错:" + str(e))
|
||||||
|
|||||||
@@ -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 '请检查权限'}")
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class DirMonitor(_PluginBase):
|
|||||||
self.transferhis = TransferHistoryOper(self.db)
|
self.transferhis = TransferHistoryOper(self.db)
|
||||||
self.downloadhis = DownloadHistoryOper(self.db)
|
self.downloadhis = DownloadHistoryOper(self.db)
|
||||||
self.transferchian = TransferChain(self.db)
|
self.transferchian = TransferChain(self.db)
|
||||||
self.tmdbchain = TmdbChain(self.db)
|
self.tmdbchain = TmdbChain()
|
||||||
# 清空配置
|
# 清空配置
|
||||||
self._dirconf = {}
|
self._dirconf = {}
|
||||||
self._transferconf = {}
|
self._transferconf = {}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class DownloadingMsg(_PluginBase):
|
|||||||
channel_value = downloadhis.channel
|
channel_value = downloadhis.channel
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
context = MediaChain(self.db).recognize_by_title(title=torrent.title)
|
context = MediaChain().recognize_by_title(title=torrent.title)
|
||||||
if not context or not context.media_info:
|
if not context or not context.media_info:
|
||||||
continue
|
continue
|
||||||
media_info = context.media_info
|
media_info = context.media_info
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class PersonMeta(_PluginBase):
|
|||||||
_remove_nozh = False
|
_remove_nozh = False
|
||||||
|
|
||||||
def init_plugin(self, config: dict = None):
|
def init_plugin(self, config: dict = None):
|
||||||
self.tmdbchain = TmdbChain(self.db)
|
self.tmdbchain = TmdbChain()
|
||||||
self.mschain = MediaServerChain(self.db)
|
self.mschain = MediaServerChain(self.db)
|
||||||
if config:
|
if config:
|
||||||
self._enabled = config.get("enabled")
|
self._enabled = config.get("enabled")
|
||||||
@@ -581,10 +581,13 @@ 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,
|
||||||
|
imdbid=mediainfo.imdb_id,
|
||||||
mtype=mediainfo.type.value,
|
mtype=mediainfo.type.value,
|
||||||
year=mediainfo.year,
|
year=mediainfo.year,
|
||||||
season=season)
|
season=season)
|
||||||
@@ -712,7 +715,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 +724,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 +735,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 +786,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
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ class Subscribe(BaseModel):
|
|||||||
include: Optional[str] = None
|
include: Optional[str] = None
|
||||||
# 排除
|
# 排除
|
||||||
exclude: Optional[str] = None
|
exclude: Optional[str] = None
|
||||||
|
# 质量
|
||||||
|
quality: Optional[str] = None
|
||||||
|
# 分辨率
|
||||||
|
resolution: Optional[str] = None
|
||||||
|
# 特效
|
||||||
|
effect: Optional[str] = None
|
||||||
# 总集数
|
# 总集数
|
||||||
total_episode: Optional[int] = 0
|
total_episode: Optional[int] = 0
|
||||||
# 开始集数
|
# 开始集数
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class EventType(Enum):
|
|||||||
UserMessage = "user.message"
|
UserMessage = "user.message"
|
||||||
# 通知消息
|
# 通知消息
|
||||||
NoticeMessage = "notice.message"
|
NoticeMessage = "notice.message"
|
||||||
|
# 名称识别请求
|
||||||
|
NameRecognize = "name.recognize"
|
||||||
|
# 名称识别结果
|
||||||
|
NameRecognizeResult = "name.recognize.result"
|
||||||
|
|
||||||
|
|
||||||
# 系统配置Key字典
|
# 系统配置Key字典
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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=
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Create Date: 2023-09-28 13:37:16.479360
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'a521fbc28b18'
|
revision = 'a521fbc28b18'
|
||||||
down_revision = 'b2f011d3a8b7'
|
down_revision = 'b2f011d3a8b7'
|
||||||
@@ -26,5 +25,6 @@ def upgrade() -> None:
|
|||||||
pass
|
pass
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
pass
|
pass
|
||||||
32
database/versions/d633ca6cd572_1_0_10.py
Normal file
32
database/versions/d633ca6cd572_1_0_10.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""1.0.10
|
||||||
|
|
||||||
|
Revision ID: d633ca6cd572
|
||||||
|
Revises: a521fbc28b18
|
||||||
|
Create Date: 2023-10-12 08:54:49.728638
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd633ca6cd572'
|
||||||
|
down_revision = 'a521fbc28b18'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
try:
|
||||||
|
with op.batch_alter_table("subscribe") as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('quality', sa.String, nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('resolution', sa.String, nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('effect', sa.String, nullable=True))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -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.3.0'
|
APP_VERSION = 'v1.3.2'
|
||||||
|
|||||||
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