mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(music): 恢复下载历史中缺失的已选音乐元数据
This commit is contained in:
@@ -256,6 +256,8 @@ class FileFilterMixin(_TransferOwnerBase):
|
||||
) -> tuple[Optional[MetaMusic], Optional[MusicInfo]]:
|
||||
"""从下载历史恢复音乐上下文,并用当前音频标签覆盖曲目级字段。
|
||||
|
||||
种子未提供的语义字段由历史中已选媒体补缺;实体类型和来源身份始终
|
||||
沿用已选媒体,不根据专辑名或文件曲名在单曲与专辑之间转换。
|
||||
多音轨批次误带单曲身份时只保留文件自身标签,避免把同一 recording
|
||||
身份传播到整张专辑;调用方随后可使用目录级证据重新匹配专辑。
|
||||
"""
|
||||
@@ -283,6 +285,22 @@ class FileFilterMixin(_TransferOwnerBase):
|
||||
return file_meta, None
|
||||
|
||||
file_meta = deepcopy(saved_meta)
|
||||
# 新旧历史都可能仅在 media 中保留已选专辑;先补缺,再沿用文件标签的
|
||||
# 覆盖规则。音质不从目标媒体补写,曲名仍取当前文件,避免混淆资源证据。
|
||||
for field_name in (
|
||||
"artists",
|
||||
"album",
|
||||
"album_artist",
|
||||
"year",
|
||||
"disc_number",
|
||||
"track_number",
|
||||
"total_tracks",
|
||||
"version",
|
||||
"isrc",
|
||||
):
|
||||
saved_value = getattr(saved_info, field_name, None)
|
||||
if getattr(file_meta, field_name, None) in (None, "", []) and saved_value not in (None, "", []):
|
||||
setattr(file_meta, field_name, deepcopy(saved_value))
|
||||
file_meta.org_string = file_path.name
|
||||
# 曲目标题始终优先使用当前文件自身的标签(缺失时回退为文件名),
|
||||
# 防止整包目录继续沿用订阅/下载标题(单曲名、专辑名等)导致所有文件重名。
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from jinja2 import Template
|
||||
|
||||
import app.chain.download.submission as download_submission
|
||||
from app.api.endpoints.download import add, download
|
||||
from app.application.audio import AudioMetadataHelper
|
||||
from app.application.messaging.message import TemplateHelper
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.transfer.facade import TransferChain
|
||||
from app.domain.context import MUSIC_ENTITY_ALBUM, Context, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.runtime.config import settings
|
||||
from app.schemas.context import TorrentInfo
|
||||
from app.schemas.mediaserver import ExistMediaInfo
|
||||
from app.schemas.music import MusicInfo as MusicInfoSchema
|
||||
@@ -174,6 +185,72 @@ def test_download_endpoint_builds_music_context():
|
||||
assert context.meta_info.media_id is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("music_type", ["recording", "album"])
|
||||
def test_download_history_restores_selected_music_album(music_type, monkeypatch):
|
||||
"""手动下载保留独立种子证据,历史恢复仍能为无标签音频补齐已选专辑。"""
|
||||
selected = _music_info()
|
||||
selected.music_type = music_type
|
||||
selected.album_artist = "周杰伦"
|
||||
if music_type == "album":
|
||||
selected.title = selected.album
|
||||
selected.media_id = "release-group-1"
|
||||
torrent_title = f"周杰伦 - {selected.title} FLAC"
|
||||
chain = Mock()
|
||||
chain.download_single.return_value = "hash-1"
|
||||
monkeypatch.setattr("app.api.endpoints.download.DownloadChain", lambda: chain)
|
||||
|
||||
response = download(
|
||||
media_in=MusicInfoSchema(**selected.to_dict()),
|
||||
torrent_in=TorrentInfo(
|
||||
title=torrent_title,
|
||||
enclosure="https://example.com/download?id=2",
|
||||
category="音乐",
|
||||
),
|
||||
downloader="qb",
|
||||
save_path=None,
|
||||
current_user=SimpleNamespace(name="admin"),
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
context = chain.download_single.call_args.kwargs["context"]
|
||||
assert context.meta_info.album is None
|
||||
assert context.meta_info.media_id is None
|
||||
assert context.meta_info.org_string == torrent_title
|
||||
assert context.media_info.music_type == music_type
|
||||
note = json.loads(json.dumps(DownloadChain._build_download_note(
|
||||
"Manual", context.media_info, context.meta_info,
|
||||
)))
|
||||
assert note["music"]["meta"]["album"] is None
|
||||
assert note["music"]["media"]["album"] == "叶惠美"
|
||||
assert note["music"]["media"]["music_type"] == music_type
|
||||
audio_path = Path("/03 - 晴天.flac")
|
||||
file_meta = AudioMetadataHelper.read_filename(audio_path)
|
||||
assert file_meta.album is None
|
||||
monkeypatch.setattr(MediaChain, "read_path_meta", Mock(return_value=file_meta))
|
||||
|
||||
restored_meta, restored_info = TransferChain._restore_music_download_context(
|
||||
SimpleNamespace(note=note), audio_path,
|
||||
)
|
||||
|
||||
assert restored_meta.album == restored_info.album == "叶惠美"
|
||||
assert restored_meta.year == restored_info.year == 2003
|
||||
assert restored_meta.title == restored_info.title == "晴天"
|
||||
assert restored_meta.track_number == restored_info.track_number == 3
|
||||
assert restored_info.music_type == music_type
|
||||
assert restored_info.media_source == selected.media_source
|
||||
assert restored_info.media_id == selected.media_id
|
||||
naming_context = TemplateHelper().builder.build(
|
||||
meta=restored_meta,
|
||||
mediainfo=restored_info,
|
||||
file_extension=".flac",
|
||||
include_raw_objects=False,
|
||||
)
|
||||
assert Template(settings.MUSIC_RENAME_FORMAT).render(naming_context) == (
|
||||
"周杰伦/叶惠美 (2003)/03 - 晴天.flac"
|
||||
)
|
||||
assert note["music"]["meta"]["album"] is None
|
||||
|
||||
|
||||
def test_manual_album_without_id_uses_subtitle_evidence():
|
||||
"""没有 ID 的人工专辑下载保留实体意图,并使用副标题中的真实艺人识别。"""
|
||||
media_chain = Mock()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from jinja2 import Template
|
||||
|
||||
from app.application.history import DownloadHistorySnapshot
|
||||
@@ -248,7 +250,8 @@ def test_restore_music_context_from_download_history():
|
||||
assert restored_info.album == "Random Access Memories"
|
||||
|
||||
|
||||
def test_restore_music_context_discards_shared_recording_identity(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize("file_album", ["Local Album", None])
|
||||
def test_restore_music_context_discards_shared_recording_identity(tmp_path, monkeypatch, file_album):
|
||||
"""多音轨批次不得把下载记录中的单曲身份恢复到每个音频文件。"""
|
||||
meta, info = _music_context()
|
||||
history = DownloadHistorySnapshot(
|
||||
@@ -270,9 +273,9 @@ def test_restore_music_context_discards_shared_recording_identity(tmp_path, monk
|
||||
org_string=audio_file.name,
|
||||
title="Give Life Back to Music",
|
||||
artists=["Daft Punk"],
|
||||
album="Local Album",
|
||||
album_artist="Daft Punk",
|
||||
year=2020,
|
||||
album=file_album,
|
||||
album_artist="Daft Punk" if file_album else None,
|
||||
year=2020 if file_album else None,
|
||||
track_number=1,
|
||||
)
|
||||
monkeypatch.setattr(MediaChain, "read_path_meta", Mock(return_value=file_meta))
|
||||
@@ -285,8 +288,8 @@ def test_restore_music_context_discards_shared_recording_identity(tmp_path, monk
|
||||
|
||||
assert restored_meta is not None
|
||||
assert restored_meta.title == "Give Life Back to Music"
|
||||
assert restored_meta.album == "Local Album"
|
||||
assert restored_meta.year == 2020
|
||||
assert restored_meta.album == file_meta.album
|
||||
assert restored_meta.year == file_meta.year
|
||||
assert restored_meta.media_source is None
|
||||
assert restored_meta.media_id is None
|
||||
assert restored_info is None
|
||||
@@ -307,7 +310,10 @@ def test_download_history_music_type_falls_back_to_versioned_note():
|
||||
assert TransferChain._download_history_music_type(history) == "album"
|
||||
|
||||
|
||||
def test_restore_album_context_keeps_album_identity_and_track_specific_tags(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize("resource_meta_only", [False, True])
|
||||
def test_restore_album_context_keeps_album_identity_and_track_specific_tags(
|
||||
tmp_path, monkeypatch, resource_meta_only,
|
||||
):
|
||||
"""整专整理应保留选中的专辑身份,同时使用每个文件自己的曲名、艺术家和曲序。"""
|
||||
album = MusicInfo(
|
||||
media_source="musicbrainz",
|
||||
@@ -320,7 +326,10 @@ def test_restore_album_context_keeps_album_identity_and_track_specific_tags(tmp_
|
||||
year=2003,
|
||||
total_tracks=11,
|
||||
)
|
||||
meta = MetaMusic.from_music_info(album)
|
||||
meta = (
|
||||
MetaMusic.parse_resource("周杰伦 - 叶惠美 FLAC")
|
||||
if resource_meta_only else MetaMusic.from_music_info(album)
|
||||
)
|
||||
history = SimpleNamespace(note={
|
||||
"music": {
|
||||
"version": 1,
|
||||
@@ -348,7 +357,9 @@ def test_restore_album_context_keeps_album_identity_and_track_specific_tags(tmp_
|
||||
),
|
||||
)
|
||||
|
||||
restored_meta, restored_info = TransferChain._restore_music_download_context(history, audio_file)
|
||||
restored_meta, restored_info = TransferChain._restore_music_download_context(
|
||||
history, audio_file, discard_recording_identity=True,
|
||||
)
|
||||
|
||||
assert restored_meta.title == "晴天"
|
||||
assert restored_meta.track_number == 3
|
||||
@@ -360,6 +371,62 @@ def test_restore_album_context_keeps_album_identity_and_track_specific_tags(tmp_
|
||||
assert restored_info.media_id == "release-group-1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_source", ["selected", "resource", "file"])
|
||||
def test_restore_music_context_only_fills_missing_selected_fields(monkeypatch, field_source):
|
||||
"""已选语义字段仅补缺,不覆盖种子证据、文件标签或注入目标音质。"""
|
||||
saved_meta, info = _music_context()
|
||||
info.disc_number = 1
|
||||
info.version = "Studio"
|
||||
info.isrc = "USQX91300108"
|
||||
info.audio_format = "MP3"
|
||||
info.bit_depth = 16
|
||||
if field_source == "selected":
|
||||
saved_meta = MetaMusic()
|
||||
else:
|
||||
saved_meta.album = "Resource Album"
|
||||
saved_meta.album_artist = "Resource Artist"
|
||||
saved_meta.year = 2014
|
||||
saved_meta.total_tracks = 15
|
||||
saved_meta.disc_number = 2
|
||||
saved_meta.version = "Live"
|
||||
saved_meta.isrc = "USQX91400108"
|
||||
note = {"music": {"version": 1, "meta": saved_meta.to_dict(), "media": info.to_dict()}}
|
||||
original_note = deepcopy(note)
|
||||
file_meta = MetaMusic(title="File Title")
|
||||
if field_source == "file":
|
||||
file_meta.artists = ["File Artist"]
|
||||
file_meta.album = "File Album"
|
||||
file_meta.album_artist = "File Album Artist"
|
||||
file_meta.year = 2020
|
||||
file_meta.total_tracks = 20
|
||||
file_meta.disc_number = 3
|
||||
file_meta.track_number = 10
|
||||
file_meta.version = "Remix"
|
||||
file_meta.isrc = "USQX92000108"
|
||||
monkeypatch.setattr(MediaChain, "read_path_meta", Mock(return_value=file_meta))
|
||||
|
||||
restored_meta, restored_info = TransferChain._restore_music_download_context(
|
||||
SimpleNamespace(note=note), Path("/downloads/03 - File Title.flac"),
|
||||
)
|
||||
|
||||
expected = {"selected": info, "resource": saved_meta, "file": file_meta}[field_source]
|
||||
for field_name in (
|
||||
"artists", "album", "album_artist", "year", "total_tracks",
|
||||
"disc_number", "track_number", "version", "isrc",
|
||||
):
|
||||
assert getattr(restored_meta, field_name) == getattr(expected, field_name)
|
||||
assert getattr(restored_info, field_name) == getattr(expected, field_name)
|
||||
assert restored_meta.title == "File Title"
|
||||
assert restored_info.music_type == "recording"
|
||||
assert restored_info.media_source == info.media_source
|
||||
assert restored_info.media_id == info.media_id
|
||||
assert restored_meta.audio_format is None
|
||||
assert restored_meta.bit_depth is None
|
||||
assert note == original_note
|
||||
restored_meta.artists.append("Another Artist")
|
||||
assert file_meta.artists == (["File Artist"] if field_source == "file" else [])
|
||||
|
||||
|
||||
def test_restore_music_context_uses_file_title_over_subscription_title(tmp_path, monkeypatch):
|
||||
"""曲目标题应优先取当前文件自身的标签/文件名,而非沿用订阅时的单曲标题。"""
|
||||
meta, info = _music_context()
|
||||
|
||||
Reference in New Issue
Block a user