feat: unify media recognition and music lifecycle

This commit is contained in:
jxxghp
2026-08-12 06:51:58 +08:00
parent b018646f61
commit 97dba2a8ae
96 changed files with 4677 additions and 719 deletions

View File

@@ -3,39 +3,22 @@
from typing import Any, Optional
from app.core.context import (
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_ARTIST,
MUSIC_ENTITY_RECORDING,
MusicAlbumInfo,
MusicArtistInfo,
MusicInfo,
)
from app.schemas.types import media_type_to_agent
from app.schemas.types import (
MUSIC_ENTITY_TYPES,
MUSIC_SUBSCRIBABLE_TYPES,
media_type_to_agent,
)
from app.utils.media import normalize_music_type
MUSIC_ENTITY_TYPES = frozenset({
MUSIC_ENTITY_RECORDING,
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_ARTIST,
})
MUSIC_SUBSCRIBABLE_TYPES = frozenset({
MUSIC_ENTITY_RECORDING,
MUSIC_ENTITY_ALBUM,
})
MUSIC_TRACK_PREVIEW_LIMIT = 100
MUSIC_RELEASE_PREVIEW_LIMIT = 20
def normalize_music_type(
value: Optional[str],
*,
allow_artist: bool = True,
) -> Optional[str]:
"""规范化音乐实体类型,非法值返回 None。"""
normalized = str(value or "").strip().lower()
allowed = MUSIC_ENTITY_TYPES if allow_artist else MUSIC_SUBSCRIBABLE_TYPES
return normalized if normalized in allowed else None
def simplify_music_info(info: MusicInfo) -> dict[str, Any]:
"""精简音乐列表项,同时保留订阅和下载所需的稳定身份。"""
payload = {

View File

@@ -9,7 +9,7 @@ from app.agent.tools.tags import ToolTag
from app.chain.subscribe import SubscribeChain
from app.db.user_oper import UserOper
from app.log import logger
from app.schemas.types import MediaType, MessageChannel
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType, MessageChannel
from ._music_utils import normalize_music_type
@@ -290,7 +290,7 @@ class AddSubscribeTool(MoviePilotTool):
if sid:
display_year = f" ({year})" if year else ""
music_label = (
"专辑" if normalized_music_type == "album" else "单曲"
"专辑" if normalized_music_type == MUSIC_ENTITY_ALBUM else "单曲"
) if normalized_music_type else ""
if message and "已存在" in message:
result_msg = f"{music_label}订阅已存在:{title}{display_year}"

View File

@@ -15,7 +15,12 @@ from app.modules.listenbrainz import (
LISTENBRAINZ_FRESH_MAX_DAYS,
LISTENBRAINZ_FRESH_SORTS,
)
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_RECORDING,
MediaType,
media_type_to_agent,
)
from ._music_utils import normalize_music_type, simplify_music_info
@@ -176,10 +181,10 @@ class GetRecommendationsTool(MoviePilotTool):
sort_by=sort_by,
min_listen_count=max(0, min_listen_count or 0),
with_cover=bool(with_cover),
entity=normalized_music_type or "recording",
entity=normalized_music_type or MUSIC_ENTITY_RECORDING,
)
else:
if normalized_music_type and normalized_music_type != "album":
if normalized_music_type and normalized_music_type != MUSIC_ENTITY_ALBUM:
return "错误ListenBrainz 新发行结果只支持 music_type='album'"
if fresh_sort not in LISTENBRAINZ_FRESH_SORTS:
return f"错误:无效的新发行排序 '{fresh_sort}'"

View File

