Fix 修复音乐整理

This commit is contained in:
jxxghp
2026-08-09 14:32:25 +08:00
parent 29f1eb4313
commit c312c5c0e6
3 changed files with 152 additions and 47 deletions

View File

@@ -1080,39 +1080,44 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
except (TypeError, ValueError):
return None, None
file_tags = AudioMetadataHelper.read(file_path) if file_path.exists() else None
file_tags = (
AudioMetadataHelper.read(file_path)
if file_path.exists()
else MetaMusic(
org_string=file_path.name,
title=file_path.stem,
audio_format=file_path.suffix.lstrip(".").upper() or None,
)
)
file_meta = deepcopy(saved_meta)
file_meta.org_string = file_path.name
if file_tags:
# 曲目标题始终优先使用当前文件自身的标签(缺失时回退为文件名),
# 防止整包目录继续沿用订阅/下载标题(单曲名、专辑名等)导致所有文件重名。
if file_tags.title:
file_meta.title = file_tags.title
for field_name in (
"artists",
"album",
"album_artist",
"year",
"disc_number",
"track_number",
"total_discs",
"total_tracks",
"version",
"isrc",
):
if getattr(file_tags, field_name, None):
setattr(file_meta, field_name, deepcopy(getattr(file_tags, field_name)))
for field_name in (
"audio_format",
"bit_depth",
"sample_rate",
"bitrate",
"duration",
):
if getattr(file_tags, field_name, None):
setattr(file_meta, field_name, getattr(file_tags, field_name))
elif not file_meta.audio_format:
file_meta.audio_format = file_path.suffix.lstrip(".").upper() or None
# 曲目标题始终优先使用当前文件自身的标签(缺失时回退为文件名),
# 防止整包目录继续沿用订阅/下载标题(单曲名、专辑名等)导致所有文件重名。
if file_tags.title:
file_meta.title = file_tags.title
for field_name in (
"artists",
"album",
"album_artist",
"year",
"disc_number",
"track_number",
"total_discs",
"total_tracks",
"version",
"isrc",
):
if getattr(file_tags, field_name, None):
setattr(file_meta, field_name, deepcopy(getattr(file_tags, field_name)))
for field_name in (
"audio_format",
"bit_depth",
"sample_rate",
"bitrate",
"duration",
):
if getattr(file_tags, field_name, None):
setattr(file_meta, field_name, getattr(file_tags, field_name))
file_meta.media_source = saved_info.source or saved_meta.media_source
file_meta.media_id = saved_info.media_id or saved_meta.media_id

View File

