mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 05:17:23 +08:00
fix(music): apply simplified metadata to manual transfers
Normalize album details and final music template fields while preserving local edition evidence, multi-artist credits and source objects. Refs #6870.
This commit is contained in:
@@ -15,13 +15,14 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Protocol, Union
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.application.configuration import get_chain_runtime_config_snapshot, get_configured_system_config
|
||||
from app.domain.context import MediaInfo, MusicInfo, TorrentInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.foundation import size as size_tools
|
||||
from app.foundation.crypto import HashUtils
|
||||
from app.foundation.singleton import Singleton, SingletonClass
|
||||
from app.foundation.text import convert as zhconv_convert
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.stop import runtime_stop_state
|
||||
@@ -129,6 +130,7 @@ class TemplateContextBuilder:
|
||||
|
||||
每次调用都新建本地 ``context`` 字典,依次填充各业务来源和当前实例的
|
||||
主机名后返回过滤掉 None 值的副本,调用之间互不影响。
|
||||
音乐在字段取值完成后统一按开关转简体,避免原始标签覆盖在线转换结果。
|
||||
|
||||
:param meta: 媒体元数据
|
||||
:param mediainfo: 识别的媒体信息
|
||||
@@ -148,6 +150,8 @@ class TemplateContextBuilder:
|
||||
self._add_transfer_info(context, transferinfo)
|
||||
self._add_torrent_info(context, torrentinfo)
|
||||
self._add_file_info(context, file_extension)
|
||||
if isinstance(meta, MetaMusic) or isinstance(mediainfo, MusicInfo):
|
||||
self._simplify_music_context(context)
|
||||
context.update(kwargs, instance_name=socket.gethostname())
|
||||
|
||||
if include_raw_objects:
|
||||
@@ -156,6 +160,18 @@ class TemplateContextBuilder:
|
||||
# 移除空值
|
||||
return {k: v for k, v in context.items() if v is not None}
|
||||
|
||||
@staticmethod
|
||||
def _simplify_music_context(context: Dict[str, Any]) -> None:
|
||||
"""只转换最终音乐展示字段,保留原始文件名、标签、媒体身份和取值优先级。"""
|
||||
if not get_chain_runtime_config_snapshot().music_metadata_to_simplified:
|
||||
return
|
||||
for field_name in ("title", "name", "artist", "album", "album_artist", "title_year", "version"):
|
||||
value = context.get(field_name)
|
||||
if isinstance(value, str):
|
||||
context[field_name] = zhconv_convert(value, "zh-hans")
|
||||
if "artists" in context:
|
||||
context["artists"] = [zhconv_convert(artist, "zh-hans") for artist in context["artists"]]
|
||||
|
||||
@classmethod
|
||||
def _add_media_info(
|
||||
cls,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""音乐来源、目录搜索与详情路由 owner。"""
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any, Iterable, Optional, cast
|
||||
from typing import Any, Iterable, Optional, Union, cast
|
||||
|
||||
from app.application.configuration import get_chain_runtime_config_snapshot
|
||||
from app.application.music.catalog import MusicCatalogService
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.media.contract import _MediaOwnerBase
|
||||
from app.chain.media.contract import _MediaOwnerBase, _MusicMetadataT
|
||||
from app.chain.musicbrainz import MusicBrainzChain, MusicMetadataSourceChain
|
||||
from app.chain.theaudiodb import TheAudioDbChain
|
||||
from app.domain.context import (
|
||||
@@ -38,7 +38,7 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
@classmethod
|
||||
def _music_artist_aliases(
|
||||
cls,
|
||||
info: MusicInfo,
|
||||
info: Union[MusicInfo, MusicAlbumInfo],
|
||||
artist_index: int,
|
||||
) -> list[str]:
|
||||
"""按艺术家身份提取别名,避免多位艺术家的别名相互串用。"""
|
||||
@@ -96,7 +96,7 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
return str(zhconv_convert(name, "zh-hans"))
|
||||
|
||||
@classmethod
|
||||
def _simplify_music_artist_names(cls, info: MusicInfo) -> list[str]:
|
||||
def _simplify_music_artist_names(cls, info: Union[MusicInfo, MusicAlbumInfo]) -> list[str]:
|
||||
"""逐位转换艺术家名称,保留多艺术家结果的身份边界。"""
|
||||
return [
|
||||
cls._simplify_music_artist_name(
|
||||
@@ -238,8 +238,8 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
return cls._simplify_recognized_music_info(result)
|
||||
|
||||
@classmethod
|
||||
def _simplify_recognized_music_info(cls, info: MusicInfo) -> MusicInfo:
|
||||
"""按开关转换标准音乐文本字段,并优先使用可信中文艺术家别名。"""
|
||||
def _simplify_recognized_music_info(cls, info: _MusicMetadataT) -> _MusicMetadataT:
|
||||
"""按开关转换音乐及专辑曲目文本,保留来源缓存、原文别名和身份。"""
|
||||
if not get_chain_runtime_config_snapshot().music_metadata_to_simplified:
|
||||
return info
|
||||
updates: dict[str, Any] = {}
|
||||
@@ -264,9 +264,14 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
)
|
||||
if converted_items != value:
|
||||
updates[field_name] = converted_items
|
||||
if isinstance(info, MusicAlbumInfo):
|
||||
tracks = [cls._simplify_recognized_music_info(track) for track in info.tracks]
|
||||
if any(converted is not original for converted, original in zip(tracks, info.tracks)):
|
||||
updates["tracks"] = tracks
|
||||
if not updates:
|
||||
return info
|
||||
simplified = deepcopy(info)
|
||||
simplified: _MusicMetadataT = deepcopy(info)
|
||||
alias_field: str
|
||||
for field_name, alias_field in (
|
||||
("title", "title_aliases"),
|
||||
("album", "album_aliases"),
|
||||
@@ -279,7 +284,7 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
if field_name in ("album_artist", "names") and info.music_type != MUSIC_ENTITY_ALBUM:
|
||||
continue
|
||||
original = getattr(info, field_name)
|
||||
originals = original if isinstance(original, list) else [original]
|
||||
originals: list[Any] = original if isinstance(original, list) else [original]
|
||||
setattr(simplified, alias_field, list(dict.fromkeys([
|
||||
*(getattr(simplified, alias_field, None) or []), *originals,
|
||||
])))
|
||||
@@ -372,7 +377,7 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
music_release_scripts: Optional[list[str]] = None,
|
||||
musicbrainz_release_id: Optional[str] = None,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""按音乐来源和原生 ID 同步获取专辑详情。"""
|
||||
"""按音乐来源和原生 ID 同步获取专辑详情,按开关转换专辑及曲目文本。"""
|
||||
source, normalized_id = resolve_media_identity(media_source=media_source, media_id=media_id)
|
||||
if not source or not normalized_id:
|
||||
return None
|
||||
@@ -387,6 +392,8 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
if music_release_scripts is not None:
|
||||
preference_kwargs["music_release_scripts"] = music_release_scripts
|
||||
result = chain.get_music_album(normalized_id, **preference_kwargs) if chain else None
|
||||
if result:
|
||||
result = self._simplify_recognized_music_info(result)
|
||||
return cast(
|
||||
Optional[MusicAlbumInfo],
|
||||
self._finalize_recognition_result(result),
|
||||
@@ -400,7 +407,7 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
music_release_scripts: Optional[list[str]] = None,
|
||||
musicbrainz_release_id: Optional[str] = None,
|
||||
) -> Optional[MusicAlbumInfo]:
|
||||
"""按音乐来源和原生 ID 异步获取专辑详情。"""
|
||||
"""按音乐来源和原生 ID 异步获取专辑详情,按开关转换专辑及曲目文本。"""
|
||||
source, normalized_id = resolve_media_identity(media_source=media_source, media_id=media_id)
|
||||
if not source or not normalized_id:
|
||||
return None
|
||||
@@ -415,6 +422,8 @@ class MediaCatalogOwner(_MediaOwnerBase):
|
||||
if music_release_scripts is not None:
|
||||
preference_kwargs["music_release_scripts"] = music_release_scripts
|
||||
result = await chain.async_get_music_album(normalized_id, **preference_kwargs) if chain else None
|
||||
if result:
|
||||
result = self._simplify_recognized_music_info(result)
|
||||
return cast(
|
||||
Optional[MusicAlbumInfo],
|
||||
await self._async_finalize_recognition_result(result),
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.schemas.types import (
|
||||
|
||||
_RecognitionCallback = Callable[[], Optional[MediaInfo]]
|
||||
_AsyncRecognitionCallback = Callable[[], Awaitable[Optional[MediaInfo]]]
|
||||
_MusicMetadataT = TypeVar("_MusicMetadataT", MusicInfo, MusicAlbumInfo)
|
||||
_ClassificationSubjectT = TypeVar(
|
||||
"_ClassificationSubjectT",
|
||||
MediaInfo,
|
||||
@@ -159,8 +160,8 @@ if TYPE_CHECKING:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def _simplify_recognized_music_info(cls, info: MusicInfo) -> MusicInfo:
|
||||
"""按配置返回简体化音乐信息。"""
|
||||
def _simplify_recognized_music_info(cls, info: _MusicMetadataT) -> _MusicMetadataT:
|
||||
"""按配置返回简体化音乐信息,专辑详情包含逐曲转换。"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -567,6 +567,12 @@ AniList 榜单、探索、详情、人物和推荐接口优先通过 `anilist-ch
|
||||
标题、副标题的实际解析,不用目标媒体回填证据;`title_aliases`、`album_aliases`、
|
||||
`artist_aliases` 分别保留同一实体的可信别名及展示转简体前的原文。
|
||||
|
||||
`MUSIC_METADATA_TO_SIMPLIFIED` 同样适用于同步/异步专辑详情及其逐曲文本,
|
||||
也适用于音乐整理最终命名:完整本地标签、手选专辑发行版和手选录音均按开关转换。
|
||||
转换不改写原始音频标签、来源缓存、歌词或媒体身份;关闭开关保留原文。
|
||||
录音身份不等同于专辑发行身份,单曲仍保留已有的本地专辑字段优先级;
|
||||
多艺人署名完整保留,不自动截取第一位艺人。
|
||||
|
||||
音乐元数据、单曲和专辑模型还分别保留 `composers`、`conductors`、`orchestras` 人名列表,
|
||||
以及 `performers` 乐器/声部到人名列表的映射(未指定乐器使用 `performer` 键)。
|
||||
这些字段不会混入主艺人或艺人别名;空字段在简化卡片中省略,完整模型与旧缓存兼容。
|
||||
|
||||
@@ -66,6 +66,7 @@ to make the directory tree look symmetrical.
|
||||
| `app/application/download/` | Download task querying/control and selection use cases; `failures.py` owns the frozen failure-cooldown write/query DTOs and persistence Port |
|
||||
| `app/application/history/` | History use cases and persistence contracts; DownloadHistory and TransferHistory own deeply frozen DTOs plus typed query/write/staging ports |
|
||||
| `app/application/music/` | 多来源音乐目录编排;`observation.py` 沿用站点搜索的调用观察模式,隔离一次音乐识别的结果、候选摘要与请求/等待预算,不持有来源客户端、不改变旧模块返回合同;`recognition.py` 通过注入来源回调串行回退、汇总诊断,并按领域候选计划有界补充真实Artist身份别名,子来源共享祖先预算,`catalog.py` 声明目录查询所需的最小来源 Port |
|
||||
| `app/application/messaging/message.py` | 模板上下文按既有来源优先级选定音乐字段后,使用配置快照和 `app.foundation.text.convert` 应用简体开关;不调用识别 Chain、不改写标签或来源缓存,也不替换录音所属发行和多艺人署名 |
|
||||
| `app/application/chain/` | Injectable Chain runtime capabilities: `context.py` owns the typed runtime and persistence dependency aggregate, and `events.py` owns durable event write contracts plus replayable payload conversion |
|
||||
| `app/application/agent.py` | Agent orchestration facade and typed `AgentDataContext`; startup injects one explicit data context into the manager, memory, tool and scheduler owners without a process-wide persistence locator |
|
||||
| `app/application/invocation.py` | Frozen Agent write-call identity, claim and receipt contracts; the injected repository provides atomic claim, fenced settlement and unresolved-state reads, while `db/adapters/invocation.py` owns short transactions and cold-start recovery is invoked by startup |
|
||||
|
||||
@@ -107,6 +107,10 @@ paths. Cross-share moves copy on the server before deleting the source and are
|
||||
not atomic; cross-share hard links are unsupported. Update saved directory and
|
||||
downloader mappings when switching path modes.
|
||||
|
||||
For music metadata and transfer naming, follow the simplification and album-edition
|
||||
precedence contract in [api/music.md](api/music.md); text conversion does not change
|
||||
source identities or reduce multi-artist credits to a single artist.
|
||||
|
||||
## Overall Workflow
|
||||
|
||||
1. Select the exact `operation_id` from the category index below.
|
||||
|
||||
@@ -188,6 +188,7 @@ online confirmation; album corrections must preserve the returned source-file sc
|
||||
### `music.album.get`
|
||||
`GET /api/v1/music/album/{album_id}`; policy effect: `safe_read`.
|
||||
Purpose: Read one album's details, tracks, releases, and aligned artist names and IDs.
|
||||
- Album and track text follows `MUSIC_METADATA_TO_SIMPLIFIED`, matching recognition and final transfer naming. Original aliases, source data, lyrics, and IDs remain unchanged. Recording selection does not override local album-edition evidence, and multi-artist credits remain complete.
|
||||
- `path_params`: `album_id*` (string): Source-native album ID returned by music search, exploration, or artist-album browsing.
|
||||
- `query`: `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `musicbrainz_release_id` (string|null): Optional exact MusicBrainz Release UUID belonging to the selected album Release Group; never use it as media_id.
|
||||
- `body`: none
|
||||
|
||||
@@ -941,8 +941,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 9299,
|
||||
"edge_sha256": "c87fbb377412559c30d21b4c44c2906085143fa58a9e645855348d72f0b056a6",
|
||||
"edge_count": 9300,
|
||||
"edge_sha256": "8f47b7d4f63b473306a741bd791077c4d88986061a3f182ded3ab7389c709d2d",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -3717,6 +3717,7 @@
|
||||
"app.application.messaging.message -> app.foundation.crypto",
|
||||
"app.application.messaging.message -> app.foundation.singleton",
|
||||
"app.application.messaging.message -> app.foundation.size",
|
||||
"app.application.messaging.message -> app.foundation.text",
|
||||
"app.application.messaging.message -> app.runtime",
|
||||
"app.application.messaging.message -> app.runtime.cache",
|
||||
"app.application.messaging.message -> app.runtime.log",
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.chain.media import MediaChain
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.music import music_tags_are_usable
|
||||
from app.modules.filemanager.transhandler import TransHandler
|
||||
from app.runtime.config import ConfigModel
|
||||
from app.runtime.config import ConfigModel, settings
|
||||
from app.schemas.category import CategoryConfig, ClassificationPolicy
|
||||
from app.schemas.file import FileItem
|
||||
from app.schemas.system import TransferDirectoryConf
|
||||
@@ -33,8 +33,8 @@ def _tagged_files(directory: Path) -> list[Path]:
|
||||
shutil.copyfile(Path(__file__).parent / "fixtures/audio/silence.flac", path)
|
||||
audio = FLAC(path)
|
||||
audio.update({
|
||||
"title": [title], "artist": ["周杰伦"], "album": ["叶惠美"],
|
||||
"albumartist": ["周杰伦"], "date": ["2003"],
|
||||
"title": [title], "artist": ["周杰倫"], "album": ["葉惠美"],
|
||||
"albumartist": ["周杰倫"], "date": ["2003"],
|
||||
"tracknumber": ["1"], "tracktotal": ["1"],
|
||||
"discnumber": [str(disc)], "disctotal": ["2"],
|
||||
})
|
||||
@@ -57,8 +57,10 @@ def _classification_service():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mtype", [None, MediaType.MUSIC])
|
||||
def test_complete_tags_preview_offline_and_preserve_custom_category(tmp_path, monkeypatch, mtype):
|
||||
"""自动与显式音乐批次均以真实标签完成目标命名,双碟不碰撞且源内容不变。"""
|
||||
@pytest.mark.parametrize("simplified", [False, True])
|
||||
def test_complete_tags_preview_offline_and_preserve_custom_category(tmp_path, monkeypatch, mtype, simplified):
|
||||
"""真实标签离线整理遵守简体开关和自定义分类,双碟不碰撞且源内容不变。"""
|
||||
monkeypatch.setattr(settings, "MUSIC_METADATA_TO_SIMPLIFIED", simplified)
|
||||
source = tmp_path / "2022-wrong-name"
|
||||
paths = _tagged_files(source)
|
||||
hashes = [hashlib.sha256(path.read_bytes()).digest() for path in paths]
|
||||
@@ -114,9 +116,10 @@ def test_complete_tags_preview_offline_and_preserve_custom_category(tmp_path, mo
|
||||
assert all(not item.music.online_confirmed and item.source_storage == "local" for item in projected.items)
|
||||
assert [item.music.disc_number for item in projected.items] == [1, 2]
|
||||
assert [item.source_item.path for item in projected.items] == [str(path) for path in paths]
|
||||
artist, album = ("周杰伦", "叶惠美") if simplified else ("周杰倫", "葉惠美")
|
||||
assert destinations == [
|
||||
"收藏/周杰伦/叶惠美 (2003)/Disc 1/01 - 晴天.flac",
|
||||
"收藏/周杰伦/叶惠美 (2003)/Disc 2/01 - 以父之名.flac",
|
||||
f"收藏/{artist}/{album} (2003)/Disc 1/01 - 晴天.flac",
|
||||
f"收藏/{artist}/{album} (2003)/Disc 2/01 - 以父之名.flac",
|
||||
]
|
||||
assert [hashlib.sha256(path.read_bytes()).digest() for path in paths] == hashes
|
||||
for operation in online:
|
||||
|
||||
109
tests/test_music_metadata_simplification.py
Normal file
109
tests/test_music_metadata_simplification.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""音乐专辑详情与整理命名共享简体开关,并保留原始来源证据。"""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from jinja2 import Template
|
||||
|
||||
from app.application.messaging.message import TemplateContextBuilder
|
||||
from app.chain.media import MediaChain
|
||||
from app.domain.context import MusicAlbumInfo, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.runtime.config import ConfigModel, settings
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asynchronous", [False, True])
|
||||
@pytest.mark.parametrize("simplified", [False, True])
|
||||
@pytest.mark.parametrize("album_title", ["歲月", "Album"])
|
||||
def test_album_details_simplify_tracks_without_mutating_source(
|
||||
monkeypatch, asynchronous, simplified, album_title,
|
||||
):
|
||||
"""同步、异步详情均转换曲目;仅曲目含繁体时也不能污染来源缓存。"""
|
||||
source = MusicAlbumInfo(
|
||||
media_source="musicbrainz", media_id="album-1", title=album_title,
|
||||
artists=["優里"] if album_title != "Album" else ["Artist"],
|
||||
artist_ids=["artist-1"], release_date="2026-09-02",
|
||||
musicbrainz_release_id="release-1",
|
||||
tracks=[MusicInfo(
|
||||
media_source="musicbrainz", media_id="recording-1",
|
||||
title="後來", artists=["優里", "UMI"], artist_ids=["artist-1", "artist-2"],
|
||||
album="歲月", album_artist="優里 / UMI", album_id="album-1",
|
||||
lyrics="後來的歌詞", track_number=1, disc_number=1,
|
||||
musicbrainz_release_id="release-1", musicbrainz_release_track_id="track-1",
|
||||
raw_data={"title": "後來", "artist-credit": [{"name": "優里"}]},
|
||||
)],
|
||||
raw_data={"title": album_title},
|
||||
)
|
||||
original = deepcopy(source)
|
||||
provider = Mock()
|
||||
provider.get_music_album.return_value = source
|
||||
provider.async_get_music_album = AsyncMock(return_value=source)
|
||||
chain = MediaChain()
|
||||
monkeypatch.setattr(chain, "_music_source_chain", Mock(return_value=provider))
|
||||
monkeypatch.setattr(settings, "MUSIC_METADATA_TO_SIMPLIFIED", simplified)
|
||||
|
||||
kwargs = {"media_source": "musicbrainz", "media_id": "album-1",
|
||||
"musicbrainz_release_id": "release-1"}
|
||||
result = (asyncio.run(chain.async_get_music_album(**kwargs)) if asynchronous
|
||||
else chain.get_music_album(**kwargs))
|
||||
|
||||
assert result.title == ("岁月" if simplified and album_title != "Album" else album_title)
|
||||
assert result.artists == (["优里"] if simplified and album_title != "Album" else original.artists)
|
||||
assert result.artist_ids == original.artist_ids
|
||||
assert result.musicbrainz_release_id == "release-1"
|
||||
track = result.tracks[0]
|
||||
assert track.title == ("后来" if simplified else "後來")
|
||||
assert track.artists == (["优里", "UMI"] if simplified else ["優里", "UMI"])
|
||||
assert track.album == ("岁月" if simplified else "歲月")
|
||||
assert track.album_artist == ("优里 / UMI" if simplified else "優里 / UMI")
|
||||
assert (track.media_id, track.album_id, track.artist_ids) == ("recording-1", "album-1", ["artist-1", "artist-2"])
|
||||
assert (track.musicbrainz_release_id, track.musicbrainz_release_track_id) == ("release-1", "track-1")
|
||||
assert (track.track_number, track.disc_number) == (1, 1)
|
||||
assert track.lyrics == original.tracks[0].lyrics
|
||||
assert track.raw_data == original.tracks[0].raw_data
|
||||
assert source == original
|
||||
if simplified:
|
||||
assert result is not source
|
||||
assert "後來" in track.title_aliases
|
||||
if album_title != "Album":
|
||||
assert "歲月" in result.title_aliases
|
||||
assert "優里" in result.artist_aliases
|
||||
|
||||
|
||||
@pytest.mark.parametrize("music_type", ["local", "recording", "album"])
|
||||
@pytest.mark.parametrize("simplified", [False, True])
|
||||
def test_music_naming_converts_selected_fields_without_changing_identity(
|
||||
monkeypatch, music_type, simplified,
|
||||
):
|
||||
"""本地标签、录音与专辑命名同样转简体,录音身份不替换本地发行专辑。"""
|
||||
meta = MetaMusic(
|
||||
title="後來", artists=["優里", "UMI"], album="歲月",
|
||||
album_artist="優里", year=2026, track_number=1, disc_number=2, total_discs=2,
|
||||
)
|
||||
info = MusicInfo.from_meta(meta)
|
||||
if music_type != "local":
|
||||
info.media_source, info.media_id, info.music_type = "musicbrainz", "selected-1", music_type
|
||||
info.album, info.album_artist, info.year = "精選", "合輯藝人", 2020
|
||||
original_meta, original_info = deepcopy(meta.to_dict()), deepcopy(info)
|
||||
monkeypatch.setattr(settings, "MUSIC_METADATA_TO_SIMPLIFIED", simplified)
|
||||
|
||||
context = TemplateContextBuilder().build(
|
||||
meta=meta, mediainfo=info, file_extension=".flac", include_raw_objects=False,
|
||||
)
|
||||
path = Template(ConfigModel().MUSIC_RENAME_FORMAT).render(context)
|
||||
|
||||
expected_artist = "合辑艺人" if music_type == "album" else "优里"
|
||||
expected_album = "精选" if music_type == "album" else "岁月"
|
||||
if not simplified:
|
||||
expected_artist = "合輯藝人" if music_type == "album" else "優里"
|
||||
expected_album = "精選" if music_type == "album" else "歲月"
|
||||
expected_title = "后来" if simplified else "後來"
|
||||
expected_year = 2020 if music_type == "album" else 2026
|
||||
assert path == f"{expected_artist}/{expected_album} ({expected_year})/Disc 2/01 - {expected_title}.flac"
|
||||
assert context["artists"] == (["优里", "UMI"] if simplified else ["優里", "UMI"])
|
||||
assert context["artist"] == ("优里 / UMI" if simplified else "優里 / UMI")
|
||||
assert context["title_year"] == f"{expected_title} ({expected_year})"
|
||||
assert meta.to_dict() == original_meta
|
||||
assert info == original_info
|
||||
Reference in New Issue
Block a user