mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 20:54:32 +08:00
- mTorrent 字幕链接解析下沉为 IndexerModule.site_subtitle_links,subtitle 模块自行跳过 API 站点 - TMDB/MusicBrainz 识别缓存管理改为模块方法(tmdb_cache_*/music_cache_*)经 run_module 分发 - WechatClawBot 客户端查找/临时客户端/缓存迁移内聚为 wechatclawbot_* 模块方法, chain 移除 WechatClawBot 类与 ModuleManager 内省 - LISTENBRAINZ_* 常量迁至 schemas/types.py,模块与链层再导入保持兼容 - TMDbException 升为 schemas/exception.py 跨层契约,vendored 异常保持类身份一致 - 架构守护测试新增 test_chain_does_not_import_module_internals(共 18 项) - 文档同步:chain->module 仅允许 run_module 分发,直接导入禁止
304 lines
11 KiB
Python
304 lines
11 KiB
Python
from typing import Any, Optional
|
|
|
|
from app.chain import ChainBase
|
|
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
|
from app.domain.meta.metamusic import MetaMusic
|
|
from app.schemas.types import MediaSource, MediaType
|
|
|
|
|
|
class _MusicMetadataSourceChain(ChainBase):
|
|
"""固定音乐元数据来源链的共用端口适配。"""
|
|
|
|
source: MediaSource
|
|
|
|
def search_music(self, meta: MetaMusic, limit: int = 20) -> list[MusicInfo]:
|
|
"""按音乐元数据搜索当前来源候选。"""
|
|
result = self.run_module(
|
|
"search_music",
|
|
meta=meta,
|
|
limit=limit,
|
|
media_source=self.source,
|
|
)
|
|
return self._music_infos(result, limit=limit)
|
|
|
|
async def async_search_music(self, meta: MetaMusic, limit: int = 20) -> list[MusicInfo]:
|
|
"""异步按音乐元数据搜索当前来源候选。"""
|
|
result = await self.async_run_module(
|
|
"search_music",
|
|
meta=meta,
|
|
limit=limit,
|
|
media_source=self.source,
|
|
)
|
|
return self._music_infos(result, limit=limit)
|
|
|
|
def recognize_music(
|
|
self,
|
|
meta: Optional[MetaMusic] = None,
|
|
media_id: Optional[str] = None,
|
|
cache: bool = True,
|
|
music_type: Optional[str] = None,
|
|
) -> Optional[MusicInfo]:
|
|
"""按当前来源身份或音乐元数据识别标准音乐信息。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
result = self.run_module(
|
|
"recognize_media",
|
|
meta=meta,
|
|
mtype=MediaType.MUSIC,
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
cache=cache,
|
|
music_type=music_type,
|
|
)
|
|
return self._music_info(result, media_id=normalized_id)
|
|
|
|
async def async_recognize_music(
|
|
self,
|
|
meta: Optional[MetaMusic] = None,
|
|
media_id: Optional[str] = None,
|
|
cache: bool = True,
|
|
music_type: Optional[str] = None,
|
|
) -> Optional[MusicInfo]:
|
|
"""异步按当前来源身份或音乐元数据识别标准音乐信息。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
result = await self.async_run_module(
|
|
"async_recognize_media",
|
|
meta=meta,
|
|
mtype=MediaType.MUSIC,
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
cache=cache,
|
|
music_type=music_type,
|
|
)
|
|
return self._music_info(result, media_id=normalized_id)
|
|
|
|
def get_music_album(self, media_id: str) -> Optional[MusicAlbumInfo]:
|
|
"""按当前来源原生 ID 获取专辑详情。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return None
|
|
result = self.run_module(
|
|
"music_album",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
)
|
|
return self._music_album(result, media_id=normalized_id)
|
|
|
|
async def async_get_music_album(self, media_id: str) -> Optional[MusicAlbumInfo]:
|
|
"""异步按当前来源原生 ID 获取专辑详情。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return None
|
|
result = await self.async_run_module(
|
|
"music_album",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
)
|
|
return self._music_album(result, media_id=normalized_id)
|
|
|
|
async def async_get_music_album_related(
|
|
self,
|
|
media_id: str,
|
|
count: int = 24,
|
|
) -> list[MusicInfo]:
|
|
"""异步获取当前来源的专辑关联条目。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return []
|
|
result = await self.async_run_module(
|
|
"music_album_related",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
count=count,
|
|
)
|
|
return self._music_infos(result, limit=count)
|
|
|
|
async def async_get_music_artist(self, media_id: str) -> Optional[MusicArtistInfo]:
|
|
"""异步按当前来源原生 ID 获取艺术家详情。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return None
|
|
result = await self.async_run_module(
|
|
"music_artist",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
)
|
|
return self._music_artist(result, media_id=normalized_id)
|
|
|
|
async def async_get_music_artist_albums(
|
|
self,
|
|
media_id: str,
|
|
page: int = 1,
|
|
count: int = 30,
|
|
album_type: Optional[str] = None,
|
|
) -> list[MusicInfo]:
|
|
"""异步分页获取当前来源艺术家的专辑目录。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return []
|
|
result = await self.async_run_module(
|
|
"music_artist_albums",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
page=page,
|
|
count=count,
|
|
album_type=album_type,
|
|
)
|
|
return self._music_infos(result, limit=count)
|
|
|
|
async def async_get_music_artist_related(
|
|
self,
|
|
media_id: str,
|
|
count: int = 24,
|
|
) -> list[MusicArtistInfo]:
|
|
"""异步获取当前来源艺术家的关联艺术家。"""
|
|
normalized_id = self._normalize_media_id(media_id)
|
|
if not normalized_id:
|
|
return []
|
|
result = await self.async_run_module(
|
|
"music_artist_related",
|
|
media_source=self.source,
|
|
media_id=normalized_id,
|
|
count=count,
|
|
)
|
|
return self._music_artists(result, limit=count)
|
|
|
|
@staticmethod
|
|
def _normalize_media_id(media_id: Optional[str]) -> Optional[str]:
|
|
"""清理来源原生 ID,空值和历史零哨兵按无身份处理。"""
|
|
normalized = str(media_id).strip() if media_id is not None else ""
|
|
return normalized if normalized and normalized != "0" else None
|
|
|
|
def _music_infos(
|
|
self,
|
|
result: Any,
|
|
limit: Optional[int] = None,
|
|
) -> list[MusicInfo]:
|
|
"""将模块或插件结果统一转换为当前来源的音乐候选列表。"""
|
|
candidates = result if isinstance(result, list) else []
|
|
infos = [
|
|
item if isinstance(item, MusicInfo) else MusicInfo.from_dict(item)
|
|
for item in candidates
|
|
if isinstance(item, (MusicInfo, dict))
|
|
]
|
|
infos = [info for info in infos if info.media_source == self.source]
|
|
return infos[:limit] if limit else infos
|
|
|
|
def _music_info(
|
|
self,
|
|
result: Any,
|
|
media_id: Optional[str] = None,
|
|
) -> Optional[MusicInfo]:
|
|
"""校验单条识别结果的来源与显式身份。"""
|
|
if isinstance(result, MusicInfo):
|
|
info = result
|
|
elif isinstance(result, dict):
|
|
info = MusicInfo.from_dict(result)
|
|
else:
|
|
return None
|
|
if info.media_source and info.media_source != self.source:
|
|
return None
|
|
if media_id and (info.media_source != self.source or info.media_id != media_id):
|
|
return None
|
|
return info
|
|
|
|
def _music_album(
|
|
self,
|
|
result: Any,
|
|
media_id: Optional[str] = None,
|
|
) -> Optional[MusicAlbumInfo]:
|
|
"""将模块或插件结果统一转换为专辑详情。"""
|
|
if isinstance(result, MusicAlbumInfo):
|
|
album = result
|
|
elif isinstance(result, dict):
|
|
album = MusicAlbumInfo.from_dict(result)
|
|
else:
|
|
return None
|
|
if album.media_source != self.source:
|
|
return None
|
|
if media_id and album.media_id != media_id:
|
|
return None
|
|
return album
|
|
|
|
def _music_artist(
|
|
self,
|
|
result: Any,
|
|
media_id: Optional[str] = None,
|
|
) -> Optional[MusicArtistInfo]:
|
|
"""将模块或插件结果统一转换为艺术家详情。"""
|
|
if isinstance(result, MusicArtistInfo):
|
|
artist = result
|
|
elif isinstance(result, dict):
|
|
artist = MusicArtistInfo.from_dict(result)
|
|
else:
|
|
return None
|
|
if artist.media_source != self.source:
|
|
return None
|
|
if media_id and artist.media_id != media_id:
|
|
return None
|
|
return artist
|
|
|
|
def _music_artists(
|
|
self,
|
|
result: Any,
|
|
limit: Optional[int] = None,
|
|
) -> list[MusicArtistInfo]:
|
|
"""将模块或插件结果统一转换为艺术家列表。"""
|
|
candidates = result if isinstance(result, list) else []
|
|
artists = [
|
|
item if isinstance(item, MusicArtistInfo) else MusicArtistInfo.from_dict(item)
|
|
for item in candidates
|
|
if isinstance(item, (MusicArtistInfo, dict))
|
|
]
|
|
artists = [artist for artist in artists if artist.media_source == self.source]
|
|
return artists[:limit] if limit else artists
|
|
|
|
|
|
class MusicBrainzChain(_MusicMetadataSourceChain):
|
|
"""MusicBrainz 音乐搜索、识别与详情来源链。"""
|
|
|
|
source = MediaSource.MusicBrainz
|
|
|
|
def match_music_album(
|
|
self,
|
|
meta: MetaMusic,
|
|
tracks: list[MetaMusic],
|
|
limit: int = 5,
|
|
) -> Optional[MusicAlbumInfo]:
|
|
"""按目录元数据与曲目证据匹配 MusicBrainz 发行版本。"""
|
|
result = self.run_module(
|
|
"match_music_album",
|
|
meta=meta,
|
|
tracks=tracks,
|
|
limit=limit,
|
|
)
|
|
return self._music_album(result)
|
|
|
|
async def async_match_music_album(
|
|
self,
|
|
meta: MetaMusic,
|
|
tracks: list[MetaMusic],
|
|
limit: int = 5,
|
|
) -> Optional[MusicAlbumInfo]:
|
|
"""异步按目录元数据与曲目证据匹配 MusicBrainz 发行版本。"""
|
|
result = await self.async_run_module(
|
|
"async_match_music_album",
|
|
meta=meta,
|
|
tracks=tracks,
|
|
limit=limit,
|
|
)
|
|
return self._music_album(result)
|
|
|
|
def cache_items(self) -> list[dict]:
|
|
"""查询音乐识别缓存条目列表。"""
|
|
result = self.run_module("music_cache_items")
|
|
return result or []
|
|
|
|
def delete_cache(self, cache_key: str) -> dict:
|
|
"""按缓存键删除单条音乐识别缓存。"""
|
|
result = self.run_module("music_cache_delete", cache_key=cache_key)
|
|
return result or {}
|
|
|
|
def clear_cache(self) -> None:
|
|
"""清空全部音乐识别缓存。"""
|
|
self.run_module("music_cache_clear")
|