@@ -87,28 +87,42 @@ class TemplateContextBuilder:
将 MediaInfo 中的标题、季年份、海报等业务字段就地写入 ``context``。
会读取 ``context`` 中由 ``_add_episode_details`` 先填好的 ``season`` /
``year`` / ``title_year`` 占位,保证电视剧场景下季/年优先沿用 meta 解析值
``year`` / ``title_year`` 占位,保证电视剧场景下季/年优先沿用 meta 解析值
音乐场景保留文件标签解析出的曲目级字段,仅用识别结果补齐专辑级字段。
"""
if not mediainfo:
return
if isinstance(mediainfo, MusicInfo):
# 专辑识别结果会被同一批次的所有曲目复用,不能覆盖每个文件自己的曲名和曲序。
title = context.get("title") or cls.__convert_invalid_characters(mediainfo.title)
artists = context.get("artists") or [
cls.__convert_invalid_characters(item) for item in mediainfo.artists
]
artist = context.get("artist") or cls.__convert_invalid_characters(mediainfo.artist)
album = context.get("album") or cls.__convert_invalid_characters(mediainfo.album)
album_artist = context.get("album_artist") or cls.__convert_invalid_characters(
mediainfo.album_artist
)
year = context.get("year") or mediainfo.year
disc_number = context.get("disc_number") or mediainfo.disc_number
track_number = context.get("track_number") or mediainfo.track_number
context.update({
"type": mediainfo.type.value,
"title": cls.__convert_invalid_characters(mediainfo.title),
"name": cls.__convert_invalid_characters(mediainfo.title),
"artists": [cls.__convert_invalid_characters(item) for item in mediainfo.artists],
"artist": cls.__convert_invalid_characters(mediainfo.artist),
"album": cls.__convert_invalid_characters(mediainfo.album),
"album_artist": cls.__convert_invalid_characters(mediainfo.album_artist),
"year": mediainfo.year or context.get("year"),
"title_year": mediainfo.title_year or context.get("title_year"),
"disc_number": mediainfo.disc_number,
"track_number": mediainfo.track_number,
"track": f"{mediainfo.track_number:02d}" if mediainfo.track_number else None,
"total_tracks": mediainfo.total_tracks,
"duration": mediainfo.duration,
"isrc": mediainfo.isrc,
"version": mediainfo.version,
"title": title,
"name": context.get("name") or title,
"artists": artists,
"artist": artist,
"album": album,
"album_artist": album_artist,
"year": year,
"title_year": f"{title} ({year})" if title and year else title,
"disc_number": disc_number,
"track_number": track_number,
"track": f"{track_number:02d}" if track_number else None,
"total_tracks": context.get("total_tracks") or mediainfo.total_tracks,
"duration": context.get("duration") or mediainfo.duration,
"isrc": context.get("isrc") or mediainfo.isrc,
"version": context.get("version") or mediainfo.version,
"category": mediainfo.category,
"poster": mediainfo.get_poster_image(),
"backdrop": mediainfo.get_backdrop_image(),

View File

@@ -1,6 +1,8 @@
from pathlib import Path
from types import SimpleNamespace
from jinja2 import Template
from app.chain.music import MusicChain
from app.chain.transfer import JobManager, TransferChain
from app.core.config import settings
@@ -46,6 +48,43 @@ def test_music_rename_context_contains_audio_fields():
assert context["fileExt"] == ".flac"
def test_music_rename_prefers_track_meta_over_album_media():
"""专辑整理时应使用每个文件的曲名和曲序,不能把专辑名写成所有目标文件名。"""
meta = MetaMusic(
org_string="10. 明天晴天.m4a",
title="明天晴天",
artists=["孙燕姿"],
album="完美的一天",
album_artist="孙燕姿",
year=2005,
track_number=10,
total_tracks=11,
)
album = MusicInfo(
source="musicbrainz",
media_id="album-1",
music_type="album",
title="完美的一天",
artists=["孙燕姿"],
album="完美的一天",
album_artist="孙燕姿",
year=2005,
total_tracks=11,
)
context = TemplateHelper().builder.build(
meta=meta,
mediainfo=album,
file_extension=".m4a",
include_raw_objects=False,
)
rendered = Template(settings.MUSIC_RENAME_FORMAT).render(context)
assert context["title"] == "明天晴天"
assert context["track"] == "10"
assert rendered == "孙燕姿/完美的一天 (2005)/10 - 明天晴天.m4a"
def test_music_rename_format_is_independent_from_movie_format():
"""音乐应使用独立重命名模板且保持影视模板不变。"""
assert settings.RENAME_FORMAT(MediaType.MUSIC) == settings.MUSIC_RENAME_FORMAT
@@ -124,6 +163,53 @@ def test_restore_music_context_uses_file_title_over_subscription_title(tmp_path,
assert restored_meta.title == "流浪地图"
def test_restore_music_context_uses_filename_when_source_is_not_locally_accessible():
"""远端音频无法直接读取标签时也应按文件名区分曲目,避免整张专辑重名。"""
meta, info = _music_context()
meta.artists = ["孙燕姿"]
meta.title = "完美的一天"
meta.album = "完美的一天"
meta.album_artist = "孙燕姿"
meta.year = 2005
meta.track_number = None
meta.total_tracks = None
info.music_type = "album"
info.artists = ["孙燕姿"]
info.title = "完美的一天"
info.album = "完美的一天"
info.album_artist = "孙燕姿"
info.year = 2005
info.track_number = None
info.total_tracks = None
history = SimpleNamespace(
note={
"music": {
"version": 1,
"meta": meta.to_dict(),
"media": info.to_dict(),
}
}
)
restored_meta, restored_info = TransferChain._restore_music_download_context(
history,
Path("/remote/10. 明天晴天.m4a"),
)
assert restored_meta is not None
assert restored_info is not None
assert restored_meta.title == "10. 明天晴天"
assert restored_info.title == "10. 明天晴天"
context = TemplateHelper().builder.build(
meta=restored_meta,
mediainfo=restored_info,
file_extension=".m4a",
include_raw_objects=False,
)
rendered = Template(settings.MUSIC_RENAME_FORMAT).render(context)
assert rendered == "孙燕姿/完美的一天 (2005)/10. 明天晴天.m4a"
def test_job_manager_serializes_music_queue_models():
"""整理队列应使用音乐专属 Schema 序列化任务。"""
meta, info = _music_context()