mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-09 01:16:50 +08:00
Merge pull request #6574 from dogodefi/codex/apev2-cover-art
This commit is contained in:
@@ -3,8 +3,10 @@ from typing import Any, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from mutagen import File as MutagenFile
|
||||
from mutagen.apev2 import APEBinaryValue
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.id3 import APIC, SYLT, USLT
|
||||
from mutagen.monkeysaudio import MonkeysAudio
|
||||
from mutagen.mp4 import MP4, MP4Cover
|
||||
|
||||
from app.domain.context import MusicInfo, MusicLyrics
|
||||
@@ -54,8 +56,12 @@ class AudioMetadataHelper:
|
||||
return None
|
||||
|
||||
tags = audio.tags or {}
|
||||
track_number, total_tracks = cls._number_pair(cls._first(tags, "tracknumber"))
|
||||
disc_number, total_discs = cls._number_pair(cls._first(tags, "discnumber"))
|
||||
track_number, total_tracks = cls._number_pair(
|
||||
cls._first_of(tags, "tracknumber", "track")
|
||||
)
|
||||
disc_number, total_discs = cls._number_pair(
|
||||
cls._first_of(tags, "discnumber", "disc")
|
||||
)
|
||||
musicbrainz_id = cls._normalize_musicbrainz_id(
|
||||
cls._first_of(
|
||||
tags,
|
||||
@@ -69,8 +75,8 @@ class AudioMetadataHelper:
|
||||
title=cls._first(tags, "title"),
|
||||
artists=cls._values(tags, "artist"),
|
||||
album=cls._first(tags, "album"),
|
||||
album_artist=cls._first(tags, "albumartist"),
|
||||
year=cls._year(cls._first(tags, "date") or cls._first(tags, "originaldate")),
|
||||
album_artist=cls._first_of(tags, "albumartist", "album artist"),
|
||||
year=cls._year(cls._first_of(tags, "date", "year", "originaldate")),
|
||||
disc_number=disc_number,
|
||||
track_number=track_number,
|
||||
total_discs=total_discs,
|
||||
@@ -269,8 +275,20 @@ class AudioMetadataHelper:
|
||||
cover_mime: str,
|
||||
overwrite: bool,
|
||||
) -> None:
|
||||
"""为 MP3、FLAC 和 MP4/M4A 写入内嵌封面,其它格式保留标签写入结果。"""
|
||||
"""为 MP3、FLAC、MP4/M4A 和 APE 写入内嵌封面,其它格式保留标签写入结果。"""
|
||||
audio = MutagenFile(path)
|
||||
if isinstance(audio, MonkeysAudio):
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
cover_key = "Cover Art (Front)"
|
||||
if cover_key in audio.tags and not overwrite:
|
||||
return
|
||||
cover_filename = "cover.png" if cover_mime == "image/png" else "cover.jpg"
|
||||
audio.tags[cover_key] = APEBinaryValue(
|
||||
cover_filename.encode("ascii") + b"\x00" + cover_data
|
||||
)
|
||||
audio.save()
|
||||
return
|
||||
if isinstance(audio, FLAC):
|
||||
if audio.pictures and not overwrite:
|
||||
return
|
||||
|
||||
@@ -402,6 +402,7 @@ class ConfigModel(BaseModel):
|
||||
".alac",
|
||||
".adif",
|
||||
".adts",
|
||||
".ape",
|
||||
".flac",
|
||||
".midi",
|
||||
".opus",
|
||||
|
||||
@@ -2,6 +2,9 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mutagen.apev2 import APEBinaryValue
|
||||
from mutagen.monkeysaudio import MonkeysAudio
|
||||
|
||||
from app.chain.media import MediaChain
|
||||
from app.domain.context import MusicInfo
|
||||
from app.domain.meta.metamusic import (
|
||||
@@ -182,6 +185,32 @@ def test_read_audio_tags_ignores_invalid_musicbrainz_id(monkeypatch):
|
||||
assert meta.media_id is None
|
||||
|
||||
|
||||
def test_read_audio_tags_accepts_conventional_apev2_names(monkeypatch):
|
||||
"""APE 常见字段名应映射为标准专辑、曲序、碟号和年份。"""
|
||||
audio = SimpleNamespace(
|
||||
tags={
|
||||
"title": ["天下太平"],
|
||||
"artist": ["陈奕迅", "张学友"],
|
||||
"album": ["Solidays"],
|
||||
"album artist": ["陈奕迅"],
|
||||
"track": ["12/14"],
|
||||
"disc": ["1/2"],
|
||||
"year": ["2008"],
|
||||
},
|
||||
info=SimpleNamespace(length=252),
|
||||
)
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
meta = AudioMetadataHelper.read_tags(Path("/music/天下太平.ape"))
|
||||
|
||||
assert meta.album_artist == "陈奕迅"
|
||||
assert meta.track_number == 12
|
||||
assert meta.total_tracks == 14
|
||||
assert meta.disc_number == 1
|
||||
assert meta.total_discs == 2
|
||||
assert meta.year == 2008
|
||||
|
||||
|
||||
def test_remote_path_meta_parses_track_prefix_once(tmp_path):
|
||||
"""远程或尚未落盘的音频路径应先剥离曲序,不能把 08 误识别成艺术家。"""
|
||||
audio_path = tmp_path / "Daft Punk - Random Access Memories (2013)" / "08 - Get Lucky.flac"
|
||||
@@ -358,3 +387,59 @@ def test_write_audio_metadata_can_embed_cover_without_rewriting_tags(monkeypatch
|
||||
cover_mime="image/jpeg",
|
||||
overwrite=False,
|
||||
)
|
||||
|
||||
|
||||
def test_write_audio_metadata_embeds_apev2_front_cover(monkeypatch):
|
||||
"""APE 封面应按 APEv2 约定写入文件名、空字节和图片数据。"""
|
||||
class FakeMonkeysAudio(MonkeysAudio):
|
||||
"""记录 Monkey's Audio 封面写入结果。"""
|
||||
|
||||
def __init__(self):
|
||||
self.tags = {}
|
||||
self.saved = False
|
||||
|
||||
def save(self, *_args, **_kwargs):
|
||||
self.saved = True
|
||||
|
||||
audio = FakeMonkeysAudio()
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
AudioMetadataHelper._write_cover(
|
||||
path=Path("/music/track.ape"),
|
||||
cover_data=b"jpeg-data",
|
||||
cover_mime="image/jpeg",
|
||||
overwrite=True,
|
||||
)
|
||||
|
||||
cover = audio.tags["Cover Art (Front)"]
|
||||
assert isinstance(cover, APEBinaryValue)
|
||||
assert bytes(cover) == b"cover.jpg\x00jpeg-data"
|
||||
assert audio.saved is True
|
||||
|
||||
|
||||
def test_write_audio_metadata_preserves_existing_apev2_cover(monkeypatch):
|
||||
"""关闭覆盖时应保留已有 APEv2 正面封面。"""
|
||||
class FakeMonkeysAudio(MonkeysAudio):
|
||||
"""记录 Monkey's Audio 封面覆盖行为。"""
|
||||
|
||||
def __init__(self):
|
||||
self.tags = {
|
||||
"Cover Art (Front)": APEBinaryValue(b"old.jpg\x00old-data")
|
||||
}
|
||||
self.saved = False
|
||||
|
||||
def save(self, *_args, **_kwargs):
|
||||
self.saved = True
|
||||
|
||||
audio = FakeMonkeysAudio()
|
||||
monkeypatch.setattr("app.application.audio.MutagenFile", lambda *_args, **_kwargs: audio)
|
||||
|
||||
AudioMetadataHelper._write_cover(
|
||||
path=Path("/music/track.ape"),
|
||||
cover_data=b"new-data",
|
||||
cover_mime="image/jpeg",
|
||||
overwrite=False,
|
||||
)
|
||||
|
||||
assert bytes(audio.tags["Cover Art (Front)"]) == b"old.jpg\x00old-data"
|
||||
assert audio.saved is False
|
||||
|
||||
@@ -343,6 +343,17 @@ def test_metainfo_routes_audio_filename_to_music():
|
||||
assert meta.apply_words == []
|
||||
|
||||
|
||||
def test_metainfo_routes_ape_filename_to_music():
|
||||
"""Monkey's Audio 文件应使用默认音频扩展配置进入音乐识别分支。"""
|
||||
meta = MetaInfo("陈奕迅 - 天下太平.ape")
|
||||
|
||||
assert isinstance(meta, MetaMusic)
|
||||
assert meta.type == MediaType.MUSIC
|
||||
assert meta.title == "天下太平"
|
||||
assert meta.artists == ["陈奕迅"]
|
||||
assert meta.audio_format == "APE"
|
||||
|
||||
|
||||
def test_metainfo_routes_audio_path_to_music_without_parent_merge():
|
||||
"""音频路径应直接构造音乐元数据,不参与影视季集合并,并拆分歌手与曲名。"""
|
||||
meta = MetaInfoPath(Path("/music/叶惠美/周杰伦 - 晴天.flac"))
|
||||
|
||||
Reference in New Issue
Block a user