mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
feat:推荐使用异步API
This commit is contained in:
@@ -3,11 +3,11 @@ from typing import Any, List, Optional
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
|
from app.chain.recommend import RecommendChain
|
||||||
from app.core.event import eventmanager
|
from app.core.event import eventmanager
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas.types import ChainEventType
|
|
||||||
from app.chain.recommend import RecommendChain
|
|
||||||
from app.schemas import RecommendSourceEventData
|
from app.schemas import RecommendSourceEventData
|
||||||
|
from app.schemas.types import ChainEventType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -29,27 +29,27 @@ def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/bangumi_calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
|
@router.get("/bangumi_calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
|
||||||
def bangumi_calendar(page: Optional[int] = 1,
|
async def bangumi_calendar(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览Bangumi每日放送
|
浏览Bangumi每日放送
|
||||||
"""
|
"""
|
||||||
return RecommendChain().bangumi_calendar(page=page, count=count)
|
return await RecommendChain().async_bangumi_calendar(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
||||||
def douban_showing(page: Optional[int] = 1,
|
async def douban_showing(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣正在热映
|
浏览豆瓣正在热映
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_movie_showing(page=page, count=count)
|
return await RecommendChain().async_douban_movie_showing(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
||||||
def douban_movies(sort: Optional[str] = "R",
|
async def douban_movies(sort: Optional[str] = "R",
|
||||||
tags: Optional[str] = "",
|
tags: Optional[str] = "",
|
||||||
page: Optional[int] = 1,
|
page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
@@ -57,11 +57,11 @@ def douban_movies(sort: Optional[str] = "R",
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣电影信息
|
浏览豆瓣电影信息
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count)
|
return await RecommendChain().async_douban_movies(sort=sort, tags=tags, page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
||||||
def douban_tvs(sort: Optional[str] = "R",
|
async def douban_tvs(sort: Optional[str] = "R",
|
||||||
tags: Optional[str] = "",
|
tags: Optional[str] = "",
|
||||||
page: Optional[int] = 1,
|
page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
@@ -69,71 +69,71 @@ def douban_tvs(sort: Optional[str] = "R",
|
|||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count)
|
return await RecommendChain().async_douban_tvs(sort=sort, tags=tags, page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
||||||
def douban_movie_top250(page: Optional[int] = 1,
|
async def douban_movie_top250(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
浏览豆瓣剧集信息
|
浏览豆瓣剧集信息
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_movie_top250(page=page, count=count)
|
return await RecommendChain().async_douban_movie_top250(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
def douban_tv_weekly_chinese(page: Optional[int] = 1,
|
async def douban_tv_weekly_chinese(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
中国每周剧集口碑榜
|
中国每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_tv_weekly_chinese(page=page, count=count)
|
return await RecommendChain().async_douban_tv_weekly_chinese(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
def douban_tv_weekly_global(page: Optional[int] = 1,
|
async def douban_tv_weekly_global(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
全球每周剧集口碑榜
|
全球每周剧集口碑榜
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_tv_weekly_global(page=page, count=count)
|
return await RecommendChain().async_douban_tv_weekly_global(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
||||||
def douban_tv_animation(page: Optional[int] = 1,
|
async def douban_tv_animation(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
热门动画剧集
|
热门动画剧集
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_tv_animation(page=page, count=count)
|
return await RecommendChain().async_douban_tv_animation(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
||||||
def douban_movie_hot(page: Optional[int] = 1,
|
async def douban_movie_hot(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
热门电影
|
热门电影
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_movie_hot(page=page, count=count)
|
return await RecommendChain().async_douban_movie_hot(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
@router.get("/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
||||||
def douban_tv_hot(page: Optional[int] = 1,
|
async def douban_tv_hot(page: Optional[int] = 1,
|
||||||
count: Optional[int] = 30,
|
count: Optional[int] = 30,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
热门电视剧
|
热门电视剧
|
||||||
"""
|
"""
|
||||||
return RecommendChain().douban_tv_hot(page=page, count=count)
|
return await RecommendChain().async_douban_tv_hot(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
|
@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
|
async def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
|
||||||
with_genres: Optional[str] = "",
|
with_genres: Optional[str] = "",
|
||||||
with_original_language: Optional[str] = "",
|
with_original_language: Optional[str] = "",
|
||||||
with_keywords: Optional[str] = "",
|
with_keywords: Optional[str] = "",
|
||||||
@@ -146,7 +146,7 @@ def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
|
|||||||
"""
|
"""
|
||||||
浏览TMDB电影信息
|
浏览TMDB电影信息
|
||||||
"""
|
"""
|
||||||
return RecommendChain().tmdb_movies(sort_by=sort_by,
|
return await RecommendChain().async_tmdb_movies(sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
with_keywords=with_keywords,
|
with_keywords=with_keywords,
|
||||||
@@ -158,7 +158,7 @@ def tmdb_movies(sort_by: Optional[str] = "popularity.desc",
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
|
async def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
|
||||||
with_genres: Optional[str] = "",
|
with_genres: Optional[str] = "",
|
||||||
with_original_language: Optional[str] = "",
|
with_original_language: Optional[str] = "",
|
||||||
with_keywords: Optional[str] = "",
|
with_keywords: Optional[str] = "",
|
||||||
@@ -171,7 +171,7 @@ def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
|
|||||||
"""
|
"""
|
||||||
浏览TMDB剧集信息
|
浏览TMDB剧集信息
|
||||||
"""
|
"""
|
||||||
return RecommendChain().tmdb_tvs(sort_by=sort_by,
|
return await RecommendChain().async_tmdb_tvs(sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
with_keywords=with_keywords,
|
with_keywords=with_keywords,
|
||||||
@@ -183,9 +183,9 @@ def tmdb_tvs(sort_by: Optional[str] = "popularity.desc",
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
@router.get("/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
||||||
def tmdb_trending(page: Optional[int] = 1,
|
async def tmdb_trending(page: Optional[int] = 1,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
TMDB流行趋势
|
TMDB流行趋势
|
||||||
"""
|
"""
|
||||||
return RecommendChain().tmdb_trending(page=page)
|
return await RecommendChain().async_tmdb_trending(page=page)
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import asyncio
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import tempfile
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Union, Annotated
|
from typing import Optional, Union, Annotated
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
import pillow_avif # noqa 用于自动注册AVIF支持
|
import pillow_avif # noqa 用于自动注册AVIF支持
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from aiopath import AsyncPath
|
from aiopath import AsyncPath
|
||||||
from app.helper.sites import SitesHelper # noqa # noqa
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Header, Request, Response
|
from fastapi import APIRouter, Body, Depends, HTTPException, Header, Request, Response
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
@@ -29,6 +28,7 @@ from app.helper.mediaserver import MediaServerHelper
|
|||||||
from app.helper.message import MessageHelper
|
from app.helper.message import MessageHelper
|
||||||
from app.helper.progress import ProgressHelper
|
from app.helper.progress import ProgressHelper
|
||||||
from app.helper.rule import RuleHelper
|
from app.helper.rule import RuleHelper
|
||||||
|
from app.helper.sites import SitesHelper # noqa # noqa
|
||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.subscribe import SubscribeHelper
|
||||||
from app.helper.system import SystemHelper
|
from app.helper.system import SystemHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@@ -121,8 +121,8 @@ async def fetch_image(
|
|||||||
try:
|
try:
|
||||||
if not await cache_path.parent.exists():
|
if not await cache_path.parent.exists():
|
||||||
await cache_path.parent.mkdir(parents=True, exist_ok=True)
|
await cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with tempfile.NamedTemporaryFile(dir=cache_path.parent, delete=False) as tmp_file:
|
async with aiofiles.tempfile.NamedTemporaryFile(dir=cache_path.parent, delete=False) as tmp_file:
|
||||||
tmp_file.write(content)
|
await tmp_file.write(content)
|
||||||
temp_path = AsyncPath(tmp_file.name)
|
temp_path = AsyncPath(tmp_file.name)
|
||||||
await temp_path.replace(cache_path)
|
await temp_path.replace(cache_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+319
-121
@@ -1,10 +1,11 @@
|
|||||||
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
import pillow_avif # noqa 用于自动注册AVIF支持
|
import pillow_avif # noqa 用于自动注册AVIF支持
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from aiopath import AsyncPath
|
||||||
|
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.chain.bangumi import BangumiChain
|
from app.chain.bangumi import BangumiChain
|
||||||
@@ -15,7 +16,7 @@ from app.core.config import settings, global_vars
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
from app.utils.common import log_execution_time
|
from app.utils.common import log_execution_time
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import AsyncRequestUtils
|
||||||
from app.utils.security import SecurityUtils
|
from app.utils.security import SecurityUtils
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
@@ -34,127 +35,13 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
def refresh_recommend(self):
|
def refresh_recommend(self):
|
||||||
"""
|
"""
|
||||||
刷新推荐
|
刷新推荐数据 - 同步包装器
|
||||||
"""
|
"""
|
||||||
logger.debug("Starting to refresh Recommend data.")
|
|
||||||
cache_backend.clear(region=recommend_cache_region)
|
|
||||||
logger.debug("Recommend Cache has been cleared.")
|
|
||||||
|
|
||||||
# 推荐来源方法
|
|
||||||
recommend_methods = [
|
|
||||||
self.tmdb_movies,
|
|
||||||
self.tmdb_tvs,
|
|
||||||
self.tmdb_trending,
|
|
||||||
self.bangumi_calendar,
|
|
||||||
self.douban_movie_showing,
|
|
||||||
self.douban_movies,
|
|
||||||
self.douban_tvs,
|
|
||||||
self.douban_movie_top250,
|
|
||||||
self.douban_tv_weekly_chinese,
|
|
||||||
self.douban_tv_weekly_global,
|
|
||||||
self.douban_tv_animation,
|
|
||||||
self.douban_movie_hot,
|
|
||||||
self.douban_tv_hot,
|
|
||||||
]
|
|
||||||
|
|
||||||
# 缓存并刷新所有推荐数据
|
|
||||||
recommends = []
|
|
||||||
# 记录哪些方法已完成
|
|
||||||
methods_finished = set()
|
|
||||||
# 这里避免区间内连续调用相同来源,因此遍历方案为每页遍历所有推荐来源,再进行页数遍历
|
|
||||||
for page in range(1, self.cache_max_pages + 1):
|
|
||||||
for method in recommend_methods:
|
|
||||||
if global_vars.is_system_stopped:
|
|
||||||
return
|
|
||||||
if method in methods_finished:
|
|
||||||
continue
|
|
||||||
logger.debug(f"Fetch {method.__name__} data for page {page}.")
|
|
||||||
data = method(page=page)
|
|
||||||
if not data:
|
|
||||||
logger.debug("All recommendation methods have finished fetching data. Ending pagination early.")
|
|
||||||
methods_finished.add(method)
|
|
||||||
continue
|
|
||||||
recommends.extend(data)
|
|
||||||
# 如果所有方法都已经完成,提前结束循环
|
|
||||||
if len(methods_finished) == len(recommend_methods):
|
|
||||||
break
|
|
||||||
|
|
||||||
# 缓存收集到的海报
|
|
||||||
self.__cache_posters(recommends)
|
|
||||||
logger.debug("Recommend data refresh completed.")
|
|
||||||
|
|
||||||
def __cache_posters(self, datas: List[dict]):
|
|
||||||
"""
|
|
||||||
提取 poster_path 并缓存图片
|
|
||||||
:param datas: 数据列表
|
|
||||||
"""
|
|
||||||
if not settings.GLOBAL_IMAGE_CACHE:
|
|
||||||
return
|
|
||||||
|
|
||||||
for data in datas:
|
|
||||||
if global_vars.is_system_stopped:
|
|
||||||
return
|
|
||||||
poster_path = data.get("poster_path")
|
|
||||||
if poster_path:
|
|
||||||
poster_url = poster_path.replace("original", "w500")
|
|
||||||
logger.debug(f"Caching poster image: {poster_url}")
|
|
||||||
self.__fetch_and_save_image(poster_url)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __fetch_and_save_image(url: str):
|
|
||||||
"""
|
|
||||||
请求并保存图片
|
|
||||||
:param url: 图片路径
|
|
||||||
"""
|
|
||||||
if not settings.GLOBAL_IMAGE_CACHE or not url:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 生成缓存路径
|
|
||||||
sanitized_path = SecurityUtils.sanitize_url_path(url)
|
|
||||||
cache_path = settings.CACHE_PATH / "images" / sanitized_path
|
|
||||||
|
|
||||||
# 没有文件类型,则添加后缀,在恶意文件类型和实际需求下的折衷选择
|
|
||||||
if not cache_path.suffix:
|
|
||||||
cache_path = cache_path.with_suffix(".jpg")
|
|
||||||
|
|
||||||
# 确保缓存路径和文件类型合法
|
|
||||||
if not SecurityUtils.is_safe_path(settings.CACHE_PATH, cache_path, settings.SECURITY_IMAGE_SUFFIXES):
|
|
||||||
logger.debug(f"Invalid cache path or file type for URL: {url}, sanitized path: {sanitized_path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 本地存在缓存图片,则直接跳过
|
|
||||||
if cache_path.exists():
|
|
||||||
logger.debug(f"Cache hit: Image already exists at {cache_path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 请求远程图片
|
|
||||||
referer = "https://movie.douban.com/" if "doubanio.com" in url else None
|
|
||||||
proxies = settings.PROXY if not referer else None
|
|
||||||
response = RequestUtils(ua=settings.NORMAL_USER_AGENT, proxies=proxies, referer=referer).get_res(url=url)
|
|
||||||
if not response:
|
|
||||||
logger.debug(f"Empty response for URL: {url}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 验证下载的内容是否为有效图片
|
|
||||||
try:
|
try:
|
||||||
Image.open(io.BytesIO(response.content)).verify()
|
asyncio.run(self.async_refresh_recommend())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Invalid image format for URL {url}: {e}")
|
logger.error(f"刷新推荐数据失败:{str(e)}")
|
||||||
return
|
raise
|
||||||
|
|
||||||
if not cache_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not cache_path.parent.exists():
|
|
||||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with tempfile.NamedTemporaryFile(dir=cache_path.parent, delete=False) as tmp_file:
|
|
||||||
tmp_file.write(response.content)
|
|
||||||
temp_path = Path(tmp_file.name)
|
|
||||||
temp_path.replace(cache_path)
|
|
||||||
logger.debug(f"Successfully cached image at {cache_path} for URL: {url}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Failed to write cache file {cache_path} for URL {url}: {e}")
|
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
@@ -310,3 +197,314 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
tvs = DoubanChain().tv_hot(page=page, count=count)
|
tvs = DoubanChain().tv_hot(page=page, count=count)
|
||||||
return [media.to_dict() for media in tvs] if tvs else []
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
# 异步版本的方法
|
||||||
|
async def async_refresh_recommend(self):
|
||||||
|
"""
|
||||||
|
异步刷新推荐
|
||||||
|
"""
|
||||||
|
logger.debug("Starting to async refresh Recommend data.")
|
||||||
|
cache_backend.clear(region=recommend_cache_region)
|
||||||
|
logger.debug("Recommend Cache has been cleared.")
|
||||||
|
|
||||||
|
# 推荐来源方法
|
||||||
|
recommend_methods = [
|
||||||
|
self.async_tmdb_movies,
|
||||||
|
self.async_tmdb_tvs,
|
||||||
|
self.async_tmdb_trending,
|
||||||
|
self.async_bangumi_calendar,
|
||||||
|
self.async_douban_movie_showing,
|
||||||
|
self.async_douban_movies,
|
||||||
|
self.async_douban_tvs,
|
||||||
|
self.async_douban_movie_top250,
|
||||||
|
self.async_douban_tv_weekly_chinese,
|
||||||
|
self.async_douban_tv_weekly_global,
|
||||||
|
self.async_douban_tv_animation,
|
||||||
|
self.async_douban_movie_hot,
|
||||||
|
self.async_douban_tv_hot,
|
||||||
|
]
|
||||||
|
|
||||||
|
# 缓存并刷新所有推荐数据
|
||||||
|
recommends = []
|
||||||
|
# 记录哪些方法已完成
|
||||||
|
methods_finished = set()
|
||||||
|
# 这里避免区间内连续调用相同来源,因此遍历方案为每页遍历所有推荐来源,再进行页数遍历
|
||||||
|
for page in range(1, self.cache_max_pages + 1):
|
||||||
|
# 为每个页面并发执行所有方法
|
||||||
|
tasks = []
|
||||||
|
for method in recommend_methods:
|
||||||
|
if global_vars.is_system_stopped:
|
||||||
|
return
|
||||||
|
if method in methods_finished:
|
||||||
|
continue
|
||||||
|
tasks.append(self._async_fetch_method_data(method, page, methods_finished))
|
||||||
|
|
||||||
|
# 并发执行所有任务
|
||||||
|
if tasks:
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
for result in results:
|
||||||
|
if isinstance(result, list) and result:
|
||||||
|
recommends.extend(result)
|
||||||
|
|
||||||
|
# 如果所有方法都已经完成,提前结束循环
|
||||||
|
if len(methods_finished) == len(recommend_methods):
|
||||||
|
break
|
||||||
|
|
||||||
|
# 缓存收集到的海报
|
||||||
|
await self.__async_cache_posters(recommends)
|
||||||
|
logger.debug("Async recommend data refresh completed.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _async_fetch_method_data(method, page: int, methods_finished: set):
|
||||||
|
"""
|
||||||
|
异步获取方法数据的辅助函数
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.debug(f"Async fetch {method.__name__} data for page {page}.")
|
||||||
|
data = await method(page=page)
|
||||||
|
if not data:
|
||||||
|
logger.debug(f"Method {method.__name__} finished fetching data. Ending pagination early.")
|
||||||
|
methods_finished.add(method)
|
||||||
|
return []
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching data from {method.__name__}: {e}")
|
||||||
|
methods_finished.add(method)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def __async_cache_posters(self, datas: List[dict]):
|
||||||
|
"""
|
||||||
|
异步提取 poster_path 并缓存图片
|
||||||
|
:param datas: 数据列表
|
||||||
|
"""
|
||||||
|
if not settings.GLOBAL_IMAGE_CACHE:
|
||||||
|
return
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for data in datas:
|
||||||
|
if global_vars.is_system_stopped:
|
||||||
|
return
|
||||||
|
poster_path = data.get("poster_path")
|
||||||
|
if poster_path:
|
||||||
|
poster_url = poster_path.replace("original", "w500")
|
||||||
|
logger.debug(f"Async caching poster image: {poster_url}")
|
||||||
|
tasks.append(self.__async_fetch_and_save_image(poster_url))
|
||||||
|
|
||||||
|
# 并发缓存图片
|
||||||
|
if tasks:
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def __async_fetch_and_save_image(url: str):
|
||||||
|
"""
|
||||||
|
异步请求并保存图片
|
||||||
|
:param url: 图片路径
|
||||||
|
"""
|
||||||
|
if not settings.GLOBAL_IMAGE_CACHE or not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 生成缓存路径
|
||||||
|
base_path = AsyncPath(settings.CACHE_PATH)
|
||||||
|
sanitized_path = SecurityUtils.sanitize_url_path(url)
|
||||||
|
cache_path = base_path / "images" / sanitized_path
|
||||||
|
|
||||||
|
# 没有文件类型,则添加后缀,在恶意文件类型和实际需求下的折衷选择
|
||||||
|
if not cache_path.suffix:
|
||||||
|
cache_path = cache_path.with_suffix(".jpg")
|
||||||
|
|
||||||
|
# 确保缓存路径和文件类型合法
|
||||||
|
if not await SecurityUtils.async_is_safe_path(base_path=base_path,
|
||||||
|
user_path=cache_path,
|
||||||
|
allowed_suffixes=settings.SECURITY_IMAGE_SUFFIXES):
|
||||||
|
logger.debug(f"Invalid cache path or file type for URL: {url}, sanitized path: {sanitized_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 本地存在缓存图片,则直接跳过
|
||||||
|
if await cache_path.exists():
|
||||||
|
logger.debug(f"Cache hit: Image already exists at {cache_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 请求远程图片
|
||||||
|
referer = "https://movie.douban.com/" if "doubanio.com" in url else None
|
||||||
|
proxies = settings.PROXY if not referer else None
|
||||||
|
response = await AsyncRequestUtils(ua=settings.NORMAL_USER_AGENT,
|
||||||
|
proxies=proxies, referer=referer).get_res(url=url)
|
||||||
|
if not response:
|
||||||
|
logger.debug(f"Empty response for URL: {url}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 验证下载的内容是否为有效图片
|
||||||
|
try:
|
||||||
|
Image.open(io.BytesIO(response.content)).verify()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Invalid image format for URL {url}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not cache_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await cache_path.parent.exists():
|
||||||
|
await cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
async with aiofiles.tempfile.NamedTemporaryFile(dir=cache_path.parent, delete=False) as tmp_file:
|
||||||
|
await tmp_file.write(response.content)
|
||||||
|
temp_path = AsyncPath(tmp_file.name)
|
||||||
|
await temp_path.replace(cache_path)
|
||||||
|
logger.debug(f"Successfully cached image at {cache_path} for URL: {url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to write cache file {cache_path} for URL {url}: {e}")
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_tmdb_movies(self, sort_by: Optional[str] = "popularity.desc",
|
||||||
|
with_genres: Optional[str] = "",
|
||||||
|
with_original_language: Optional[str] = "",
|
||||||
|
with_keywords: Optional[str] = "",
|
||||||
|
with_watch_providers: Optional[str] = "",
|
||||||
|
vote_average: Optional[float] = 0.0,
|
||||||
|
vote_count: Optional[int] = 0,
|
||||||
|
release_date: Optional[str] = "",
|
||||||
|
page: Optional[int] = 1) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步TMDB热门电影
|
||||||
|
"""
|
||||||
|
movies = await TmdbChain().async_run_module("async_tmdb_discover", mtype=MediaType.MOVIE,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
return [movie.to_dict() for movie in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_tmdb_tvs(self, sort_by: Optional[str] = "popularity.desc",
|
||||||
|
with_genres: Optional[str] = "",
|
||||||
|
with_original_language: Optional[str] = "zh|en|ja|ko",
|
||||||
|
with_keywords: Optional[str] = "",
|
||||||
|
with_watch_providers: Optional[str] = "",
|
||||||
|
vote_average: Optional[float] = 0.0,
|
||||||
|
vote_count: Optional[int] = 0,
|
||||||
|
release_date: Optional[str] = "",
|
||||||
|
page: Optional[int] = 1) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步TMDB热门电视剧
|
||||||
|
"""
|
||||||
|
tvs = await TmdbChain().async_run_module("async_tmdb_discover", mtype=MediaType.TV,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_tmdb_trending(self, page: Optional[int] = 1) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步TMDB流行趋势
|
||||||
|
"""
|
||||||
|
infos = await TmdbChain().async_run_module("async_tmdb_trending", page=page)
|
||||||
|
return [info.to_dict() for info in infos] if infos else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_bangumi_calendar(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步Bangumi每日放送
|
||||||
|
"""
|
||||||
|
medias = await BangumiChain().async_run_module("async_bangumi_calendar")
|
||||||
|
return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_movie_showing(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣正在热映
|
||||||
|
"""
|
||||||
|
movies = await DoubanChain().async_run_module("async_movie_showing", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_movies(self, sort: Optional[str] = "R", tags: Optional[str] = "",
|
||||||
|
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣最新电影
|
||||||
|
"""
|
||||||
|
movies = await DoubanChain().async_run_module("async_douban_discover", mtype=MediaType.MOVIE,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_tvs(self, sort: Optional[str] = "R", tags: Optional[str] = "",
|
||||||
|
page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣最新电视剧
|
||||||
|
"""
|
||||||
|
tvs = await DoubanChain().async_run_module("async_douban_discover", mtype=MediaType.TV,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_movie_top250(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣电影TOP250
|
||||||
|
"""
|
||||||
|
movies = await DoubanChain().async_run_module("async_movie_top250", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_tv_weekly_chinese(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣国产剧集榜
|
||||||
|
"""
|
||||||
|
tvs = await DoubanChain().async_run_module("async_tv_weekly_chinese", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_tv_weekly_global(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣全球剧集榜
|
||||||
|
"""
|
||||||
|
tvs = await DoubanChain().async_run_module("async_tv_weekly_global", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_tv_animation(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣热门动漫
|
||||||
|
"""
|
||||||
|
tvs = await DoubanChain().async_run_module("async_tv_animation", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_movie_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣热门电影
|
||||||
|
"""
|
||||||
|
movies = await DoubanChain().async_run_module("async_movie_hot", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
@log_execution_time(logger=logger)
|
||||||
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
|
async def async_douban_tv_hot(self, page: Optional[int] = 1, count: Optional[int] = 30) -> List[dict]:
|
||||||
|
"""
|
||||||
|
异步豆瓣热门电视剧
|
||||||
|
"""
|
||||||
|
tvs = await DoubanChain().async_run_module("async_tv_hot", page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|||||||
@@ -782,6 +782,152 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
return [MediaInfo(tmdb_info=info) for info in infos]
|
return [MediaInfo(tmdb_info=info) for info in infos]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_trending(self, page: Optional[int] = 1) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
TMDB流行趋势(异步版本)
|
||||||
|
:param page: 第几页
|
||||||
|
:return: TMDB信息列表
|
||||||
|
"""
|
||||||
|
trending = await self.tmdb.async_discover_trending(page=page)
|
||||||
|
if trending:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in trending]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_collection(self, collection_id: int) -> Optional[List[MediaInfo]]:
|
||||||
|
"""
|
||||||
|
根据合集ID查询集合(异步版本)
|
||||||
|
:param collection_id: 合集ID
|
||||||
|
"""
|
||||||
|
results = await self.tmdb.async_get_collection(collection_id)
|
||||||
|
if results:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in results]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_seasons(self, tmdbid: int) -> List[schemas.TmdbSeason]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询themoviedb所有季信息(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
"""
|
||||||
|
tmdb_info = await self.tmdb.async_get_info(tmdbid=tmdbid, mtype=MediaType.TV)
|
||||||
|
if not tmdb_info:
|
||||||
|
return []
|
||||||
|
return [schemas.TmdbSeason(**sea)
|
||||||
|
for sea in tmdb_info.get("seasons", []) if sea.get("season_number")]
|
||||||
|
|
||||||
|
async def async_tmdb_group_seasons(self, group_id: str) -> List[schemas.TmdbSeason]:
|
||||||
|
"""
|
||||||
|
根据剧集组ID查询themoviedb所有季集信息(异步版本)
|
||||||
|
:param group_id: 剧集组ID
|
||||||
|
"""
|
||||||
|
group_seasons = await self.tmdb.async_get_tv_group_seasons(group_id)
|
||||||
|
if not group_seasons:
|
||||||
|
return []
|
||||||
|
return [schemas.TmdbSeason(
|
||||||
|
season_number=sea.get("order"),
|
||||||
|
name=sea.get("name"),
|
||||||
|
episode_count=len(sea.get("episodes") or []),
|
||||||
|
air_date=sea.get("episodes")[0].get("air_date") if sea.get("episodes") else None,
|
||||||
|
) for sea in group_seasons]
|
||||||
|
|
||||||
|
async def async_tmdb_episodes(self, tmdbid: int, season: int,
|
||||||
|
episode_group: Optional[str] = None) -> List[schemas.TmdbEpisode]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询某季的所有集信息(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
:param season: 季
|
||||||
|
:param episode_group: 剧集组
|
||||||
|
"""
|
||||||
|
if episode_group:
|
||||||
|
season_info = await self.tmdb.async_get_tv_group_detail(episode_group, season=season)
|
||||||
|
else:
|
||||||
|
season_info = await self.tmdb.async_get_tv_season_detail(tmdbid=tmdbid, season=season)
|
||||||
|
if not season_info or not season_info.get("episodes"):
|
||||||
|
return []
|
||||||
|
return [schemas.TmdbEpisode(**episode) for episode in season_info.get("episodes")]
|
||||||
|
|
||||||
|
async def async_tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询类似电影(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
"""
|
||||||
|
similar = await self.tmdb.async_get_movie_similar(tmdbid=tmdbid)
|
||||||
|
if similar:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in similar]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_tv_similar(self, tmdbid: int) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询类似电视剧(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
"""
|
||||||
|
similar = await self.tmdb.async_get_tv_similar(tmdbid=tmdbid)
|
||||||
|
if similar:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in similar]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_movie_recommend(self, tmdbid: int) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询推荐电影(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
"""
|
||||||
|
recommend = await self.tmdb.async_get_movie_recommend(tmdbid=tmdbid)
|
||||||
|
if recommend:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in recommend]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_tv_recommend(self, tmdbid: int) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询推荐电视剧(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
"""
|
||||||
|
recommend = await self.tmdb.async_get_tv_recommend(tmdbid=tmdbid)
|
||||||
|
if recommend:
|
||||||
|
return [MediaInfo(tmdb_info=info) for info in recommend]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_movie_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[schemas.MediaPerson]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询电影演职员表(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
:param page: 页码
|
||||||
|
"""
|
||||||
|
credit_infos = await self.tmdb.async_get_movie_credits(tmdbid=tmdbid, page=page)
|
||||||
|
if credit_infos:
|
||||||
|
return [schemas.MediaPerson(source="themoviedb", **info) for info in credit_infos]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_tv_credits(self, tmdbid: int, page: Optional[int] = 1) -> List[schemas.MediaPerson]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询电视剧演职员表(异步版本)
|
||||||
|
:param tmdbid: TMDBID
|
||||||
|
:param page: 页码
|
||||||
|
"""
|
||||||
|
credit_infos = await self.tmdb.async_get_tv_credits(tmdbid=tmdbid, page=page)
|
||||||
|
if credit_infos:
|
||||||
|
return [schemas.MediaPerson(source="themoviedb", **info) for info in credit_infos]
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def async_tmdb_person_detail(self, person_id: int) -> schemas.MediaPerson:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询人物详情(异步版本)
|
||||||
|
:param person_id: 人物ID
|
||||||
|
"""
|
||||||
|
detail = await self.tmdb.async_get_person_detail(person_id=person_id)
|
||||||
|
if detail:
|
||||||
|
return schemas.MediaPerson(source="themoviedb", **detail)
|
||||||
|
return schemas.MediaPerson()
|
||||||
|
|
||||||
|
async def async_tmdb_person_credits(self, person_id: int, page: Optional[int] = 1) -> List[MediaInfo]:
|
||||||
|
"""
|
||||||
|
根据TMDBID查询人物参演作品(异步版本)
|
||||||
|
:param person_id: 人物ID
|
||||||
|
:param page: 页码
|
||||||
|
"""
|
||||||
|
infos = await self.tmdb.async_get_person_credits(person_id=person_id, page=page)
|
||||||
|
if infos:
|
||||||
|
return [MediaInfo(tmdb_info=tmdbinfo) for tmdbinfo in infos]
|
||||||
|
return []
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
"""
|
"""
|
||||||
清除缓存
|
清除缓存
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ fastapi~=0.115.14
|
|||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
PyJWT~=2.10.1
|
PyJWT~=2.10.1
|
||||||
python-multipart~=0.0.9
|
python-multipart~=0.0.9
|
||||||
|
aiofiles~=24.1.0
|
||||||
alembic~=1.16.2
|
alembic~=1.16.2
|
||||||
bcrypt~=4.0.1
|
bcrypt~=4.0.1
|
||||||
regex~=2024.11.6
|
regex~=2024.11.6
|
||||||
|
|||||||
Reference in New Issue
Block a user