feat(music): 完善歌词与专辑目录整理

This commit is contained in:
jxxghp
2026-08-25 21:02:26 +08:00
parent f28e8b1538
commit 574c50586e
31 changed files with 1228 additions and 105 deletions
+79 -2
View File
@@ -4,10 +4,10 @@ from uuid import UUID
from mutagen import File as MutagenFile
from mutagen.flac import FLAC, Picture
from mutagen.id3 import APIC
from mutagen.id3 import APIC, SYLT, USLT
from mutagen.mp4 import MP4, MP4Cover
from app.domain.context import MusicInfo
from app.domain.context import MusicInfo, MusicLyrics
from app.domain.meta.metamusic import MetaMusic
from app.runtime.log import logger
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -86,6 +86,83 @@ class AudioMetadataHelper:
media_id=musicbrainz_id,
)
@classmethod
def read_lyrics(cls, path: Path) -> Optional[MusicLyrics]:
"""读取常见音频容器中的逐行同步歌词、纯文本歌词和 Lyricsfile 标签。"""
try:
audio = MutagenFile(path, easy=False)
except Exception as err:
logger.warning(f"读取内嵌歌词失败:{path} - {err}")
return None
if not audio or not audio.tags:
return None
tags = audio.tags
synced = None
plain = None
lyricsfile = None
getall = getattr(tags, "getall", None)
if callable(getall):
synced_frames = getall("SYLT")
plain_frames = getall("USLT")
synced = cls._sylt_to_lrc(synced_frames[0]) if synced_frames else None
plain = str(plain_frames[0].text or "").strip() if plain_frames else None
normalized = {
str(key).casefold(): value
for key, value in getattr(tags, "items", lambda: [])()
}
synced = synced or cls._tag_text(
normalized,
"syncedlyrics",
"synced lyrics",
"lyrics_synced",
)
plain = plain or cls._tag_text(
normalized,
"lyrics",
"unsyncedlyrics",
"unsynced lyrics",
"©lyr",
"\xa9lyr",
)
lyricsfile = cls._tag_text(normalized, "lyricsfile", "lyricsfile.yaml")
if not synced and not plain and not lyricsfile:
return None
return MusicLyrics(
provider="embedded",
plain_lyrics=plain,
synced_lyrics=synced,
lyricsfile=lyricsfile,
match_score=100,
provider_priority=100,
)
@staticmethod
def _tag_text(tags: dict[str, Any], *keys: str) -> Optional[str]:
"""从不同容器的单值或列表标签中提取首个非空文本。"""
for key in keys:
value = tags.get(key.casefold())
if isinstance(value, (list, tuple)) and value:
value = value[0]
if isinstance(value, (USLT, SYLT)):
value = getattr(value, "text", None)
text = str(value or "").strip()
if text:
return text
return None
@staticmethod
def _sylt_to_lrc(frame: SYLT) -> Optional[str]:
"""把 ID3 SYLT 的毫秒时间戳转换为通用 LRC 行。"""
output = []
for text, timestamp in frame.text or []:
if not str(text or "").strip():
continue
minutes, remainder = divmod(max(int(timestamp), 0), 60000)
output.append(f"[{minutes:02d}:{remainder / 1000:05.2f}]{str(text).strip()}")
return "\n".join(output) or None
@staticmethod
def read_filename(path: Path) -> MetaMusic:
"""只从文件名和目录结构解析音乐元数据。"""
+1
View File
@@ -168,6 +168,7 @@ class ChainRuntimeConfig:
data_cleanup_transfer_history_days: Any = 0
data_cleanup_download_failure_days: Any = 0
download_subtitle: bool = True
lyrics_batch_timeout: int = 120
music_metadata_to_simplified: bool = True
recognize_plugin_first: bool = False
ai_agent_enable: bool = False