mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-02 05:56:47 +08:00
129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
import time
|
|
from typing import Any, Optional, Union
|
|
|
|
from app.chain import ChainBase
|
|
from app.domain.context import MusicInfo, MusicLyrics
|
|
from app.domain.meta.metamusic import MetaMusic
|
|
|
|
|
|
class LyricsChain(ChainBase):
|
|
"""聚合本地、插件和宿主歌词候选,并按匹配度与内容质量择优。"""
|
|
|
|
def get_music_lyrics_candidates(
|
|
self,
|
|
music: Union[MetaMusic, MusicInfo],
|
|
local_candidates: Optional[list[MusicLyrics]] = None,
|
|
) -> list[MusicLyrics]:
|
|
"""获取所有标准化歌词候选,同时兼容旧版单结果插件接口。"""
|
|
candidates = list(local_candidates or [])
|
|
if self.deadline is not None and time.monotonic() >= self.deadline:
|
|
self.budget_exceeded = True
|
|
return self._deduplicate(candidates)
|
|
legacy = self.run_module("music_lyrics", music=music)
|
|
candidates.extend(self._normalize_results(legacy))
|
|
results = self.run_module("music_lyrics_candidates", music=music)
|
|
candidates.extend(self._normalize_results(results))
|
|
|
|
fallback = str(getattr(music, "lyrics", None) or "").strip()
|
|
if fallback:
|
|
candidates.append(MusicLyrics(
|
|
provider="theaudiodb",
|
|
plain_lyrics=fallback,
|
|
match_score=85,
|
|
provider_priority=10,
|
|
))
|
|
return self._deduplicate(candidates)
|
|
|
|
def get_music_lyrics(
|
|
self,
|
|
music: Union[MetaMusic, MusicInfo],
|
|
local_candidates: Optional[list[MusicLyrics]] = None,
|
|
) -> Optional[MusicLyrics]:
|
|
"""返回匹配可信且质量最高的歌词候选。"""
|
|
candidates = self.get_music_lyrics_candidates(music, local_candidates)
|
|
return self._select_best(candidates)
|
|
|
|
async def async_get_music_lyrics_candidates(
|
|
self,
|
|
music: Union[MetaMusic, MusicInfo],
|
|
local_candidates: Optional[list[MusicLyrics]] = None,
|
|
) -> list[MusicLyrics]:
|
|
"""异步聚合新旧模块候选,并复用同步路径的本地兜底规则。"""
|
|
candidates = list(local_candidates or [])
|
|
if self.deadline is not None and time.monotonic() >= self.deadline:
|
|
self.budget_exceeded = True
|
|
return self._deduplicate(candidates)
|
|
legacy = await self.async_run_module("music_lyrics", music=music)
|
|
candidates.extend(self._normalize_results(legacy))
|
|
results = await self.async_run_module("music_lyrics_candidates", music=music)
|
|
candidates.extend(self._normalize_results(results))
|
|
fallback = str(getattr(music, "lyrics", None) or "").strip()
|
|
if fallback:
|
|
candidates.append(MusicLyrics(
|
|
provider="theaudiodb",
|
|
plain_lyrics=fallback,
|
|
match_score=85,
|
|
provider_priority=10,
|
|
))
|
|
return self._deduplicate(candidates)
|
|
|
|
async def async_get_music_lyrics(
|
|
self,
|
|
music: Union[MetaMusic, MusicInfo],
|
|
local_candidates: Optional[list[MusicLyrics]] = None,
|
|
) -> Optional[MusicLyrics]:
|
|
"""异步返回质量最高的标准歌词候选。"""
|
|
candidates = await self.async_get_music_lyrics_candidates(music, local_candidates)
|
|
return self._select_best(candidates)
|
|
|
|
@staticmethod
|
|
def _select_best(candidates: list[MusicLyrics]) -> Optional[MusicLyrics]:
|
|
"""在可信候选中按内容质量、匹配度和来源优先级择优。"""
|
|
if not candidates:
|
|
return None
|
|
return max(
|
|
candidates,
|
|
key=lambda item: (
|
|
item.quality_rank,
|
|
item.match_score,
|
|
item.provider_priority,
|
|
),
|
|
)
|
|
|
|
@classmethod
|
|
def _normalize_results(cls, value: Any) -> list[MusicLyrics]:
|
|
"""把模块的单结果、列表或字典结果统一转换为候选列表。"""
|
|
values = value if isinstance(value, list) else [value]
|
|
normalized = []
|
|
for item in values:
|
|
if isinstance(item, MusicLyrics):
|
|
normalized.append(item)
|
|
elif isinstance(item, dict):
|
|
normalized.append(MusicLyrics.from_dict(item))
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _deduplicate(candidates: list[MusicLyrics]) -> list[MusicLyrics]:
|
|
"""按来源身份和内容去重,重复项保留质量与匹配度更高的一条。"""
|
|
selected: dict[tuple[str, str, str], MusicLyrics] = {}
|
|
for candidate in candidates:
|
|
if not candidate or not (candidate.content or candidate.instrumental):
|
|
continue
|
|
current = selected.get(candidate.identity_key)
|
|
if current is None or (
|
|
candidate.match_score,
|
|
candidate.quality_rank,
|
|
candidate.provider_priority,
|
|
) > (
|
|
current.match_score,
|
|
current.quality_rank,
|
|
current.provider_priority,
|
|
):
|
|
selected[candidate.identity_key] = candidate
|
|
return list(selected.values())
|
|
def __init__(self, deadline: Optional[float] = None) -> None:
|
|
"""保存批次查询截止时间,防止单张专辑长期占用刮削任务。"""
|
|
super().__init__()
|
|
self.deadline = deadline
|
|
self.budget_exceeded = False
|