mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 13:28:32 +08:00
fix(music): preserve native APE identities and precise tag dates
This commit is contained in:
@@ -603,13 +603,20 @@ class AudioMetadataHelper:
|
||||
@classmethod
|
||||
def _needs_tag_write(cls, audio: Any, music: Union[MetaMusic, MusicInfo], overwrite: bool) -> bool:
|
||||
"""先只读比较已有标签,完整元数据不应触发大音频文件的副本创建。"""
|
||||
return bool(cls._tag_updates(audio, music, overwrite))
|
||||
|
||||
@classmethod
|
||||
def _tag_updates(cls, audio: Any, music: Union[MetaMusic, MusicInfo], overwrite: bool) -> dict[str, list[str]]:
|
||||
"""预检和实际写入共用字段差异,年份模型不能抹掉标签中同年的完整日期。"""
|
||||
tags = cls._readable_tags(audio.tags)
|
||||
aliases = {"albumartist": ("albumartist", "album artist"), "date": ("date", "year"),
|
||||
"originaldate": ("originaldate", "originalyear"),
|
||||
"tracknumber": ("tracknumber", "track"), "discnumber": ("discnumber", "disc"),
|
||||
"musicbrainz_trackid": ("musicbrainz_trackid", "musicbrainz_track_id"),
|
||||
"musicbrainz_albumid": ("musicbrainz_albumid", "musicbrainz_album_id"),
|
||||
"musicbrainz_releasegroupid": ("musicbrainz_releasegroupid", "musicbrainz_release_group_id"),
|
||||
"musicbrainz_releasetrackid": ("musicbrainz_releasetrackid", "musicbrainz_release_track_id")}
|
||||
updates = {}
|
||||
for key, value in cls._tag_values(music).items():
|
||||
if value in (None, "", []):
|
||||
continue
|
||||
@@ -618,9 +625,11 @@ class AudioMetadataHelper:
|
||||
if current and key in {"tracknumber", "discnumber"} and cls._number_pair(current[0]) == cls._number_pair(expected[0]):
|
||||
# MP4用(1, 0)表达未知总数,与文本标签的1具有相同位置语义。
|
||||
continue
|
||||
if key in {"date", "originaldate"} and current and len(expected[0]) == 4 and current[0].startswith(f"{expected[0]}-"):
|
||||
continue
|
||||
if not current or (overwrite and current != expected):
|
||||
return True
|
||||
return False
|
||||
updates[key] = expected
|
||||
return updates
|
||||
|
||||
@staticmethod
|
||||
def _has_cover(audio: Any) -> bool:
|
||||
@@ -641,8 +650,7 @@ class AudioMetadataHelper:
|
||||
"""按标签策略保存可支持的字段;空标签音频仍然是可写的有效容器。"""
|
||||
if audio.tags is None:
|
||||
audio.add_tags()
|
||||
values = {key: value if isinstance(value, list) else [str(value)]
|
||||
for key, value in cls._tag_values(music).items() if value not in (None, "", [])}
|
||||
values = cls._tag_updates(audio, music, overwrite)
|
||||
if isinstance(audio.tags, ID3):
|
||||
changed = cls._write_id3_tags(audio.tags, values, overwrite)
|
||||
elif isinstance(audio.tags, MP4Tags):
|
||||
@@ -655,9 +663,9 @@ class AudioMetadataHelper:
|
||||
@classmethod
|
||||
def _write_text_tags(cls, audio: Any, values: dict[str, list[str]], overwrite: bool, path: Path) -> bool:
|
||||
"""写入Vorbis/APEv2文本字段,并使用APE播放器通用的字段名。"""
|
||||
ape_fields = {"albumartist": "Album Artist", "date": "Year", "tracknumber": "Track", "discnumber": "Disc",
|
||||
"musicbrainz_trackid": "MusicBrainz Track Id", "musicbrainz_albumid": "MusicBrainz Album Id",
|
||||
"musicbrainz_releasegroupid": "MusicBrainz Release Group Id", "musicbrainz_releasetrackid": "MusicBrainz Release Track Id"}
|
||||
# APEv2的MusicBrainz字段沿用下划线名称,不能套用ID3 TXXX描述中的空格。
|
||||
ape_fields = {"albumartist": "Album Artist", "date": "Year", "originaldate": "Originalyear",
|
||||
"tracknumber": "Track", "discnumber": "Disc"}
|
||||
changed = False
|
||||
for name, value in values.items():
|
||||
key = ape_fields.get(name, name) if isinstance(audio.tags, APEv2) else name
|
||||
|
||||
@@ -545,7 +545,9 @@ SSE 的 `candidate_items` 是站点原始返回数量,`match_counts` 记录身
|
||||
写入失败或目标在处理期间改变时,丢弃临时副本,不覆盖原目标。LRC 与 Lyricsfile 旁挂也按目录项原子替换。
|
||||
WAV、DSF 和 MP3 使用原生 ID3 帧,MP4/M4A 使用标准 atom 及文本 freeform;无标签音频可直接补写,
|
||||
错误扩展名按实际容器处理。Recording、Release、Release Group、Release Track ID 分别写入专用标签;
|
||||
仅有首发年份时只写 `ORIGINALDATE`,不能据此生成当前发行日期。
|
||||
仅有首发年份时只写原始年份标签,不能据此生成当前发行日期;已有同年的完整日期不因模型只携带年份而截断。
|
||||
APEv2 按[标准标签映射](https://picard-docs.musicbrainz.org/en/latest/appendices/tag_mapping.html)
|
||||
使用下划线形式的 MusicBrainz 字段和 `Originalyear`,不套用 ID3 的带空格描述;读取仍兼容旧字段别名。
|
||||
|
||||
音乐整理按每个文件的 `storage` 决定证据来源:远端仅使用名称、目录和原始种子线索,
|
||||
不读取本机同路径的标签、时长或 CUE。仅有 `.m4a` 扩展名时不声明 AAC/ALAC 或有损/无损。
|
||||
|
||||
@@ -64,7 +64,9 @@ Music recognition, exploration, album and artist navigation, and recognition-cac
|
||||
Untagged files can be written, and incorrect extensions use the actual
|
||||
container. Native ID3 and MP4/freeform writers keep recording/release/group/
|
||||
release-track identities separate. An original year alone writes
|
||||
`ORIGINALDATE`, never a fabricated current-release date.
|
||||
the original-year tag (`ORIGINALDATE` or its native equivalent), never a
|
||||
fabricated current-release date. Preserve more precise existing dates in the
|
||||
same year. APEv2 MusicBrainz keys use underscores, not ID3's spaced descriptions.
|
||||
- Organization uses each file's `storage`: remote items use names and original
|
||||
torrent evidence, never local tags, duration, or CUE at an identical path.
|
||||
An `.m4a` suffix alone does not establish AAC/ALAC or lossless status.
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
from mutagen import File as MutagenFile
|
||||
from mutagen.apev2 import APEv2
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.id3 import ID3
|
||||
from mutagen.id3 import ID3, TDOR, TDRC
|
||||
from mutagen.mp4 import MP4
|
||||
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
@@ -268,14 +268,44 @@ def test_native_ape_tags_keep_standard_names_and_identity_namespaces(tmp_path):
|
||||
|
||||
path = tmp_path / "native-ape-tags"
|
||||
path.touch()
|
||||
info = MusicInfo(title="Song", album_artist="Artist", year=2020, track_number=2, total_tracks=8,
|
||||
info = MusicInfo(title="Song", album_artist="Artist", year=2020, original_year=2003, track_number=2, total_tracks=8,
|
||||
media_source="musicbrainz", media_id=_RECORDING, musicbrainz_release_id=_RELEASE,
|
||||
musicbrainz_release_group_id=_GROUP, musicbrainz_release_track_id=_TRACK)
|
||||
AudioMetadataHelper._write_tag_values(ApeTagFile(), info, True, path)
|
||||
tags = APEv2(path)
|
||||
assert str(tags["Album Artist"]) == "Artist" and str(tags["Track"]) == "2/8"
|
||||
assert str(tags["Originalyear"]) == "2003"
|
||||
assert "MusicBrainz Track Id" not in tags
|
||||
readable = AudioMetadataHelper._readable_tags(tags)
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_track_id") == [_RECORDING]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_album_id") == [_RELEASE]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_release_group_id") == [_GROUP]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_release_track_id") == [_TRACK]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_trackid") == [_RECORDING]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_albumid") == [_RELEASE]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_releasegroupid") == [_GROUP]
|
||||
assert AudioMetadataHelper._values(readable, "musicbrainz_releasetrackid") == [_TRACK]
|
||||
reloaded = ApeTagFile()
|
||||
reloaded.tags = tags
|
||||
assert not AudioMetadataHelper._needs_tag_write(reloaded, info, True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("container", ["wav", "dsf", "flac", "mp3", "m4a"])
|
||||
def test_writing_other_fields_preserves_more_precise_tag_dates(tmp_path, container):
|
||||
"""改曲名时不能把已有完整日期截断成年份;各容器都保留同年日期的精度。"""
|
||||
path = _audio_file(tmp_path, container)
|
||||
assert AudioMetadataHelper.write(path, MusicInfo(title="Old"))
|
||||
audio = MutagenFile(path)
|
||||
if isinstance(audio.tags, ID3):
|
||||
audio.tags.add(TDRC(encoding=3, text=["2020-06-01"]))
|
||||
audio.tags.add(TDOR(encoding=3, text=["2003-05-02"]))
|
||||
elif isinstance(audio, MP4):
|
||||
audio.tags["\xa9day"] = ["2020-06-01"]
|
||||
audio.tags["----:com.apple.iTunes:ORIGINALDATE"] = [b"2003-05-02"]
|
||||
else:
|
||||
audio["date"] = ["2020-06-01"]
|
||||
audio["originaldate"] = ["2003-05-02"]
|
||||
audio.save()
|
||||
|
||||
assert AudioMetadataHelper.write(path, MusicInfo(title="New", year=2020, release_year=2020, original_year=2003))
|
||||
|
||||
tags = AudioMetadataHelper._readable_tags(MutagenFile(path).tags)
|
||||
assert AudioMetadataHelper._values(tags, "title") == ["New"]
|
||||
assert AudioMetadataHelper._values(tags, "date") == ["2020-06-01"]
|
||||
assert AudioMetadataHelper._values(tags, "originaldate") == ["2003-05-02"]
|
||||
|
||||
Reference in New Issue
Block a user