@@ -11,7 +11,7 @@ from app.chain.download import DownloadChain
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.log import logger
from app.schemas import DownloaderTorrent
from app.schemas.types import TorrentQueryStatus, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_RECORDING, TorrentQueryStatus, media_type_to_agent
class QueryDownloadTasksInput(BaseModel):
@@ -124,7 +124,7 @@ class QueryDownloadTasksTool(MoviePilotTool):
music_media = music_note.get("media") or {}
if media_type_to_agent(history.type) == "music":
media_payload.update({
"music_type": music_media.get("music_type") or "recording",
"music_type": music_media.get("music_type") or MUSIC_ENTITY_RECORDING,
"artists": music_media.get("artists") or [],
"album": music_media.get("album"),
"album_id": music_media.get("album_id"),

View File

@@ -10,10 +10,9 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.chain.mediaserver import MediaServerChain
from app.core.context import MUSIC_ENTITY_ALBUM
from app.helper.mediaserver import MediaServerHelper
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType, media_type_to_agent
from ._music_utils import normalize_music_type

View File

@@ -10,13 +10,13 @@ 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.core.context import (
from app.log import logger
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
MUSIC_ENTITY_ARTIST,
MUSIC_ENTITY_RECORDING,
MediaType,
)
from app.log import logger
from app.schemas.types import MediaType
from ._music_utils import (
normalize_music_type,
simplify_music_album,
@@ -200,6 +200,7 @@ class QueryMediaDetailTool(MoviePilotTool):
source=media_source,
mediaid=media_id,
mtype=MediaType.MUSIC,
music_type=normalized_music_type,
)
if (
not mediainfo

View File

@@ -11,7 +11,7 @@ from app.agent.tools.tags import ToolTag
from app.core.context import MediaInfo
from app.helper.server import MoviePilotServerHelper
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaType, media_type_to_agent
from ._music_utils import normalize_music_type
MAX_PAGE_SIZE = 50
@@ -133,7 +133,7 @@ class QueryPopularSubscribesTool(MoviePilotTool):
raw_type = str(sub.get("type") or "").strip().lower()
if raw_type in ["music", "音乐"]:
sub_music_type = normalize_music_type(
sub.get("music_type") or "recording",
sub.get("music_type") or MUSIC_ENTITY_RECORDING,
allow_artist=False,
)
if not sub_music_type:

View File

@@ -9,7 +9,7 @@ from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.subscribehistory_oper import SubscribeHistoryOper
from app.log import logger
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaType, media_type_to_agent
from ._music_utils import normalize_music_type
PAGE_SIZE = 20
@@ -197,7 +197,7 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
return True
if media_type_to_agent(getattr(record, "type", None)) != "music":
return False
return (getattr(record, "music_type", None) or "recording") == music_type
return (getattr(record, "music_type", None) or MUSIC_ENTITY_RECORDING) == music_type
@staticmethod
def _simplify_records(records) -> list:
@@ -217,7 +217,7 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
"media_source": record.media_source,
"media_id": record.media_id,
"music_type": getattr(record, "music_type", None) or (
"recording" if media_type_to_agent(record.type) == "music" else None
MUSIC_ENTITY_RECORDING if media_type_to_agent(record.type) == "music" else None
),
"total_tracks": getattr(record, "total_tracks", None),
"poster": record.poster,

View File

@@ -9,7 +9,7 @@ from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.helper.server import MoviePilotServerHelper
from app.log import logger
from app.schemas.types import media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_RECORDING, media_type_to_agent
from ._music_utils import normalize_music_type
MAX_PAGE_SIZE = 50
@@ -97,7 +97,7 @@ class QuerySubscribeSharesTool(MoviePilotTool):
normalized_music_type = None
if normalized_type == "music":
normalized_music_type = normalize_music_type(
share.get("music_type") or "recording",
share.get("music_type") or MUSIC_ENTITY_RECORDING,
allow_artist=False,
)
if not normalized_music_type:

View File

@@ -10,7 +10,7 @@ from app.agent.tools.tags import ToolTag
from app.db.subscribe_oper import SubscribeOper
from app.log import logger
from app.schemas.subscribe import Subscribe as SubscribeSchema
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaType, media_type_to_agent
from ._music_utils import normalize_music_type
PAGE_SIZE = 100
@@ -190,7 +190,7 @@ class QuerySubscribesTool(MoviePilotTool):
if media_id is not None and sub.media_id != media_id:
continue
if normalized_music_type:
sub_music_type = sub.music_type or "recording"
sub_music_type = sub.music_type or MUSIC_ENTITY_RECORDING
if sub_music_type != normalized_music_type:
continue
filtered_subscribes.append(sub)
@@ -217,7 +217,7 @@ class QuerySubscribesTool(MoviePilotTool):
payload["manual_total_episode"] = subscribe.manual_total_episode or 0
payload["type"] = media_type_to_agent(payload.get("type"))
if payload["type"] == "music" and not payload.get("music_type"):
payload["music_type"] = "recording"
payload["music_type"] = MUSIC_ENTITY_RECORDING
full_subscribes.append(payload)
result_json = json.dumps(full_subscribes, ensure_ascii=False, indent=2)

View File

@@ -9,12 +9,10 @@ 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.core.config import settings
from app.core.context import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_ARTIST
from app.log import logger
from app.schemas import FileItem
from app.schemas.types import MediaType, media_type_to_agent
from app.schemas.types import MUSIC_ENTITY_ARTIST, MediaType, media_type_to_agent
from ._music_utils import normalize_music_type, simplify_music_info
@@ -180,18 +178,16 @@ class ScrapeMetadataTool(MoviePilotTool):
mediainfo = None
if media_source and media_id:
if normalized_music_type == MUSIC_ENTITY_ALBUM:
album_info = await MusicChain().async_album(
source=media_source,
media_id=media_id,
)
mediainfo = album_info.to_music_info() if album_info else None
else:
mediainfo = await media_chain.async_recognize_media(
source=media_source,
mediaid=media_id,
mtype=MediaType.MUSIC,
)
recognize_kwargs = {
"source": media_source,
"mediaid": media_id,
"mtype": MediaType.MUSIC,
}
if normalized_music_type:
recognize_kwargs["music_type"] = normalized_music_type
mediainfo = await media_chain.async_recognize_media(
**recognize_kwargs
)
if not mediainfo:
return json.dumps({
"success": False,

View File

@@ -126,18 +126,11 @@ class SearchTorrentsTool(MoviePilotTool):
source=media_source,
mediaid=media_id,
mtype=media_type_enum,
music_type=normalized_music_type,
area=area or "title",
sites=sites,
cache_local=False,
)
if normalized_music_type:
filtered_torrents = [
context
for context in filtered_torrents or []
if getattr(context.media_info, "music_type", None)
== normalized_music_type
]
# 获取站点信息
all_indexers = await SitesHelper().async_get_indexers()
all_sites = [{"id": indexer.get("id"), "name": indexer.get("name")} for indexer in (all_indexers or [])]

View File

@@ -7,9 +7,9 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.core.context import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING
from app.log import logger
from app.schemas import FileItem, MediaType
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING
from ._music_utils import normalize_music_type