Files
MoviePilot/app/api/endpoints/recommend.py
T
jxxghp 4345fcfa22 refactor(chain): chain 与 module 仅经 run_module 契约互联,实现模块内容封闭
- mTorrent 字幕链接解析下沉为 IndexerModule.site_subtitle_links,subtitle 模块自行跳过 API 站点
- TMDB/MusicBrainz 识别缓存管理改为模块方法(tmdb_cache_*/music_cache_*)经 run_module 分发
- WechatClawBot 客户端查找/临时客户端/缓存迁移内聚为 wechatclawbot_* 模块方法,
  chain 移除 WechatClawBot 类与 ModuleManager 内省
- LISTENBRAINZ_* 常量迁至 schemas/types.py,模块与链层再导入保持兼容
- TMDbException 升为 schemas/exception.py 跨层契约,vendored 异常保持类身份一致
- 架构守护测试新增 test_chain_does_not_import_module_internals(共 18 项)
- 文档同步:chain->module 仅允许 run_module 分发,直接导入禁止
2026-08-16 05:25:28 +08:00

308 lines
8.8 KiB
Python

from typing import Any, Awaitable, List, Optional
from fastapi import Depends, HTTPException, status
from app import schemas
from app.api.response import ResponseAPIRouter
from app.chain.recommend import RecommendChain
from app.runtime.events import eventmanager
from app.application.security.access import verify_token
from app.schemas.exception import TMDbException
from app.schemas import RecommendSourceEventData
from app.schemas.types import ChainEventType
router = ResponseAPIRouter()
async def _require_tmdb_result(operation: Awaitable[List[Any]]) -> List[Any]:
"""保留 TMDB 成功空列表,并把上游请求异常转换为明确的网关错误。"""
try:
return await operation
except TMDbException as error:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="TMDB请求失败",
) from error
@router.get(
"/source",
summary="获取推荐数据源",
response_model=List[schemas.RecommendMediaSource],
)
def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
获取推荐数据源
"""
# 广播事件,请示额外的推荐数据源支持
event_data = RecommendSourceEventData()
event = eventmanager.send_event(ChainEventType.RecommendSource, event_data)
# 使用事件返回的上下文数据
if event and event.event_data:
event_data: RecommendSourceEventData = event.event_data
if event_data.extra_sources:
return event_data.extra_sources
return []
@router.get(
"/bangumi_calendar",
summary="Bangumi每日放送",
response_model=List[schemas.MediaInfo],
)
async def bangumi_calendar(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览Bangumi每日放送
"""
return await RecommendChain().async_bangumi_calendar(page=page, count=count)
@router.get(
"/music_weekly",
summary="ListenBrainz 本周热门音乐",
response_model=List[schemas.MusicInfo],
)
async def music_weekly(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""浏览本周全站热门音乐。"""
return await RecommendChain().async_music_weekly(page=page, count=count)
@router.get(
"/music_douban",
summary="豆瓣音乐推荐",
response_model=List[schemas.MusicInfo],
)
async def music_douban(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""浏览豆瓣音乐推荐合集。"""
return await RecommendChain().async_music_douban(page=page, count=count)
@router.get(
"/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo]
)
async def douban_showing(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览豆瓣正在热映
"""
return await RecommendChain().async_douban_movie_showing(page=page, count=count)
@router.get(
"/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo]
)
async def douban_movies(
sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览豆瓣电影信息
"""
return await RecommendChain().async_douban_movies(
sort=sort, tags=tags, page=page, count=count
)
@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
async def douban_tvs(
sort: Optional[str] = "R",
tags: Optional[str] = "",
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览豆瓣剧集信息
"""
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],
)
async def douban_movie_top250(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览豆瓣剧集信息
"""
return await RecommendChain().async_douban_movie_top250(page=page, count=count)
@router.get(
"/douban_tv_weekly_chinese",
summary="豆瓣国产剧集周榜",
response_model=List[schemas.MediaInfo],
)
async def douban_tv_weekly_chinese(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
中国每周剧集口碑榜
"""
return await RecommendChain().async_douban_tv_weekly_chinese(page=page, count=count)
@router.get(
"/douban_tv_weekly_global",
summary="豆瓣全球剧集周榜",
response_model=List[schemas.MediaInfo],
)
async def douban_tv_weekly_global(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
全球每周剧集口碑榜
"""
return await RecommendChain().async_douban_tv_weekly_global(page=page, count=count)
@router.get(
"/douban_tv_animation",
summary="豆瓣动画剧集",
response_model=List[schemas.MediaInfo],
)
async def douban_tv_animation(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
热门动画剧集
"""
return await RecommendChain().async_douban_tv_animation(page=page, count=count)
@router.get(
"/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo]
)
async def douban_movie_hot(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
热门电影
"""
return await RecommendChain().async_douban_movie_hot(page=page, count=count)
@router.get(
"/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo]
)
async def douban_tv_hot(
page: Optional[int] = 1,
count: Optional[int] = 30,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
热门电视剧
"""
return await RecommendChain().async_douban_tv_hot(page=page, count=count)
@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
async def tmdb_movies(
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,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览TMDB电影信息
"""
return await _require_tmdb_result(
RecommendChain().async_tmdb_movies(
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,
raise_exception=True,
)
)
@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
async def tmdb_tvs(
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,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
浏览TMDB剧集信息
"""
return await _require_tmdb_result(
RecommendChain().async_tmdb_tvs(
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,
raise_exception=True,
)
)
@router.get(
"/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo]
)
async def tmdb_trending(
page: Optional[int] = 1, _: schemas.TokenPayload = Depends(verify_token)
) -> Any:
"""
TMDB流行趋势
"""
return await _require_tmdb_result(
RecommendChain().async_tmdb_trending(page=page, raise_exception=True)
)