mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-22 08:43:37 +08:00
refactor(media): unify media identity and chain responsibilities
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user