diff --git a/app/api/apiv1.py b/app/api/apiv1.py index 6182ac71..1810b8a4 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from app.api.endpoints import login, user, site, message, webhook, subscribe, \ media, douban, search, plugin, tmdb, history, system, download, dashboard, \ - transfer, mediaserver, bangumi, storage, discover + transfer, mediaserver, bangumi, storage, discover, recommend api_router = APIRouter() api_router.include_router(login.router, prefix="/login", tags=["login"]) @@ -25,3 +25,4 @@ api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"] api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"]) api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"]) api_router.include_router(discover.router, prefix="/discover", tags=["discover"]) +api_router.include_router(recommend.router, prefix="/recommend", tags=["recommend"]) diff --git a/app/api/endpoints/bangumi.py b/app/api/endpoints/bangumi.py index ae081b3b..05cb9709 100644 --- a/app/api/endpoints/bangumi.py +++ b/app/api/endpoints/bangumi.py @@ -4,38 +4,12 @@ from fastapi import APIRouter, Depends from app import schemas from app.chain.bangumi import BangumiChain -from app.chain.recommend import RecommendChain from app.core.context import MediaInfo from app.core.security import verify_token router = APIRouter() -@router.get("/calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo]) -def calendar(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览Bangumi每日放送 - """ - return RecommendChain().bangumi_calendar(page=page, count=count) - - -@router.get("/subjects", summary="搜索Bangumi", response_model=List[schemas.MediaInfo]) -def bangumi_subjects(type: int = 2, - cat: int = None, - sort: str = 'rank', - year: int = None, - page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 搜索Bangumi - """ - return RecommendChain().bangumi_discover(type=type, cat=cat, sort=sort, year=year, - page=page, count=count) - - @router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson]) def bangumi_credits(bangumiid: int, page: int = 1, diff --git a/app/api/endpoints/discover.py b/app/api/endpoints/discover.py index a115f861..21ad2e98 100644 --- a/app/api/endpoints/discover.py +++ b/app/api/endpoints/discover.py @@ -7,6 +7,8 @@ from app.core.event import eventmanager from app.core.security import verify_token from app.schemas import DiscoverSourceEventData from app.schemas.types import ChainEventType +from chain.bangumi import BangumiChain +from chain.recommend import RecommendChain router = APIRouter() @@ -16,7 +18,7 @@ def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any: """ 获取探索数据源 """ - # 广播事件,请示额外的发现数据源支持 + # 广播事件,请示额外的探索数据源支持 event_data = DiscoverSourceEventData() event = eventmanager.send_event(ChainEventType.DiscoverSource, event_data) # 使用事件返回的上下文数据 @@ -25,3 +27,95 @@ def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any: if event_data.extra_sources: return event_data.extra_sources return [] + + +@router.get("/bangumi", summary="探索Bangumi", response_model=List[schemas.MediaInfo]) +def bangumi(type: int = 2, + cat: int = None, + sort: str = 'rank', + year: int = None, + page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 探索Bangumi + """ + medias = BangumiChain().discover(type=type, cat=cat, sort=sort, year=year, + limit=count, offset=(page - 1) * count) + if medias: + return [media.to_dict() for media in medias] + return [] + + +@router.get("/douban_movies", summary="探索豆瓣电影", response_model=List[schemas.MediaInfo]) +def douban_movies(sort: str = "R", + tags: str = "", + page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣电影信息 + """ + return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count) + + +@router.get("/douban_tvs", summary="探索豆瓣剧集", response_model=List[schemas.MediaInfo]) +def douban_tvs(sort: str = "R", + tags: str = "", + page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣剧集信息 + """ + return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count) + + +@router.get("/tmdb_movies", summary="探索TMDB电影", response_model=List[schemas.MediaInfo]) +def tmdb_movies(sort_by: str = "popularity.desc", + with_genres: str = "", + with_original_language: str = "", + with_keywords: str = "", + with_watch_providers: str = "", + vote_average: float = 0, + vote_count: int = 0, + release_date: str = "", + page: int = 1, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览TMDB电影信息 + """ + return RecommendChain().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) + + +@router.get("/tmdb_tvs", summary="探索TMDB剧集", response_model=List[schemas.MediaInfo]) +def tmdb_tvs(sort_by: str = "popularity.desc", + with_genres: str = "", + with_original_language: str = "", + with_keywords: str = "", + with_watch_providers: str = "", + vote_average: float = 0, + vote_count: int = 0, + release_date: str = "", + page: int = 1, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览TMDB剧集信息 + """ + return RecommendChain().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) diff --git a/app/api/endpoints/douban.py b/app/api/endpoints/douban.py index 6be0e8e7..5717ab3b 100644 --- a/app/api/endpoints/douban.py +++ b/app/api/endpoints/douban.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from app import schemas from app.chain.douban import DoubanChain -from app.chain.recommend import RecommendChain from app.core.context import MediaInfo from app.core.security import verify_token from app.schemas import MediaType @@ -34,100 +33,6 @@ def douban_person_credits(person_id: int, return [] -@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo]) -def movie_showing(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览豆瓣正在热映 - """ - return RecommendChain().douban_movie_showing(page=page, count=count) - - -@router.get("/movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo]) -def douban_movies(sort: str = "R", - tags: str = "", - page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览豆瓣电影信息 - """ - return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count) - - -@router.get("/tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo]) -def douban_tvs(sort: str = "R", - tags: str = "", - page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览豆瓣剧集信息 - """ - return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count) - - -@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo]) -def movie_top250(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览豆瓣剧集信息 - """ - return RecommendChain().douban_movie_top250(page=page, count=count) - - -@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo]) -def tv_weekly_chinese(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 中国每周剧集口碑榜 - """ - return RecommendChain().douban_tv_weekly_chinese(page=page, count=count) - - -@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo]) -def tv_weekly_global(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 全球每周剧集口碑榜 - """ - return RecommendChain().douban_tv_weekly_global(page=page, count=count) - - -@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: - """ - 热门动画剧集 - """ - return RecommendChain().douban_tv_animation(page=page, count=count) - - -@router.get("/movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo]) -def movie_hot(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 热门电影 - """ - return RecommendChain().douban_movie_hot(page=page, count=count) - - -@router.get("/tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo]) -def tv_hot(page: int = 1, - count: int = 30, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 热门电视剧 - """ - return RecommendChain().douban_tv_hot(page=page, count=count) - - @router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.MediaPerson]) def douban_credits(doubanid: str, type_name: str, diff --git a/app/api/endpoints/recommend.py b/app/api/endpoints/recommend.py new file mode 100644 index 00000000..86b1d2ac --- /dev/null +++ b/app/api/endpoints/recommend.py @@ -0,0 +1,191 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends + +from app import schemas +from app.core.event import eventmanager +from app.core.security import verify_token +from app.schemas.types import ChainEventType +from chain.recommend import RecommendChain +from schemas import RecommendSourceEventData + +router = APIRouter() + + +@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]) +def bangumi_calendar(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览Bangumi每日放送 + """ + return RecommendChain().bangumi_calendar(page=page, count=count) + + +@router.get("/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo]) +def douban_showing(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣正在热映 + """ + return RecommendChain().douban_movie_showing(page=page, count=count) + + +@router.get("/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo]) +def douban_movies(sort: str = "R", + tags: str = "", + page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣电影信息 + """ + return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count) + + +@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo]) +def douban_tvs(sort: str = "R", + tags: str = "", + page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣剧集信息 + """ + return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count) + + +@router.get("/douban_movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo]) +def douban_movie_top250(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览豆瓣剧集信息 + """ + return RecommendChain().douban_movie_top250(page=page, count=count) + + +@router.get("/douban_tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo]) +def douban_tv_weekly_chinese(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 中国每周剧集口碑榜 + """ + return RecommendChain().douban_tv_weekly_chinese(page=page, count=count) + + +@router.get("/douban_tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo]) +def douban_tv_weekly_global(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 全球每周剧集口碑榜 + """ + return RecommendChain().douban_tv_weekly_global(page=page, count=count) + + +@router.get("/douban_tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo]) +def douban_tv_animation(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 热门动画剧集 + """ + return RecommendChain().douban_tv_animation(page=page, count=count) + + +@router.get("/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo]) +def douban_movie_hot(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 热门电影 + """ + return RecommendChain().douban_movie_hot(page=page, count=count) + + +@router.get("/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo]) +def douban_tv_hot(page: int = 1, + count: int = 30, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 热门电视剧 + """ + return RecommendChain().douban_tv_hot(page=page, count=count) + + +@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo]) +def tmdb_movies(sort_by: str = "popularity.desc", + with_genres: str = "", + with_original_language: str = "", + with_keywords: str = "", + with_watch_providers: str = "", + vote_average: float = 0, + vote_count: int = 0, + release_date: str = "", + page: int = 1, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览TMDB电影信息 + """ + return RecommendChain().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) + + +@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo]) +def tmdb_tvs(sort_by: str = "popularity.desc", + with_genres: str = "", + with_original_language: str = "", + with_keywords: str = "", + with_watch_providers: str = "", + vote_average: float = 0, + vote_count: int = 0, + release_date: str = "", + page: int = 1, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 浏览TMDB剧集信息 + """ + return RecommendChain().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) + + +@router.get("/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo]) +def tmdb_trending(page: int = 1, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + TMDB流行趋势 + """ + return RecommendChain().tmdb_trending(page=page) diff --git a/app/api/endpoints/tmdb.py b/app/api/endpoints/tmdb.py index 238036fd..e75efcf2 100644 --- a/app/api/endpoints/tmdb.py +++ b/app/api/endpoints/tmdb.py @@ -3,7 +3,6 @@ from typing import List, Any from fastapi import APIRouter, Depends from app import schemas -from app.chain.recommend import RecommendChain from app.chain.tmdb import TmdbChain from app.core.security import verify_token from app.schemas.types import MediaType @@ -114,65 +113,6 @@ def tmdb_person_credits(person_id: int, return [] -@router.get("/movies", summary="TMDB电影", response_model=List[schemas.MediaInfo]) -def tmdb_movies(sort_by: str = "popularity.desc", - with_genres: str = "", - with_original_language: str = "", - with_keywords: str = "", - with_watch_providers: str = "", - vote_average: float = 0, - vote_count: int = 0, - release_date: str = "", - page: int = 1, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览TMDB电影信息 - """ - return RecommendChain().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) - - -@router.get("/tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo]) -def tmdb_tvs(sort_by: str = "popularity.desc", - with_genres: str = "", - with_original_language: str = "", - with_keywords: str = "", - with_watch_providers: str = "", - vote_average: float = 0, - vote_count: int = 0, - release_date: str = "", - page: int = 1, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 浏览TMDB剧集信息 - """ - return RecommendChain().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) - - -@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo]) -def tmdb_trending(page: int = 1, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - TMDB流行趋势 - """ - return RecommendChain().tmdb_trending(page=page) - - @router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode]) def tmdb_season_episodes(tmdbid: int, season: int, _: schemas.TokenPayload = Depends(verify_token)) -> Any: diff --git a/app/chain/recommend.py b/app/chain/recommend.py index 369ffa4e..321d77a6 100644 --- a/app/chain/recommend.py +++ b/app/chain/recommend.py @@ -1,7 +1,7 @@ import io import tempfile from pathlib import Path -from typing import Any, List +from typing import List from PIL import Image @@ -225,23 +225,6 @@ class RecommendChain(ChainBase, metaclass=Singleton): medias = self.bangumichain.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) - def bangumi_discover(self, type: int = 2, - cat: int = None, - sort: str = 'rank', - year: int = None, - count: int = 30, - page: int = 1) -> List[dict]: - """ - 搜索Bangumi - """ - medias = self.bangumichain.discover(type=type, cat=cat, sort=sort, year=year, - limit=count, offset=(page - 1) * count) - if medias: - return [media.to_dict() for media in medias] - return [] - @log_execution_time(logger=logger) @cached(ttl=recommend_ttl, region=recommend_cache_region) def douban_movie_showing(self, page: int = 1, count: int = 30) -> List[dict]: diff --git a/app/schemas/event.py b/app/schemas/event.py index 1eabe712..5dab160d 100644 --- a/app/schemas/event.py +++ b/app/schemas/event.py @@ -258,6 +258,26 @@ class DiscoverSourceEventData(ChainEventData): extra_sources: List[DiscoverMediaSource] = Field(default_factory=list, description="额外媒体数据源") +class RecommendMediaSource(BaseModel): + """ + 推荐媒体数据源的基类 + """ + name: str = Field(..., description="数据源名称") + api_path: str = Field(..., description="媒体数据源API地址") + + +class RecommendSourceEventData(ChainEventData): + """ + RecommendSource 事件的数据模型 + + Attributes: + # 输出参数 + extra_sources (List[RecommendMediaSource]): 额外媒体数据源 + """ + # 输出参数 + extra_sources: List[RecommendMediaSource] = Field(default_factory=list, description="额外媒体数据源") + + class MediaRecognizeConvertEventData(ChainEventData): """ MediaRecognizeConvert 事件的数据模型 diff --git a/app/schemas/types.py b/app/schemas/types.py index d9d0186e..b24fd786 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -81,10 +81,12 @@ class ChainEventType(Enum): ResourceSelection = "resource.selection" # 资源下载 ResourceDownload = "resource.download" - # 发现数据源 + # 探索数据源 DiscoverSource = "discover.source" # 媒体识别转换 MediaRecognizeConvert = "media.recognize.convert" + # 推荐数据源 + RecommendSource = "recommend.source" # 系统配置Key字典