From 2f38b3040d108feb095db3857d6fd5d9820fe36a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 23 Mar 2025 12:10:21 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=85=BC=E5=AE=B9=E6=80=A7=E5=86=99=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + app/api/endpoints/dashboard.py | 14 +++--- app/api/endpoints/download.py | 10 ++-- app/api/endpoints/login.py | 6 +-- app/api/endpoints/media.py | 11 +++-- app/api/endpoints/mediaserver.py | 2 +- app/api/endpoints/search.py | 44 +++++++++-------- app/api/endpoints/subscribe.py | 6 +-- app/api/endpoints/system.py | 8 ++-- app/api/endpoints/transfer.py | 4 +- app/api/endpoints/webhook.py | 6 +-- app/api/servarr.py | 38 +++++++-------- app/chain/__init__.py | 6 ++- app/core/meta/metavideo.py | 6 +-- app/core/security.py | 10 ++-- app/helper/module.py | 14 ++++-- app/helper/progress.py | 2 +- app/modules/__init__.py | 4 -- app/modules/emby/__init__.py | 24 +++++----- app/modules/jellyfin/__init__.py | 20 ++++---- app/modules/plex/__init__.py | 26 +++++----- app/modules/themoviedb/category.py | 4 +- app/plugins/__init__.py | 1 - app/utils/object.py | 76 ++++++++++++++++++++++++------ app/utils/string.py | 4 +- requirements.in | 2 +- setup.py | 37 +++++++++++++++ 27 files changed, 242 insertions(+), 145 deletions(-) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index c17d9b7f..c5b5aa2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea/ *.c +*.so +*.pyd build/ dist/ nginx/ diff --git a/app/api/endpoints/dashboard.py b/app/api/endpoints/dashboard.py index 819d51d4..a88dbc0e 100644 --- a/app/api/endpoints/dashboard.py +++ b/app/api/endpoints/dashboard.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, List, Optional +from typing import Any, List, Optional, Annotated from fastapi import APIRouter, Depends from sqlalchemy.orm import Session @@ -37,7 +37,7 @@ def statistic(name: str = None, _: schemas.TokenPayload = Depends(verify_token)) @router.get("/statistic2", summary="媒体数量统计(API_TOKEN)", response_model=schemas.Statistic) -def statistic2(_: str = Depends(verify_apitoken)) -> Any: +def statistic2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 查询媒体数量统计信息 API_TOKEN认证(?token=xxx) """ @@ -66,7 +66,7 @@ def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any: @router.get("/storage2", summary="本地存储空间(API_TOKEN)", response_model=schemas.Storage) -def storage2(_: str = Depends(verify_apitoken)) -> Any: +def storage2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 查询本地存储空间信息 API_TOKEN认证(?token=xxx) """ @@ -103,7 +103,7 @@ def downloader(name: str = None, _: schemas.TokenPayload = Depends(verify_token) @router.get("/downloader2", summary="下载器信息(API_TOKEN)", response_model=schemas.DownloaderInfo) -def downloader2(_: str = Depends(verify_apitoken)) -> Any: +def downloader2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 查询下载器信息 API_TOKEN认证(?token=xxx) """ @@ -119,7 +119,7 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any: @router.get("/schedule2", summary="后台服务(API_TOKEN)", response_model=List[schemas.ScheduleInfo]) -def schedule2(_: str = Depends(verify_apitoken)) -> Any: +def schedule2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 查询下载器信息 API_TOKEN认证(?token=xxx) """ @@ -145,7 +145,7 @@ def cpu(_: schemas.TokenPayload = Depends(verify_token)) -> Any: @router.get("/cpu2", summary="获取当前CPU使用率(API_TOKEN)", response_model=int) -def cpu2(_: str = Depends(verify_apitoken)) -> Any: +def cpu2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 获取当前CPU使用率 API_TOKEN认证(?token=xxx) """ @@ -161,7 +161,7 @@ def memory(_: schemas.TokenPayload = Depends(verify_token)) -> Any: @router.get("/memory2", summary="获取当前内存使用量和使用率(API_TOKEN)", response_model=List[int]) -def memory2(_: str = Depends(verify_apitoken)) -> Any: +def memory2(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 获取当前内存使用率 API_TOKEN认证(?token=xxx) """ diff --git a/app/api/endpoints/download.py b/app/api/endpoints/download.py index 8d4193fb..76e847de 100644 --- a/app/api/endpoints/download.py +++ b/app/api/endpoints/download.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Annotated from fastapi import APIRouter, Depends, Body @@ -30,8 +30,8 @@ def current( def download( media_in: schemas.MediaInfo, torrent_in: schemas.TorrentInfo, - downloader: str = Body(None), - save_path: str = Body(None), + downloader: Annotated[str | None, Body()] = None, + save_path: Annotated[str | None, Body()] = None, current_user: User = Depends(get_current_active_user)) -> Any: """ 添加下载任务(含媒体信息) @@ -62,8 +62,8 @@ def download( @router.post("/add", summary="添加下载(不含媒体信息)", response_model=schemas.Response) def add( torrent_in: schemas.TorrentInfo, - downloader: str = Body(None), - save_path: str = Body(None), + downloader: Annotated[str | None, Body()] = None, + save_path: Annotated[str | None, Body()] = None, current_user: User = Depends(get_current_active_user)) -> Any: """ 添加下载任务(不含媒体信息) diff --git a/app/api/endpoints/login.py b/app/api/endpoints/login.py index 262a84ff..39d564a6 100644 --- a/app/api/endpoints/login.py +++ b/app/api/endpoints/login.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import Any, List +from typing import Any, List, Annotated from fastapi import APIRouter, Depends, Form, HTTPException from fastapi.security import OAuth2PasswordRequestForm @@ -18,8 +18,8 @@ router = APIRouter() @router.post("/access-token", summary="获取token", response_model=schemas.Token) def login_access_token( - form_data: OAuth2PasswordRequestForm = Depends(), - otp_password: str = Form(None) + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + otp_password: Annotated[str | None, Form()] = None ) -> Any: """ 获取认证Token diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index 5811e168..1959d263 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Any, Union +from typing import List, Any, Union, Annotated from fastapi import APIRouter, Depends @@ -33,9 +33,10 @@ def recognize(title: str, @router.get("/recognize2", summary="识别种子媒体信息(API_TOKEN)", response_model=schemas.Context) -def recognize2(title: str, - subtitle: str = None, - _: str = Depends(verify_apitoken)) -> Any: +def recognize2(_: Annotated[str, Depends(verify_apitoken)], + title: str, + subtitle: str = None + ) -> Any: """ 根据标题、副标题识别媒体信息 API_TOKEN认证(?token=xxx) """ @@ -58,7 +59,7 @@ def recognize_file(path: str, @router.get("/recognize_file2", summary="识别文件媒体信息(API_TOKEN)", response_model=schemas.Context) def recognize_file2(path: str, - _: str = Depends(verify_apitoken)) -> Any: + _: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 根据文件路径识别媒体信息 API_TOKEN认证(?token=xxx) """ diff --git a/app/api/endpoints/mediaserver.py b/app/api/endpoints/mediaserver.py index 1f1e90b8..98d14e85 100644 --- a/app/api/endpoints/mediaserver.py +++ b/app/api/endpoints/mediaserver.py @@ -44,7 +44,7 @@ def play_item(itemid: str, _: schemas.TokenPayload = Depends(verify_token)) -> s @router.get("/exists", summary="查询本地是否存在(数据库)", response_model=schemas.Response) def exists_local(title: str = None, - year: int = None, + year: str = None, mtype: str = None, tmdbid: int = None, season: int = None, diff --git a/app/api/endpoints/search.py b/app/api/endpoints/search.py index a11fc17f..83c61f0d 100644 --- a/app/api/endpoints/search.py +++ b/app/api/endpoints/search.py @@ -37,9 +37,13 @@ def search_by_id(mediaid: str, 根据TMDBID/豆瓣ID精确搜索站点资源 tmdb:/douban:/bangumi: """ if mtype: - mtype = MediaType(mtype) + media_type = MediaType(mtype) + else: + media_type = None if season: - season = int(season) + media_season = int(season) + else: + media_season = None if sites: site_list = [int(site) for site in sites.split(",") if site] else: @@ -50,31 +54,31 @@ def search_by_id(mediaid: str, tmdbid = int(mediaid.replace("tmdb:", "")) if settings.RECOGNIZE_SOURCE == "douban": # 通过TMDBID识别豆瓣ID - doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=mtype) + doubaninfo = MediaChain().get_doubaninfo_by_tmdbid(tmdbid=tmdbid, mtype=media_type) if doubaninfo: torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"), - mtype=mtype, area=area, season=season, + mtype=media_type, area=area, season=media_season, sites=site_list) else: return schemas.Response(success=False, message="未识别到豆瓣媒体信息") else: - torrents = SearchChain().search_by_id(tmdbid=tmdbid, mtype=mtype, area=area, season=season, + torrents = SearchChain().search_by_id(tmdbid=tmdbid, mtype=media_type, area=area, season=media_season, sites=site_list) elif mediaid.startswith("douban:"): doubanid = mediaid.replace("douban:", "") if settings.RECOGNIZE_SOURCE == "themoviedb": # 通过豆瓣ID识别TMDBID - tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype) + tmdbinfo = MediaChain().get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=media_type) if tmdbinfo: - if tmdbinfo.get('season') and not season: - season = tmdbinfo.get('season') + if tmdbinfo.get('season') and not media_season: + media_season = tmdbinfo.get('season') torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"), - mtype=mtype, area=area, season=season, + mtype=media_type, area=area, season=media_season, sites=site_list) else: return schemas.Response(success=False, message="未识别到TMDB媒体信息") else: - torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=mtype, area=area, season=season, + torrents = SearchChain().search_by_id(doubanid=doubanid, mtype=media_type, area=area, season=media_season, sites=site_list) elif mediaid.startswith("bangumi:"): bangumiid = int(mediaid.replace("bangumi:", "")) @@ -83,7 +87,7 @@ def search_by_id(mediaid: str, tmdbinfo = MediaChain().get_tmdbinfo_by_bangumiid(bangumiid=bangumiid) if tmdbinfo: torrents = SearchChain().search_by_id(tmdbid=tmdbinfo.get("id"), - mtype=mtype, area=area, season=season, + mtype=media_type, area=area, season=media_season, sites=site_list) else: return schemas.Response(success=False, message="未识别到TMDB媒体信息") @@ -92,7 +96,7 @@ def search_by_id(mediaid: str, doubaninfo = MediaChain().get_doubaninfo_by_bangumiid(bangumiid=bangumiid) if doubaninfo: torrents = SearchChain().search_by_id(doubanid=doubaninfo.get("id"), - mtype=mtype, area=area, season=season, + mtype=media_type, area=area, season=media_season, sites=site_list) else: return schemas.Response(success=False, message="未识别到豆瓣媒体信息") @@ -110,10 +114,10 @@ def search_by_id(mediaid: str, search_id = event_data.media_dict.get("id") if event_data.convert_type == "themoviedb": torrents = SearchChain().search_by_id(tmdbid=search_id, - mtype=mtype, area=area, season=season) + mtype=media_type, area=area, season=media_season) elif event_data.convert_type == "douban": torrents = SearchChain().search_by_id(doubanid=search_id, - mtype=mtype, area=area, season=season) + mtype=media_type, area=area, season=media_season) else: if not title: return schemas.Response(success=False, message="未知的媒体ID") @@ -121,19 +125,19 @@ def search_by_id(mediaid: str, meta = MetaInfo(title) if year: meta.year = year - if mtype: - meta.type = mtype - if season: + if media_type: + meta.type = media_type + if media_season: meta.type = MediaType.TV - meta.begin_season = season + meta.begin_season = media_season mediainfo = MediaChain().recognize_media(meta=meta) if mediainfo: if settings.RECOGNIZE_SOURCE == "themoviedb": torrents = SearchChain().search_by_id(tmdbid=mediainfo.tmdb_id, - mtype=mtype, area=area, season=season) + mtype=media_type, area=area, season=media_season) else: torrents = SearchChain().search_by_id(doubanid=mediainfo.douban_id, - mtype=mtype, area=area, season=season) + mtype=media_type, area=area, season=media_season) # 返回搜索结果 if not torrents: return schemas.Response(success=False, message="未搜索到任何资源") diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 04a351d8..f9e33b4e 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -1,4 +1,4 @@ -from typing import List, Any +from typing import List, Any, Annotated import cn2an from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header @@ -44,7 +44,7 @@ def read_subscribes( @router.get("/list", summary="查询所有订阅(API_TOKEN)", response_model=List[schemas.Subscribe]) -def list_subscribes(_: str = Depends(verify_apitoken)) -> Any: +def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 查询所有订阅 API_TOKEN认证(?token=xxx) """ @@ -331,7 +331,7 @@ def delete_subscribe_by_mediaid( @router.post("/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=schemas.Response) async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks, - authorization: str = Header(None)) -> Any: + authorization: Annotated[str | None, Header()] = None) -> Any: """ Jellyseerr/Overseerr网络勾子通知订阅 """ diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index add27188..76f5ed42 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -5,7 +5,7 @@ import tempfile from collections import deque from datetime import datetime from pathlib import Path -from typing import Optional, Union +from typing import Optional, Union, Annotated import aiofiles import pillow_avif # noqa 用于自动注册AVIF支持 @@ -141,7 +141,7 @@ def fetch_image( def proxy_img( imgurl: str, proxy: bool = False, - if_none_match: Optional[str] = Header(None), + if_none_match: Annotated[str | None, Header()] = None, _: schemas.TokenPayload = Depends(verify_resource_token) ) -> Response: """ @@ -158,7 +158,7 @@ def proxy_img( @router.get("/cache/image", summary="图片缓存") def cache_img( url: str, - if_none_match: Optional[str] = Header(None), + if_none_match: Annotated[str | None, Header()] = None, _: schemas.TokenPayload = Depends(verify_resource_token) ) -> Response: """ @@ -500,7 +500,7 @@ def run_scheduler(jobid: str, @router.get("/runscheduler2", summary="运行服务(API_TOKEN)", response_model=schemas.Response) def run_scheduler2(jobid: str, - _: str = Depends(verify_apitoken)): + _: Annotated[str, Depends(verify_apitoken)]): """ 执行命令(API_TOKEN认证) """ diff --git a/app/api/endpoints/transfer.py b/app/api/endpoints/transfer.py index bbe1ccbd..f28ed16e 100644 --- a/app/api/endpoints/transfer.py +++ b/app/api/endpoints/transfer.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, List +from typing import Any, List, Annotated from fastapi import APIRouter, Depends from sqlalchemy.orm import Session @@ -165,7 +165,7 @@ def manual_transfer(transer_item: ManualTransferItem, @router.get("/now", summary="立即执行下载器文件整理", response_model=schemas.Response) -def now(_: str = Depends(verify_apitoken)) -> Any: +def now(_: Annotated[str, Depends(verify_apitoken)]) -> Any: """ 立即执行下载器文件整理 API_TOKEN认证(?token=xxx) """ diff --git a/app/api/endpoints/webhook.py b/app/api/endpoints/webhook.py index 88856732..c6705a17 100644 --- a/app/api/endpoints/webhook.py +++ b/app/api/endpoints/webhook.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Annotated from fastapi import APIRouter, BackgroundTasks, Request, Depends @@ -19,7 +19,7 @@ def start_webhook_chain(body: Any, form: Any, args: Any): @router.post("/", summary="Webhook消息响应", response_model=schemas.Response) async def webhook_message(background_tasks: BackgroundTasks, request: Request, - _: str = Depends(verify_apitoken) + _: Annotated[str, Depends(verify_apitoken)] ) -> Any: """ Webhook响应,配置请求中需要添加参数:token=API_TOKEN&source=媒体服务器名 @@ -33,7 +33,7 @@ async def webhook_message(background_tasks: BackgroundTasks, @router.get("/", summary="Webhook消息响应", response_model=schemas.Response) def webhook_message(background_tasks: BackgroundTasks, - request: Request, _: str = Depends(verify_apitoken)) -> Any: + request: Request, _: Annotated[str, Depends(verify_apitoken)]) -> Any: """ Webhook响应,配置请求中需要添加参数:token=API_TOKEN&source=媒体服务器名 """ diff --git a/app/api/servarr.py b/app/api/servarr.py index 83b6c9dd..f8e4bace 100644 --- a/app/api/servarr.py +++ b/app/api/servarr.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Annotated from fastapi import APIRouter, HTTPException, Depends from sqlalchemy.orm import Session @@ -18,7 +18,7 @@ arr_router = APIRouter(tags=['servarr']) @arr_router.get("/system/status", summary="系统状态") -def arr_system_status(_: str = Depends(verify_apikey)) -> Any: +def arr_system_status(_: Annotated[str, Depends(verify_apikey)]) -> Any: """ 模拟Radarr、Sonarr系统状态 """ @@ -72,7 +72,7 @@ def arr_system_status(_: str = Depends(verify_apikey)) -> Any: @arr_router.get("/qualityProfile", summary="质量配置") -def arr_qualityProfile(_: str = Depends(verify_apikey)) -> Any: +def arr_qualityProfile(_: Annotated[str, Depends(verify_apikey)]) -> Any: """ 模拟Radarr、Sonarr质量配置 """ @@ -113,7 +113,7 @@ def arr_qualityProfile(_: str = Depends(verify_apikey)) -> Any: @arr_router.get("/rootfolder", summary="根目录") -def arr_rootfolder(_: str = Depends(verify_apikey)) -> Any: +def arr_rootfolder(_: Annotated[str, Depends(verify_apikey)]) -> Any: """ 模拟Radarr、Sonarr根目录 """ @@ -129,7 +129,7 @@ def arr_rootfolder(_: str = Depends(verify_apikey)) -> Any: @arr_router.get("/tag", summary="标签") -def arr_tag(_: str = Depends(verify_apikey)) -> Any: +def arr_tag(_: Annotated[str, Depends(verify_apikey)]) -> Any: """ 模拟Radarr、Sonarr标签 """ @@ -142,7 +142,7 @@ def arr_tag(_: str = Depends(verify_apikey)) -> Any: @arr_router.get("/languageprofile", summary="语言") -def arr_languageprofile(_: str = Depends(verify_apikey)) -> Any: +def arr_languageprofile(_: Annotated[str, Depends(verify_apikey)]) -> Any: """ 模拟Radarr、Sonarr语言 """ @@ -168,7 +168,7 @@ def arr_languageprofile(_: str = Depends(verify_apikey)) -> Any: @arr_router.get("/movie", summary="所有订阅电影", response_model=List[schemas.RadarrMovie]) -def arr_movies(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -> Any: +def arr_movies(_: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Rardar电影 """ @@ -259,7 +259,7 @@ def arr_movies(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) - @arr_router.get("/movie/lookup", summary="查询电影", response_model=List[schemas.RadarrMovie]) -def arr_movie_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_movie_lookup(term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Rardar电影 term: `tmdb:${id}` 存在和不存在均不能返回错误 @@ -305,7 +305,7 @@ def arr_movie_lookup(term: str, db: Session = Depends(get_db), _: str = Depends( @arr_router.get("/movie/{mid}", summary="电影订阅详情", response_model=schemas.RadarrMovie) -def arr_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_movie(mid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Rardar电影订阅 """ @@ -331,9 +331,9 @@ def arr_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_a @arr_router.post("/movie", summary="新增电影订阅") -def arr_add_movie(movie: RadarrMovie, - db: Session = Depends(get_db), - _: str = Depends(verify_apikey) +def arr_add_movie(_: Annotated[str, Depends(verify_apikey)], + movie: RadarrMovie, + db: Session = Depends(get_db) ) -> Any: """ 新增Rardar电影订阅 @@ -362,7 +362,7 @@ def arr_add_movie(movie: RadarrMovie, @arr_router.delete("/movie/{mid}", summary="删除电影订阅", response_model=schemas.Response) -def arr_remove_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_remove_movie(mid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 删除Rardar电影订阅 """ @@ -378,7 +378,7 @@ def arr_remove_movie(mid: int, db: Session = Depends(get_db), _: str = Depends(v @arr_router.get("/series", summary="所有剧集", response_model=List[schemas.SonarrSeries]) -def arr_series(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) -> Any: +def arr_series(_: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Sonarr剧集 """ @@ -514,7 +514,7 @@ def arr_series(_: str = Depends(verify_apikey), db: Session = Depends(get_db)) - @arr_router.get("/series/lookup", summary="查询剧集") -def arr_series_lookup(term: str, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_series_lookup(term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Sonarr剧集 term: `tvdb:${id}` title """ @@ -603,7 +603,7 @@ def arr_series_lookup(term: str, db: Session = Depends(get_db), _: str = Depends @arr_router.get("/series/{tid}", summary="剧集详情") -def arr_serie(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_serie(tid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 查询Sonarr剧集 """ @@ -638,8 +638,8 @@ def arr_serie(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_a @arr_router.post("/series", summary="新增剧集订阅") def arr_add_series(tv: schemas.SonarrSeries, - db: Session = Depends(get_db), - _: str = Depends(verify_apikey)) -> Any: + _: Annotated[str, Depends(verify_apikey)], + db: Session = Depends(get_db)) -> Any: """ 新增Sonarr剧集订阅 """ @@ -689,7 +689,7 @@ def arr_update_series(tv: schemas.SonarrSeries) -> Any: @arr_router.delete("/series/{tid}", summary="删除剧集订阅") -def arr_remove_series(tid: int, db: Session = Depends(get_db), _: str = Depends(verify_apikey)) -> Any: +def arr_remove_series(tid: int, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)) -> Any: """ 删除Sonarr剧集订阅 """ diff --git a/app/chain/__init__.py b/app/chain/__init__.py index 62495e7d..9b6ed70b 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -94,10 +94,10 @@ class ChainBase(metaclass=ABCMeta): if isinstance(ret, tuple): return all(value is None for value in ret) else: - return result is None + return ret is None - logger.debug(f"请求模块执行:{method} ...") result = None + logger.debug(f"请求模块执行:{method} ...") modules = self.modulemanager.get_running_modules(method) # 按优先级排序 modules = sorted(modules, key=lambda x: x.get_priority()) @@ -110,6 +110,8 @@ class ChainBase(metaclass=ABCMeta): module_name = module_id try: func = getattr(module, method) + # 添加日志记录类型 + logger.debug(f"调用方法类型: {type(func)}") if is_result_empty(result): # 返回None,第一次执行或者需继续执行下一模块 result = func(*args, **kwargs) diff --git a/app/core/meta/metavideo.py b/app/core/meta/metavideo.py index 090a99a4..3c15483f 100644 --- a/app/core/meta/metavideo.py +++ b/app/core/meta/metavideo.py @@ -172,7 +172,7 @@ class MetaVideo(MetaBase): return None @staticmethod - def __is_pinyin(name_str: str) -> bool: + def __is_pinyin(name_str: Optional[str]) -> bool: """ 判断是否拼音 """ @@ -183,7 +183,7 @@ class MetaVideo(MetaBase): return False return True - def __fix_name(self, name: str): + def __fix_name(self, name: Optional[str]): """ 去掉名字中不需要的干扰字符 """ @@ -207,7 +207,7 @@ class MetaVideo(MetaBase): name = None return name - def __init_name(self, token: str): + def __init_name(self, token: Optional[str]): """ 识别名称 """ diff --git a/app/core/security.py b/app/core/security.py index 511972c7..ad7b19ce 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -12,7 +12,7 @@ import jwt from Crypto.Cipher import AES from Crypto.Util.Padding import pad from cryptography.fernet import Fernet -from fastapi import HTTPException, status, Security, Request, Response +from fastapi import HTTPException, status, Security, Request, Response, Depends from fastapi.security import OAuth2PasswordBearer, APIKeyHeader, APIKeyQuery, APIKeyCookie from passlib.context import CryptContext @@ -176,7 +176,7 @@ def __verify_token(token: str, purpose: str = "authentication") -> schemas.Token def verify_token( request: Request, response: Response, - token: str = Security(oauth2_scheme) + token: Annotated[str, Security(oauth2_scheme)] ) -> schemas.TokenPayload: """ 验证 JWT 令牌并自动处理 resource_token 写入 @@ -196,7 +196,7 @@ def verify_token( def verify_resource_token( - resource_token: str = Security(resource_token_cookie) + resource_token: Annotated[str, Security(resource_token_cookie)] ) -> schemas.TokenPayload: """ 验证资源访问令牌(从 Cookie 中获取) @@ -249,7 +249,7 @@ def __verify_key(key: str, expected_key: str, key_type: str) -> str: return key -def verify_apitoken(token: str = Security(__get_api_token)) -> str: +def verify_apitoken(token: Annotated[str, Security(__get_api_token)]) -> str: """ 使用 API Token 进行身份认证 :param token: API Token,从 URL 查询参数中获取 @@ -258,7 +258,7 @@ def verify_apitoken(token: str = Security(__get_api_token)) -> str: return __verify_key(token, settings.API_TOKEN, "API_TOKEN") -def verify_apikey(apikey: str = Security(__get_api_key)) -> str: +def verify_apikey(apikey: Annotated[str, Security(__get_api_key)]) -> str: """ 使用 API Key 进行身份认证 :param apikey: API Key,从 URL 查询参数或请求头中获取 diff --git a/app/helper/module.py b/app/helper/module.py index c4f05177..d9f5c1fc 100644 --- a/app/helper/module.py +++ b/app/helper/module.py @@ -3,18 +3,26 @@ import importlib import pkgutil import traceback from pathlib import Path -from typing import List, Any +from typing import List, Any, Callable from app.log import logger +FilterFuncType = Callable[[str, Any], bool] + +def _default_filter(name: str, obj: Any) -> bool: + """ + 默认过滤器 + """ + return True + class ModuleHelper: """ 模块动态加载 """ @classmethod - def load(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]: + def load(cls, package_path: str, filter_func: FilterFuncType = _default_filter) -> List[Any]: """ 导入模块 :param package_path: 父包名 @@ -46,7 +54,7 @@ class ModuleHelper: return submodules @classmethod - def load_with_pre_filter(cls, package_path: str, filter_func=lambda name, obj: True) -> List[Any]: + def load_with_pre_filter(cls, package_path: str, filter_func: FilterFuncType = _default_filter) -> List[Any]: """ 导入子模块 :param package_path: 父包名 diff --git a/app/helper/progress.py b/app/helper/progress.py index 67294f6c..e852106f 100644 --- a/app/helper/progress.py +++ b/app/helper/progress.py @@ -40,7 +40,7 @@ class ProgressHelper(metaclass=Singleton): "text": "正在处理..." } - def update(self, key: Union[ProgressKey, str], value: float = None, text: str = None): + def update(self, key: Union[ProgressKey, str], value: Union[float, int] = None, text: str = None): if isinstance(key, Enum): key = key.value if not self._process_detail.get(key, {}).get('enable'): diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 27a725f7..955dcae3 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -29,7 +29,6 @@ class _ModuleBase(metaclass=ABCMeta): pass @staticmethod - @abstractmethod def get_name() -> str: """ 获取模块名称 @@ -37,7 +36,6 @@ class _ModuleBase(metaclass=ABCMeta): pass @staticmethod - @abstractmethod def get_type() -> ModuleType: """ 获取模块类型 @@ -45,7 +43,6 @@ class _ModuleBase(metaclass=ABCMeta): pass @staticmethod - @abstractmethod def get_subtype() -> Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]: """ 获取模块子类型(下载器、媒体服务器、消息通道、存储类型、其他杂项模块类型) @@ -53,7 +50,6 @@ class _ModuleBase(metaclass=ABCMeta): pass @staticmethod - @abstractmethod def get_priority() -> int: """ 获取模块优先级,数字越小优先级越高,只有同一接口下优先级才生效 diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 59b1f625..740901af 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -148,12 +148,12 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): servers = [(server, self.get_instance(server))] else: servers = self.get_instances().items() - for name, server in servers: + for name, s in servers: if not server: continue if mediainfo.type == MediaType.MOVIE: if itemid: - movie = server.get_iteminfo(itemid) + movie = s.get_iteminfo(itemid) if movie: logger.info(f"媒体库 {name} 中找到了 {movie}") return schemas.ExistMediaInfo( @@ -162,9 +162,9 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): server=name, itemid=movie.item_id ) - movies = server.get_movies(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id) + movies = s.get_movies(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id) if not movies: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -177,10 +177,10 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): itemid=movies[0].item_id ) else: - itemid, tvs = server.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) + itemid, tvs = s.get_tv_episodes(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) if not tvs: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -207,11 +207,11 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): else: servers = self.get_instances().values() media_statistics = [] - for server in servers: - media_statistic = server.get_medias_count() + for s in servers: + media_statistic = s.get_medias_count() if not media_statistic: continue - media_statistic.user_count = server.get_user_count() + media_statistic.user_count = s.get_user_count() media_statistics.append(media_statistic) return media_statistics diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index a781b500..084b568c 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -149,12 +149,12 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): servers = [(server, self.get_instance(server))] else: servers = self.get_instances().items() - for name, server in servers: + for name, s in servers: if not server: continue if mediainfo.type == MediaType.MOVIE: if itemid: - movie = server.get_iteminfo(itemid) + movie = s.get_iteminfo(itemid) if movie: logger.info(f"媒体库 {name} 中找到了 {movie}") return schemas.ExistMediaInfo( @@ -163,7 +163,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): server=name, itemid=movie.item_id ) - movies = server.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id) + movies = s.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id) if not movies: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -176,10 +176,10 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): itemid=movies[0].item_id ) else: - itemid, tvs = server.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) + itemid, tvs = s.get_tv_episodes(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) if not tvs: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -206,11 +206,11 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): else: servers = self.get_instances().values() media_statistics = [] - for server in servers: - media_statistic = server.get_medias_count() + for s in servers: + media_statistic = s.get_medias_count() if not media_statistic: continue - media_statistic.user_count = server.get_user_count() + media_statistic.user_count = s.get_user_count() media_statistics.append(media_statistic) return media_statistics diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index 3fecf029..c61c6c37 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -152,12 +152,12 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): servers = [(server, self.get_instance(server))] else: servers = self.get_instances().items() - for name, server in servers: + for name, s in servers: if not server: continue if mediainfo.type == MediaType.MOVIE: if itemid: - movie = server.get_iteminfo(itemid) + movie = s.get_iteminfo(itemid) if movie: logger.info(f"媒体库 {name} 中找到了 {movie}") return schemas.ExistMediaInfo( @@ -166,10 +166,10 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): server=name, itemid=movie.item_id ) - movies = server.get_movies(title=mediainfo.title, - original_title=mediainfo.original_title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id) + movies = s.get_movies(title=mediainfo.title, + original_title=mediainfo.original_title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id) if not movies: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -182,11 +182,11 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): itemid=movies[0].item_id ) else: - item_id, tvs = server.get_tv_episodes(title=mediainfo.title, - original_title=mediainfo.original_title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) + item_id, tvs = s.get_tv_episodes(title=mediainfo.title, + original_title=mediainfo.original_title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) if not tvs: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") continue @@ -213,8 +213,8 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): else: servers = self.get_instances().values() media_statistics = [] - for server in servers: - media_statistic = server.get_medias_count() + for s in servers: + media_statistic = s.get_medias_count() if not media_statistic: continue media_statistic.user_count = 1 diff --git a/app/modules/themoviedb/category.py b/app/modules/themoviedb/category.py index 1852986b..72cd201d 100644 --- a/app/modules/themoviedb/category.py +++ b/app/modules/themoviedb/category.py @@ -1,7 +1,9 @@ import shutil from pathlib import Path +from typing import Union import ruamel.yaml +from ruamel.yaml import CommentedMap from app.core.config import settings from app.log import logger @@ -95,7 +97,7 @@ class CategoryHelper(metaclass=Singleton): return self.get_category(self._tv_categorys, tmdb_info) @staticmethod - def get_category(categorys: dict, tmdb_info: dict) -> str: + def get_category(categorys: Union[dict, CommentedMap], tmdb_info: dict) -> str: """ 根据 TMDB信息与分类配置文件进行比较,确定所属分类 :param categorys: 分类配置 diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py index ad0720ee..973bafdc 100644 --- a/app/plugins/__init__.py +++ b/app/plugins/__init__.py @@ -63,7 +63,6 @@ class _PluginBase(metaclass=ABCMeta): pass @staticmethod - @abstractmethod def get_command() -> List[Dict[str, Any]]: """ 注册插件远程命令 diff --git a/app/utils/object.py b/app/utils/object.py index edca9b1d..08bcc07f 100644 --- a/app/utils/object.py +++ b/app/utils/object.py @@ -1,6 +1,7 @@ +import dis import inspect from types import FunctionType -from typing import Any, Callable +from typing import Any, Callable, get_type_hints class ObjectUtils: @@ -43,24 +44,45 @@ class ObjectUtils: 检查函数是否已实现 """ try: + # 尝试通过源代码分析 source = inspect.getsource(func) in_comment = False for line in source.split('\n'): line = line.strip() + # 跳过空行 if not line: continue - if line.startswith('"""') or line.startswith("'''"): + # 处理多行注释 + if line.startswith(('"""', "'''")): in_comment = not in_comment continue - if not in_comment and not (line.startswith('#') - or line == "pass" - or line.startswith('@') - or line.startswith('def ')): - return True - except Exception as err: - print(str(err)) - return func.__code__.co_code not in [b'd\x01S\x00', b'\x97\x00d\x00S\x00'] - return False + # 在注释中则跳过 + if in_comment: + continue + # 跳过注释、pass语句、装饰器、函数定义行 + if line.startswith('#') or line == "pass" or line.startswith('@') or line.startswith('def '): + continue + # 发现有效代码行 + return True + # 没有有效代码行 + return False + except Exception: + # 源代码分析失败时,进行字节码分析 + code_obj = func.__code__ + instructions = list(dis.get_instructions(code_obj)) + # 检查是否为仅返回None的简单结构 + if len(instructions) == 2: + first, second = instructions + if (first.opname == 'LOAD_CONST' and + second.opname == 'RETURN_VALUE'): + # 验证加载的常量是否为None + const_index = first.arg + if (const_index < len(code_obj.co_consts) and + code_obj.co_consts[const_index] is None): + # 未实现的空函数 + return False + # 其他情况认为已实现 + return True @staticmethod def check_signature(func: FunctionType, *args) -> bool: @@ -70,11 +92,35 @@ class ObjectUtils: # 获取函数的参数信息 signature = inspect.signature(func) parameters = signature.parameters - - # 检查输入参数个数和类型是否一致 if len(args) != len(parameters): return False - for arg, param in zip(args, parameters.values()): - if not isinstance(arg, param.annotation): + try: + # 获取解析后的类型提示 + type_hints = get_type_hints(func) + except TypeError: + type_hints = {} + for arg, (param_name, param) in zip(args, parameters.items()): + # 优先使用解析后的类型提示 + param_type = type_hints.get(param_name, None) + if param_type is None: + # 处理原始注解(可能为字符串或Cython类型) + param_annotation = param.annotation + if param_annotation is inspect.Parameter.empty: + continue + # 处理字符串类型的注解 + if isinstance(param_annotation, str): + # 尝试解析字符串为实际类型 + module = inspect.getmodule(func) + global_vars = module.__dict__ if module else globals() + try: + param_type = eval(param_annotation, global_vars) + except Exception as err: + print(str(err)) + continue + else: + param_type = param_annotation + if param_type is None: + continue + if not isinstance(arg, param_type): return False return True diff --git a/app/utils/string.py b/app/utils/string.py index 680df0aa..527626f8 100644 --- a/app/utils/string.py +++ b/app/utils/string.py @@ -207,7 +207,7 @@ class StringUtils: return [StringUtils.clear(x) for x in text] @staticmethod - def clear_upper(text: str) -> str: + def clear_upper(text: Optional[str]) -> str: """ 去除特殊字符,同时大写 """ @@ -596,7 +596,7 @@ class StringUtils: return mtype, key_word, season_num, episode_num, year, content @staticmethod - def str_title(s: str) -> str: + def str_title(s: Optional[str]) -> str: """ 大写首字母兼容None """ diff --git a/requirements.in b/requirements.in index f33ab713..419652f5 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ -Cython~=3.0.2 +Cython~=3.0.12 pydantic~=1.10.13 SQLAlchemy~=2.0.15 uvicorn~=0.22.0 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..428ee37e --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, Extension +from Cython.Build import cythonize +import glob + +# 递归获取所有.py文件 +sources = glob.glob("app/**/*.py", recursive=True) + +# 移除不需要编译的文件 +sources.remove("app/main.py") + +# 配置编译参数(可选优化选项) +extensions = [ + Extension( + name=path.replace("/", ".").replace(".py", ""), + sources=[path], + extra_compile_args=["-O3", "-ffast-math"], + ) + for path in sources +] + +setup( + name="MoviePilot", + author="jxxghp", + ext_modules=cythonize( + extensions, + build_dir="cython_cache", + compiler_directives={ + "language_level": "3", + "auto_pickle": False, + "embedsignature": True, + "annotation_typing": True, + "infer_types": False + }, + annotate=True + ), + script_args=["build_ext", "-j8", "--inplace"], +)