refactor(media): unify media identity and chain responsibilities

This commit is contained in:
jxxghp
2026-08-12 18:31:09 +08:00
parent cbb969e0ff
commit ca32922a7f
155 changed files with 8371 additions and 4526 deletions

View File

@@ -7,7 +7,6 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.music import MusicChain
from app.chain.recommend import RecommendChain
from app.log import logger
from app.modules.listenbrainz import (
@@ -168,13 +167,13 @@ class GetRecommendationsTool(MoviePilotTool):
f"错误:无效的音乐实体类型 '{music_type}'"
"支持的类型:'recording', 'album'"
)
music_chain = MusicChain()
recommend_chain = RecommendChain()
if source == "listenbrainz_chart":
if range_name not in LISTENBRAINZ_CHART_RANGES:
return f"错误:无效的榜单周期 '{range_name}'"
if sort_by not in {"listen_count.desc", "listen_count.asc"}:
return f"错误:无效的榜单排序 '{sort_by}'"
results = await music_chain.async_chart(
results = await recommend_chain.async_music_chart(
range_name=range_name,
page=page,
count=page_size,
@@ -191,7 +190,7 @@ class GetRecommendationsTool(MoviePilotTool):
if not past and not future:
return "错误past 和 future 不能同时为 false"
normalized_days = max(1, min(days or 14, LISTENBRAINZ_FRESH_MAX_DAYS))
results = await music_chain.async_fresh_releases(
results = await recommend_chain.async_music_fresh_releases(
days=normalized_days,
sort=fresh_sort,
past=bool(past),
@@ -324,7 +323,7 @@ class GetRecommendationsTool(MoviePilotTool):
"douban_id": r.get("douban_id"),
"bangumi_id": r.get("bangumi_id"),
"anilist_id": r.get("anilist_id"),
"media_source": r.get("source"),
"media_source": r.get("media_source"),
"media_id": r.get("media_id"),
"vote_average": r.get("vote_average"),
"poster_path": r.get("poster_path"),

View File

@@ -9,7 +9,6 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain
from app.chain.music import MusicChain
from app.log import logger
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -125,9 +124,11 @@ class QueryMediaDetailTool(MoviePilotTool):
"message": "查询音乐详情必须同时提供 media_source 和 media_id",
}, ensure_ascii=False)
music_chain = MusicChain()
media_chain = MediaChain()
if normalized_music_type == MUSIC_ENTITY_ALBUM:
album_info = await music_chain.async_album(media_source, media_id)
album_info = await media_chain.async_get_music_album(
media_source, media_id
)
if not album_info:
return json.dumps({
"success": False,
@@ -140,7 +141,9 @@ class QueryMediaDetailTool(MoviePilotTool):
)
if normalized_music_type == MUSIC_ENTITY_ARTIST:
artist_info = await music_chain.async_artist(media_source, media_id)
artist_info = await media_chain.async_get_music_artist(
media_source, media_id
)
if not artist_info:
return json.dumps({
"success": False,
@@ -153,7 +156,7 @@ class QueryMediaDetailTool(MoviePilotTool):
if include_artist_albums:
pending.append((
"albums",
music_chain.async_artist_albums(
media_chain.async_get_music_artist_albums(
media_source=media_source,
media_id=media_id,
page=normalized_page,
@@ -164,7 +167,7 @@ class QueryMediaDetailTool(MoviePilotTool):
if include_related_artists:
pending.append((
"related_artists",
music_chain.async_artist_related(
media_chain.async_get_music_artist_related(
media_source=media_source,
media_id=media_id,
count=normalized_count,
@@ -181,7 +184,6 @@ class QueryMediaDetailTool(MoviePilotTool):
]
return json.dumps(result, ensure_ascii=False, indent=2)
media_chain = MediaChain()
mediainfo = await media_chain.async_recognize_media(
media_source=media_source,
media_id=media_id,

View File

@@ -170,23 +170,17 @@ class QueryPopularSubscribesTool(MoviePilotTool):
# 跳过无法识别类型的数据,避免单条脏数据导致整批失败
logger.warning(f"跳过未知媒体类型: {sub.get('type')}")
continue
media.tmdb_id = sub.get("tmdbid")
# 处理标题
title = sub.get("name")
season = sub.get("season")
if season not in (None, "") and int(season) != 1 and media.tmdb_id:
if season not in (None, "") and int(season) != 1:
# 小写数据转大写
season_str = cn2an.an2cn(season, "low")
title = f"{title}{season_str}"
media.title = title
media.year = sub.get("year")
media.douban_id = sub.get("doubanid")
media.bangumi_id = sub.get("bangumiid")
media.anilist_id = sub.get("anilistid")
media.media_source = sub.get("media_source")
media.media_id = sub.get("media_id")
media.tvdb_id = sub.get("tvdbid")
media.imdb_id = sub.get("imdbid")
media.season = sub.get("season")
media.vote_average = sub.get("vote")
media.poster_path = sub.get("poster")
@@ -208,14 +202,8 @@ class QueryPopularSubscribesTool(MoviePilotTool):
"type": media_type_to_agent(media_dict.get("type")),
"title": media_dict.get("title"),
"year": media_dict.get("year"),
"tmdb_id": media_dict.get("tmdb_id"),
"douban_id": media_dict.get("douban_id"),
"bangumi_id": media_dict.get("bangumi_id"),
"anilist_id": media_dict.get("anilist_id"),
"media_source": media_dict.get("source"),
"media_source": media_dict.get("media_source"),
"media_id": media_dict.get("media_id"),
"tvdb_id": media_dict.get("tvdb_id"),
"imdb_id": media_dict.get("imdb_id"),
"season": media_dict.get("season"),
"vote_average": media_dict.get("vote_average"),
"poster_path": media_dict.get("poster_path"),

View File

@@ -111,10 +111,6 @@ class QuerySubscribeSharesTool(MoviePilotTool):
"year": share.get("year"),
"type": normalized_type,
"season": share.get("season"),
"tmdbid": share.get("tmdbid"),
"doubanid": share.get("doubanid"),
"bangumiid": share.get("bangumiid"),
"anilistid": share.get("anilistid"),
"media_source": share.get("media_source"),
"media_id": share.get("media_id"),
"music_type": normalized_music_type,

View File

@@ -8,10 +8,10 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.music import MusicChain
from app.chain.media import MediaChain
from app.core.config import settings
from app.core.context import Context
from app.core.meta import MetaMusic
from app.core.metainfo import MetaInfo
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
@@ -108,7 +108,6 @@ class RecognizeMediaTool(MoviePilotTool):
media_type_enum is None and is_audio_path
)
if recognize_music:
music_chain = MusicChain()
if path:
if not is_audio_path:
return json.dumps({
@@ -129,7 +128,7 @@ class RecognizeMediaTool(MoviePilotTool):
"path": path,
}, ensure_ascii=False)
if title:
metainfo = music_chain.parse_query(title)
metainfo = MetaMusic.parse_query(title)
if artist:
metainfo.artists = [artist]
if album:

View File

@@ -9,10 +9,17 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain
from app.chain.scraping import ScrapingChain
from app.core.config import settings
from app.log import logger
from app.schemas import FileItem
from app.schemas.types import MUSIC_ENTITY_ARTIST, MediaType, media_type_to_agent
from app.schemas.types import (
MUSIC_ENTITY_ARTIST,
MediaSource,
MediaType,
media_type_to_agent,
)
from app.utils.media import normalize_media_source
from ._music_utils import normalize_music_type, simplify_music_info
@@ -39,7 +46,7 @@ class ScrapeMetadataInput(BaseModel):
None,
description="For an explicit music ID: recording for one file or album for a complete album directory",
)
media_source: Optional[str] = Field(
media_source: Optional[MediaSource] = Field(
None,
description=(
"Music metadata source: musicbrainz, theaudiodb, or doubanmusic. "
@@ -97,7 +104,7 @@ class ScrapeMetadataTool(MoviePilotTool):
overwrite: Optional[bool] = False,
media_type: Optional[str] = None,
music_type: Optional[str] = None,
media_source: Optional[str] = None,
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
**kwargs,
) -> str:
@@ -127,11 +134,18 @@ class ScrapeMetadataTool(MoviePilotTool):
"支持的类型:'movie', 'tv', 'music'"
),
}, ensure_ascii=False)
if bool(media_source) != bool(media_id):
explicit_identity = media_source is not None or media_id is not None
normalized_source = normalize_media_source(media_source)
normalized_media_id = str(media_id).strip() if media_id is not None else ""
if explicit_identity and (
not normalized_source or not normalized_media_id
):
return json.dumps({
"success": False,
"message": "media_source 和 media_id 必须同时提供",
"message": "必须同时提供有效的 media_source 和 media_id",
}, ensure_ascii=False)
media_source = normalized_source
media_id = normalized_media_id or None
local_path = Path(path)
is_local_directory = (storage or "local") == "local" and local_path.is_dir()
@@ -151,6 +165,7 @@ class ScrapeMetadataTool(MoviePilotTool):
)
media_chain = MediaChain()
scraping_chain = ScrapingChain()
is_audio_file = (
fileitem.type == "file"
and Path(path).suffix.lower() in settings.RMT_AUDIOEXT
@@ -179,8 +194,8 @@ class ScrapeMetadataTool(MoviePilotTool):
mediainfo = None
if media_source and media_id:
recognize_kwargs = {
"source": media_source,
"mediaid": media_id,
"media_source": media_source,
"media_id": media_id,
"mtype": MediaType.MUSIC,
}
if normalized_music_type:
@@ -205,11 +220,11 @@ class ScrapeMetadataTool(MoviePilotTool):
success, message = await self.run_blocking(
"storage",
media_chain.scrape_music_metadata,
scraping_chain.scrape_music_metadata,
fileitem=fileitem,
mediainfo=mediainfo,
overwrite=bool(overwrite),
source=media_source,
media_source=media_source,
)
result = {
"success": success,
@@ -246,7 +261,7 @@ class ScrapeMetadataTool(MoviePilotTool):
# 刮削会包含磁盘写入和外部图片/元数据访问,统一放到 storage 线程池。
await self.run_blocking(
"storage",
media_chain.scrape_metadata,
scraping_chain.scrape_metadata,
fileitem=fileitem,
meta=context.meta_info,
mediainfo=context.media_info,
@@ -262,7 +277,8 @@ class ScrapeMetadataTool(MoviePilotTool):
"title": context.media_info.title,
"year": context.media_info.year,
"type": media_type_to_agent(context.media_info.type),
"tmdb_id": context.media_info.tmdb_id,
"media_source": context.media_info.media_source,
"media_id": context.media_info.media_id,
"season": context.media_info.season,
},
},

View File

@@ -8,7 +8,6 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.media import MediaChain
from app.chain.music import MusicChain
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
from app.utils.media import resolve_media_identity
@@ -93,7 +92,7 @@ class SearchMediaTool(MoviePilotTool):
f"错误:无效的音乐实体类型 '{music_type}'"
"支持的类型:'recording', 'album', 'artist'"
)
results = await MusicChain().async_search(query=title, limit=100)
results = await MediaChain().async_search_music(query=title, limit=100)
filtered_music = [
item
for item in results or []