mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-14 10:14:36 +08:00
Fix 修复音乐整理
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user