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
+36 -31
View File
@@ -1080,39 +1080,44 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
except (TypeError, ValueError): except (TypeError, ValueError):
return None, None 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 = deepcopy(saved_meta)
file_meta.org_string = file_path.name file_meta.org_string = file_path.name
if file_tags: # 曲目标题始终优先使用当前文件自身的标签(缺失时回退为文件名),
# 曲目标题始终优先使用当前文件自身的标签(缺失时回退为文件名), # 防止整包目录继续沿用订阅/下载标题(单曲名、专辑名等)导致所有文件重名。
# 防止整包目录继续沿用订阅/下载标题(单曲名、专辑名等)导致所有文件重名。 if file_tags.title:
if file_tags.title: file_meta.title = file_tags.title
file_meta.title = file_tags.title for field_name in (
for field_name in ( "artists",
"artists", "album",
"album", "album_artist",
"album_artist", "year",
"year", "disc_number",
"disc_number", "track_number",
"track_number", "total_discs",
"total_discs", "total_tracks",
"total_tracks", "version",
"version", "isrc",
"isrc", ):
): if getattr(file_tags, field_name, None):
if getattr(file_tags, field_name, None): setattr(file_meta, field_name, deepcopy(getattr(file_tags, field_name)))
setattr(file_meta, field_name, deepcopy(getattr(file_tags, field_name))) for field_name in (
for field_name in ( "audio_format",
"audio_format", "bit_depth",
"bit_depth", "sample_rate",
"sample_rate", "bitrate",
"bitrate", "duration",
"duration", ):
): if getattr(file_tags, field_name, None):
if getattr(file_tags, field_name, None): setattr(file_meta, field_name, getattr(file_tags, field_name))
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
file_meta.media_source = saved_info.source or saved_meta.media_source 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 file_meta.media_id = saved_info.media_id or saved_meta.media_id
+30 -16
View File
@@ -87,28 +87,42 @@ class TemplateContextBuilder:
将 MediaInfo 中的标题、季年份、海报等业务字段就地写入 ``context``。 将 MediaInfo 中的标题、季年份、海报等业务字段就地写入 ``context``。
会读取 ``context`` 中由 ``_add_episode_details`` 先填好的 ``season`` / 会读取 ``context`` 中由 ``_add_episode_details`` 先填好的 ``season`` /
``year`` / ``title_year`` 占位,保证电视剧场景下季/年优先沿用 meta 解析值 ``year`` / ``title_year`` 占位,保证电视剧场景下季/年优先沿用 meta 解析值
音乐场景保留文件标签解析出的曲目级字段,仅用识别结果补齐专辑级字段。
""" """
if not mediainfo: if not mediainfo:
return return
if isinstance(mediainfo, MusicInfo): 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({ context.update({
"type": mediainfo.type.value, "type": mediainfo.type.value,
"title": cls.__convert_invalid_characters(mediainfo.title), "title": title,
"name": cls.__convert_invalid_characters(mediainfo.title), "name": context.get("name") or title,
"artists": [cls.__convert_invalid_characters(item) for item in mediainfo.artists], "artists": artists,
"artist": cls.__convert_invalid_characters(mediainfo.artist), "artist": artist,
"album": cls.__convert_invalid_characters(mediainfo.album), "album": album,
"album_artist": cls.__convert_invalid_characters(mediainfo.album_artist), "album_artist": album_artist,
"year": mediainfo.year or context.get("year"), "year": year,
"title_year": mediainfo.title_year or context.get("title_year"), "title_year": f"{title} ({year})" if title and year else title,
"disc_number": mediainfo.disc_number, "disc_number": disc_number,
"track_number": mediainfo.track_number, "track_number": track_number,
"track": f"{mediainfo.track_number:02d}" if mediainfo.track_number else None, "track": f"{track_number:02d}" if track_number else None,
"total_tracks": mediainfo.total_tracks, "total_tracks": context.get("total_tracks") or mediainfo.total_tracks,
"duration": mediainfo.duration, "duration": context.get("duration") or mediainfo.duration,
"isrc": mediainfo.isrc, "isrc": context.get("isrc") or mediainfo.isrc,
"version": mediainfo.version, "version": context.get("version") or mediainfo.version,
"category": mediainfo.category, "category": mediainfo.category,
"poster": mediainfo.get_poster_image(), "poster": mediainfo.get_poster_image(),
"backdrop": mediainfo.get_backdrop_image(), "backdrop": mediainfo.get_backdrop_image(),
+86
View File
@@ -1,6 +1,8 @@
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from jinja2 import Template
from app.chain.music import MusicChain from app.chain.music import MusicChain
from app.chain.transfer import JobManager, TransferChain from app.chain.transfer import JobManager, TransferChain
from app.core.config import settings from app.core.config import settings
@@ -46,6 +48,43 @@ def test_music_rename_context_contains_audio_fields():
assert context["fileExt"] == ".flac" 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(): def test_music_rename_format_is_independent_from_movie_format():
"""音乐应使用独立重命名模板且保持影视模板不变。""" """音乐应使用独立重命名模板且保持影视模板不变。"""
assert settings.RENAME_FORMAT(MediaType.MUSIC) == settings.MUSIC_RENAME_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 == "流浪地图" 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(): def test_job_manager_serializes_music_queue_models():
"""整理队列应使用音乐专属 Schema 序列化任务。""" """整理队列应使用音乐专属 Schema 序列化任务。"""
meta, info = _music_context() meta, info = _music_context()