mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
refactor(recommend): unify paired decision plans
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from typing import Callable, List, Optional
|
||||
from typing import Any, Callable, List, Optional, Sequence
|
||||
|
||||
import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用
|
||||
|
||||
@@ -35,6 +35,111 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
# 推荐缓存区域
|
||||
recommend_cache_region = "recommend"
|
||||
|
||||
@staticmethod
|
||||
def _music_chart_request(
|
||||
range_name: str,
|
||||
page: int,
|
||||
count: int,
|
||||
entity: str,
|
||||
) -> dict[str, Any]:
|
||||
"""构造同步和异步榜单入口共用的 ListenBrainz 请求。"""
|
||||
return {
|
||||
"range_name": range_name,
|
||||
"page": page,
|
||||
"count": count,
|
||||
"entity": entity,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _music_discover_request(
|
||||
page: int,
|
||||
count: int,
|
||||
entity: str,
|
||||
mode: str,
|
||||
tags: str,
|
||||
sort: str,
|
||||
) -> dict[str, Any]:
|
||||
"""构造同步和异步豆瓣音乐发现入口共用的请求。"""
|
||||
return {
|
||||
"page": page,
|
||||
"count": count,
|
||||
"entity": entity,
|
||||
"mode": mode,
|
||||
"tags": tags,
|
||||
"sort": sort,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _tmdb_discover_request(
|
||||
mtype: MediaType,
|
||||
sort_by: Optional[str],
|
||||
with_genres: Optional[str],
|
||||
with_original_language: Optional[str],
|
||||
with_keywords: Optional[str],
|
||||
with_watch_providers: Optional[str],
|
||||
vote_average: Optional[float],
|
||||
vote_count: Optional[int],
|
||||
release_date: Optional[str],
|
||||
page: Optional[int],
|
||||
) -> dict[str, Any]:
|
||||
"""构造同步和异步 TMDB 发现入口共用的业务请求。"""
|
||||
return {
|
||||
"mtype": mtype,
|
||||
"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,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _douban_discover_request(
|
||||
mtype: MediaType,
|
||||
sort: Optional[str],
|
||||
tags: Optional[str],
|
||||
page: Optional[int],
|
||||
count: Optional[int],
|
||||
) -> dict[str, Any]:
|
||||
"""构造同步和异步豆瓣影视发现入口共用的业务请求。"""
|
||||
return {
|
||||
"mtype": mtype,
|
||||
"sort": sort,
|
||||
"tags": tags,
|
||||
"page": page,
|
||||
"count": count,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _supports_music_source(media_source: MediaSource) -> bool:
|
||||
"""判断推荐发现是否支持请求中的音乐数据源。"""
|
||||
return normalize_media_source(media_source) == MediaSource.DoubanMusic
|
||||
|
||||
@staticmethod
|
||||
def _serialize_medias(
|
||||
medias: Optional[Sequence[Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""把来源媒体对象统一投影为推荐缓存使用的字典。"""
|
||||
return [media.to_dict() for media in medias] if medias else []
|
||||
|
||||
@classmethod
|
||||
def _serialize_media_page(
|
||||
cls,
|
||||
medias: Optional[Sequence[Any]],
|
||||
page: Optional[int],
|
||||
count: Optional[int],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""按规范化页码截取来源媒体,并统一投影为推荐字典。"""
|
||||
if not medias:
|
||||
return []
|
||||
normalized_page = page or 1
|
||||
normalized_count = count or 30
|
||||
start = (normalized_page - 1) * normalized_count
|
||||
return cls._serialize_medias(medias[start:start + normalized_count])
|
||||
|
||||
def music_chart(
|
||||
self,
|
||||
range_name: str,
|
||||
@@ -46,12 +151,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
entity: str = MUSIC_ENTITY_RECORDING,
|
||||
) -> list[MusicInfo]:
|
||||
"""读取 ListenBrainz 音乐榜单并应用推荐筛选与排序。"""
|
||||
results = ListenBrainzChain().music_chart(
|
||||
range_name=range_name,
|
||||
page=page,
|
||||
count=count,
|
||||
entity=entity,
|
||||
)
|
||||
request = self._music_chart_request(range_name, page, count, entity)
|
||||
results = ListenBrainzChain().music_chart(**request)
|
||||
return self._filter_music_candidates(
|
||||
results,
|
||||
count=count,
|
||||
@@ -71,12 +172,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
entity: str = MUSIC_ENTITY_RECORDING,
|
||||
) -> list[MusicInfo]:
|
||||
"""异步读取 ListenBrainz 音乐榜单并应用推荐筛选与排序。"""
|
||||
results = await ListenBrainzChain().async_music_chart(
|
||||
range_name=range_name,
|
||||
page=page,
|
||||
count=count,
|
||||
entity=entity,
|
||||
)
|
||||
request = self._music_chart_request(range_name, page, count, entity)
|
||||
results = await ListenBrainzChain().async_music_chart(**request)
|
||||
return self._filter_music_candidates(
|
||||
results,
|
||||
count=count,
|
||||
@@ -121,16 +218,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
sort: str = "U",
|
||||
) -> list[MusicInfo]:
|
||||
"""按固定音乐来源读取发现内容,当前支持豆瓣音乐。"""
|
||||
if normalize_media_source(media_source) != MediaSource.DoubanMusic:
|
||||
if not self._supports_music_source(media_source):
|
||||
return []
|
||||
return DoubanChain().music_discover(
|
||||
page=page,
|
||||
count=count,
|
||||
entity=entity,
|
||||
mode=mode,
|
||||
tags=tags,
|
||||
sort=sort,
|
||||
)
|
||||
request = self._music_discover_request(page, count, entity, mode, tags, sort)
|
||||
return DoubanChain().music_discover(**request)
|
||||
|
||||
async def async_music_discover(
|
||||
self,
|
||||
@@ -143,16 +234,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
sort: str = "U",
|
||||
) -> list[MusicInfo]:
|
||||
"""异步按固定音乐来源读取发现内容,当前支持豆瓣音乐。"""
|
||||
if normalize_media_source(media_source) != MediaSource.DoubanMusic:
|
||||
if not self._supports_music_source(media_source):
|
||||
return []
|
||||
return await DoubanChain().async_music_discover(
|
||||
page=page,
|
||||
count=count,
|
||||
entity=entity,
|
||||
mode=mode,
|
||||
tags=tags,
|
||||
sort=sort,
|
||||
)
|
||||
request = self._music_discover_request(page, count, entity, mode, tags, sort)
|
||||
return await DoubanChain().async_music_discover(**request)
|
||||
|
||||
@staticmethod
|
||||
def _filter_music_candidates(
|
||||
@@ -312,17 +397,12 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
TMDB热门电影
|
||||
"""
|
||||
movies = TmdbChain().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 []
|
||||
request = self._tmdb_discover_request(
|
||||
MediaType.MOVIE, sort_by, with_genres, with_original_language,
|
||||
with_keywords, with_watch_providers, vote_average, vote_count,
|
||||
release_date, page,
|
||||
)
|
||||
return self._serialize_medias(TmdbChain().tmdb_discover(**request))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -334,7 +414,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
count=count or 30,
|
||||
entity=MUSIC_ENTITY_ALBUM,
|
||||
)
|
||||
return [media.to_dict() for media in medias]
|
||||
return self._serialize_medias(medias)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -350,7 +430,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
count=count or 30,
|
||||
entity=MUSIC_ENTITY_ALBUM,
|
||||
)
|
||||
return [media.to_dict() for media in medias]
|
||||
return self._serialize_medias(medias)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -366,17 +446,12 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
TMDB热门电视剧
|
||||
"""
|
||||
tvs = TmdbChain().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 []
|
||||
request = self._tmdb_discover_request(
|
||||
MediaType.TV, sort_by, with_genres, with_original_language,
|
||||
with_keywords, with_watch_providers, vote_average, vote_count,
|
||||
release_date, page,
|
||||
)
|
||||
return self._serialize_medias(TmdbChain().tmdb_discover(**request))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -384,8 +459,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
TMDB流行趋势
|
||||
"""
|
||||
infos = TmdbChain().tmdb_trending(page=page)
|
||||
return [info.to_dict() for info in infos] if infos else []
|
||||
return self._serialize_medias(TmdbChain().tmdb_trending(page=page))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -393,8 +467,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
Bangumi每日放送
|
||||
"""
|
||||
medias = BangumiChain().calendar()
|
||||
return [media.to_dict() for media in medias[(page - 1) * count: page * count]] if medias else []
|
||||
return self._serialize_media_page(BangumiChain().calendar(), page, count)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -402,8 +475,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣正在热映
|
||||
"""
|
||||
movies = DoubanChain().movie_showing(page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().movie_showing(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -412,9 +486,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣最新电影
|
||||
"""
|
||||
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
|
||||
sort=sort, tags=tags, page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
request = self._douban_discover_request(
|
||||
MediaType.MOVIE, sort, tags, page, count
|
||||
)
|
||||
return self._serialize_medias(DoubanChain().douban_discover(**request))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -423,9 +498,8 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣最新电视剧
|
||||
"""
|
||||
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
|
||||
sort=sort, tags=tags, page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
request = self._douban_discover_request(MediaType.TV, sort, tags, page, count)
|
||||
return self._serialize_medias(DoubanChain().douban_discover(**request))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -433,8 +507,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣电影TOP250
|
||||
"""
|
||||
movies = DoubanChain().movie_top250(page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().movie_top250(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -442,8 +517,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣国产剧集榜
|
||||
"""
|
||||
tvs = DoubanChain().tv_weekly_chinese(page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().tv_weekly_chinese(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -451,8 +527,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣全球剧集榜
|
||||
"""
|
||||
tvs = DoubanChain().tv_weekly_global(page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().tv_weekly_global(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -460,8 +537,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣热门动漫
|
||||
"""
|
||||
tvs = DoubanChain().tv_animation(page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().tv_animation(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -469,8 +547,9 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣热门电影
|
||||
"""
|
||||
movies = DoubanChain().movie_hot(page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
return self._serialize_medias(
|
||||
DoubanChain().movie_hot(page=page, count=count)
|
||||
)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -478,8 +557,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
豆瓣热门电视剧
|
||||
"""
|
||||
tvs = DoubanChain().tv_hot(page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
return self._serialize_medias(DoubanChain().tv_hot(page=page, count=count))
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -496,18 +574,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步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,
|
||||
raise_exception=raise_exception)
|
||||
return [movie.to_dict() for movie in movies] if movies else []
|
||||
request = self._tmdb_discover_request(
|
||||
MediaType.MOVIE, sort_by, with_genres, with_original_language,
|
||||
with_keywords, with_watch_providers, vote_average, vote_count,
|
||||
release_date, page,
|
||||
)
|
||||
movies = await TmdbChain().async_run_module(
|
||||
"async_tmdb_discover", **request, raise_exception=raise_exception
|
||||
)
|
||||
return self._serialize_medias(movies)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -524,18 +599,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步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,
|
||||
raise_exception=raise_exception)
|
||||
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||
request = self._tmdb_discover_request(
|
||||
MediaType.TV, sort_by, with_genres, with_original_language,
|
||||
with_keywords, with_watch_providers, vote_average, vote_count,
|
||||
release_date, page,
|
||||
)
|
||||
tvs = await TmdbChain().async_run_module(
|
||||
"async_tmdb_discover", **request, raise_exception=raise_exception
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -550,7 +622,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
page=page,
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
return [info.to_dict() for info in infos] if infos else []
|
||||
return self._serialize_medias(infos)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -559,7 +631,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
异步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 []
|
||||
return self._serialize_media_page(medias, page, count)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -567,8 +639,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣正在热映
|
||||
"""
|
||||
movies = await DoubanChain().async_run_module("async_movie_showing", page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
movies = await DoubanChain().async_run_module(
|
||||
"async_movie_showing", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(movies)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -580,7 +654,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
count=count or 30,
|
||||
entity=MUSIC_ENTITY_ALBUM,
|
||||
)
|
||||
return [media.to_dict() for media in medias]
|
||||
return self._serialize_medias(medias)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -596,7 +670,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
count=count or 30,
|
||||
entity=MUSIC_ENTITY_ALBUM,
|
||||
)
|
||||
return [media.to_dict() for media in medias]
|
||||
return self._serialize_medias(medias)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -605,9 +679,13 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣最新电影
|
||||
"""
|
||||
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 []
|
||||
request = self._douban_discover_request(
|
||||
MediaType.MOVIE, sort, tags, page, count
|
||||
)
|
||||
movies = await DoubanChain().async_run_module(
|
||||
"async_douban_discover", **request
|
||||
)
|
||||
return self._serialize_medias(movies)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -616,9 +694,11 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣最新电视剧
|
||||
"""
|
||||
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 []
|
||||
request = self._douban_discover_request(MediaType.TV, sort, tags, page, count)
|
||||
tvs = await DoubanChain().async_run_module(
|
||||
"async_douban_discover", **request
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -626,8 +706,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣电影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 []
|
||||
movies = await DoubanChain().async_run_module(
|
||||
"async_movie_top250", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(movies)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -635,8 +717,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣国产剧集榜
|
||||
"""
|
||||
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 []
|
||||
tvs = await DoubanChain().async_run_module(
|
||||
"async_tv_weekly_chinese", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -644,8 +728,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣全球剧集榜
|
||||
"""
|
||||
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 []
|
||||
tvs = await DoubanChain().async_run_module(
|
||||
"async_tv_weekly_global", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -653,8 +739,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣热门动漫
|
||||
"""
|
||||
tvs = await DoubanChain().async_run_module("async_tv_animation", page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
tvs = await DoubanChain().async_run_module(
|
||||
"async_tv_animation", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -662,8 +750,10 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣热门电影
|
||||
"""
|
||||
movies = await DoubanChain().async_run_module("async_movie_hot", page=page, count=count)
|
||||
return [media.to_dict() for media in movies] if movies else []
|
||||
movies = await DoubanChain().async_run_module(
|
||||
"async_movie_hot", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(movies)
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region, skip_empty=True)
|
||||
@@ -671,5 +761,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
异步豆瓣热门电视剧
|
||||
"""
|
||||
tvs = await DoubanChain().async_run_module("async_tv_hot", page=page, count=count)
|
||||
return [media.to_dict() for media in tvs] if tvs else []
|
||||
tvs = await DoubanChain().async_run_module(
|
||||
"async_tv_hot", page=page, count=count
|
||||
)
|
||||
return self._serialize_medias(tvs)
|
||||
|
||||
@@ -78,7 +78,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 约 298,700 行 | 60 个文件超过 1,000 行,10 个超过 2,000 行 |
|
||||
| 长方法 | 299 个超过 80 行 | 70 个超过 150 行,23 个超过 250 行;大量是私有方法 |
|
||||
| 全量 mypy 历史债务 | 10,231 / 592 文件 | 识别链 typed plan/result、TMDB、消息、Indexer 与 AniList/Bangumi/IMDb 双 ABI 共用计划继续清除参数、返回值和泛型注解债;strict frontier 当前覆盖 41 个文件 |
|
||||
| 全量 mypy 历史债务 | 10,190 / 592 文件 | 识别链 typed plan/result、TMDB、消息、Indexer、目录源与 Recommend 双 ABI 共用计划继续清除参数、返回值和泛型注解债;strict frontier 当前覆盖 41 个文件 |
|
||||
| Ruff 历史诊断 | 641 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率低水位 | Application 81.00%,Domain 80.67% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
|
||||
@@ -1316,11 +1316,9 @@
|
||||
"type-arg": 2
|
||||
},
|
||||
"app/chain/recommend.py": {
|
||||
"arg-type": 20,
|
||||
"misc": 60,
|
||||
"no-untyped-call": 14,
|
||||
"no-untyped-call": 1,
|
||||
"no-untyped-def": 1,
|
||||
"operator": 8,
|
||||
"type-arg": 31
|
||||
},
|
||||
"app/chain/scraping.py": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from typing import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -96,6 +96,80 @@ def test_async_recommend_methods_do_not_cache_empty_result(
|
||||
assert backend_chain.return_value.async_run_module.call_count == 2
|
||||
|
||||
|
||||
def test_tmdb_discover_sync_async_share_request_and_projection() -> None:
|
||||
"""TMDB 推荐双入口应复用同一发现请求和结果投影。"""
|
||||
chain = RecommendChain()
|
||||
candidate = Mock()
|
||||
candidate.to_dict.return_value = {"media_id": "tmdb-1"}
|
||||
|
||||
with patch("app.chain.recommend.TmdbChain") as source_chain:
|
||||
source_chain.return_value.tmdb_discover.return_value = [candidate]
|
||||
source_chain.return_value.async_run_module = AsyncMock(return_value=[candidate])
|
||||
|
||||
sync_result = chain.tmdb_movies(
|
||||
with_genres="18", vote_average=7.5, vote_count=100, page=2
|
||||
)
|
||||
async_result = asyncio.run(chain.async_tmdb_movies(
|
||||
with_genres="18", vote_average=7.5, vote_count=100, page=2
|
||||
))
|
||||
|
||||
sync_request = source_chain.return_value.tmdb_discover.call_args.kwargs
|
||||
async_request = dict(source_chain.return_value.async_run_module.call_args.kwargs)
|
||||
assert async_request.pop("raise_exception") is False
|
||||
assert source_chain.return_value.async_run_module.call_args.args == (
|
||||
"async_tmdb_discover",
|
||||
)
|
||||
assert sync_request == async_request
|
||||
assert sync_result == async_result == [{"media_id": "tmdb-1"}]
|
||||
|
||||
|
||||
def test_bangumi_calendar_sync_async_share_page_projection() -> None:
|
||||
"""Bangumi 日历双入口应复用同一分页与字典投影规则。"""
|
||||
chain = RecommendChain()
|
||||
candidates = []
|
||||
for index in range(5):
|
||||
candidate = Mock()
|
||||
candidate.to_dict.return_value = {"media_id": str(index)}
|
||||
candidates.append(candidate)
|
||||
|
||||
with patch("app.chain.recommend.BangumiChain") as source_chain:
|
||||
source_chain.return_value.calendar.return_value = candidates
|
||||
source_chain.return_value.async_run_module = AsyncMock(return_value=candidates)
|
||||
|
||||
sync_result = chain.bangumi_calendar(page=2, count=2)
|
||||
async_result = asyncio.run(chain.async_bangumi_calendar(page=2, count=2))
|
||||
|
||||
assert sync_result == async_result == [
|
||||
{"media_id": "2"},
|
||||
{"media_id": "3"},
|
||||
]
|
||||
|
||||
|
||||
def test_douban_discover_sync_async_share_request_and_projection() -> None:
|
||||
"""豆瓣推荐双入口应复用媒体类型、分页和结果投影。"""
|
||||
chain = RecommendChain()
|
||||
candidate = Mock()
|
||||
candidate.to_dict.return_value = {"media_id": "douban-1"}
|
||||
|
||||
with patch("app.chain.recommend.DoubanChain") as source_chain:
|
||||
source_chain.return_value.douban_discover.return_value = [candidate]
|
||||
source_chain.return_value.async_run_module = AsyncMock(return_value=[candidate])
|
||||
|
||||
sync_result = chain.douban_movies(sort="U", tags="喜剧", page=3, count=12)
|
||||
clear_recommend_cache()
|
||||
async_result = asyncio.run(chain.async_douban_movies(
|
||||
sort="U", tags="喜剧", page=3, count=12
|
||||
))
|
||||
|
||||
sync_request = source_chain.return_value.douban_discover.call_args.kwargs
|
||||
async_request = source_chain.return_value.async_run_module.call_args.kwargs
|
||||
assert source_chain.return_value.async_run_module.call_args.args == (
|
||||
"async_douban_discover",
|
||||
)
|
||||
assert sync_request == async_request
|
||||
assert sync_result == async_result == [{"media_id": "douban-1"}]
|
||||
|
||||
|
||||
def test_music_weekly_uses_music_chart():
|
||||
"""同步推荐缓存应从本周音乐榜单生成通用媒体字典。"""
|
||||
chain = RecommendChain()
|
||||
|
||||
Reference in New Issue
Block a user