mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
129 lines
4.6 KiB
Python
129 lines
4.6 KiB
Python
"""多来源音乐目录搜索应用服务。"""
|
|
|
|
import asyncio
|
|
from typing import Any, Callable, Iterable, Optional
|
|
|
|
from app.domain.context import MusicInfo
|
|
from app.domain.meta.metamusic import MetaMusic
|
|
from app.schemas.media import normalize_media_source
|
|
from app.schemas.types import MediaSource, MediaSourceSelection
|
|
|
|
|
|
class MusicCatalogService:
|
|
"""编排音乐来源选择、容错搜索和候选归一化。"""
|
|
|
|
def __init__(
|
|
self,
|
|
source_resolver: Callable[[MediaSource], Any],
|
|
warning: Callable[[str], None],
|
|
primary_source: MediaSource = MediaSource.MusicBrainz,
|
|
) -> None:
|
|
"""注入来源解析器、告警输出和默认音乐来源。"""
|
|
self._source_resolver = source_resolver
|
|
self._warning = warning
|
|
self._primary_source = primary_source
|
|
|
|
def search_sources(
|
|
self,
|
|
media_source: Optional[MediaSourceSelection],
|
|
) -> list[MediaSource]:
|
|
"""解析有序音乐来源,保留合法插件扩展来源并去重。"""
|
|
if not media_source:
|
|
return [self._primary_source]
|
|
raw_sources = (
|
|
(media_source,)
|
|
if isinstance(media_source, MediaSource)
|
|
else media_source
|
|
)
|
|
sources = []
|
|
for raw_source in raw_sources:
|
|
source = normalize_media_source(raw_source)
|
|
if source and source not in sources:
|
|
sources.append(source)
|
|
return sources
|
|
|
|
@staticmethod
|
|
def normalize_candidates(
|
|
candidates: Optional[Iterable[MusicInfo | dict[str, Any]]],
|
|
limit: Optional[int] = None,
|
|
) -> list[MusicInfo]:
|
|
"""标准化并按来源身份或元数据去重音乐候选。"""
|
|
results = []
|
|
identities = set()
|
|
for candidate in candidates or []:
|
|
info = candidate if isinstance(candidate, MusicInfo) else MusicInfo.from_dict(candidate)
|
|
if info.media_source and info.media_id:
|
|
identity = (
|
|
"id",
|
|
str(info.media_source).casefold(),
|
|
str(info.music_type).casefold(),
|
|
str(info.media_id).casefold(),
|
|
)
|
|
else:
|
|
identity = (
|
|
"metadata",
|
|
str(info.music_type).casefold(),
|
|
MetaMusic.compact_text(info.title),
|
|
MetaMusic.compact_text(info.artist),
|
|
MetaMusic.compact_text(info.album),
|
|
)
|
|
if identity in identities:
|
|
continue
|
|
identities.add(identity)
|
|
results.append(info)
|
|
if limit and len(results) >= limit:
|
|
break
|
|
return results
|
|
|
|
def search(
|
|
self,
|
|
query: str,
|
|
limit: int = 20,
|
|
media_source: Optional[MediaSourceSelection] = None,
|
|
) -> list[MusicInfo]:
|
|
"""顺序搜索一个或多个音乐来源,隔离单一来源失败。"""
|
|
meta = MetaMusic.parse_query(query)
|
|
candidates = []
|
|
for source in self.search_sources(media_source):
|
|
chain = self._source_resolver(source)
|
|
if not chain:
|
|
continue
|
|
try:
|
|
candidates.extend(chain.search_music(meta, limit=limit))
|
|
except Exception as error:
|
|
self._warning(f"音乐来源 {source} 搜索失败:{str(error)}")
|
|
return self.normalize_candidates(candidates, limit=limit)
|
|
|
|
async def async_search(
|
|
self,
|
|
query: str,
|
|
limit: int = 20,
|
|
media_source: Optional[MediaSourceSelection] = None,
|
|
) -> list[MusicInfo]:
|
|
"""并行搜索一个或多个音乐来源,隔离单一来源失败。"""
|
|
meta = MetaMusic.parse_query(query)
|
|
searches = []
|
|
for source in self.search_sources(media_source):
|
|
chain = self._source_resolver(source)
|
|
if chain:
|
|
searches.append(self._async_search_source(chain, source, meta, limit))
|
|
source_results = await asyncio.gather(*searches) if searches else []
|
|
return self.normalize_candidates(
|
|
[candidate for results in source_results for candidate in results],
|
|
limit=limit,
|
|
)
|
|
|
|
async def _async_search_source(
|
|
self,
|
|
chain: Any,
|
|
source: MediaSource,
|
|
meta: MetaMusic,
|
|
limit: int,
|
|
) -> list[MusicInfo]:
|
|
"""异步搜索单个来源,并把异常降级为空候选。"""
|
|
try:
|
|
return await chain.async_search_music(meta, limit=limit)
|
|
except Exception as error:
|
|
self._warning(f"音乐来源 {source} 搜索失败:{str(error)}")
|
|
return []
|