Skip to content

Commit f4a879b

Browse files
committed
fix(rename): 剥离专辑名尾部年份,避免重命名目录重复年份 (#6355)
音频标签或下载目录常把发行年份写进专辑名(如 "欲望反光 (2018)"), 而音乐重命名模板会独立追加 ({{year}}),两者叠加生成 "欲望反光 (2018) (2018)" 这类重复年份目录,并把同一专辑拆到多个目录。 在模板上下文中统一剥离专辑名尾部的括号年份;仅当存在可独立渲染的 年份时才剥离,避免丢失只存在于专辑名中的年份信息。
1 parent 87fe986 commit f4a879b

2 files changed

Lines changed: 98 additions & 8 deletions

File tree

app/application/messaging/message.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
from app.foundation.crypto import HashUtils
3030

3131

32+
# 专辑名尾部的括号年份标记;重命名模板会独立追加 `({{year}})`,
33+
# 标签或目录名中自带的尾部年份若不剥离,会生成重复年份的目录名(issue #6355)
34+
_ALBUM_TRAILING_YEAR_RE = re.compile(
35+
r"(?:[\s\u3000]*[\(\[(【]\s*(?:19|20)\d{2}\s*[\)\])】])+$"
36+
)
37+
38+
3239
class AsyncMessageQueryRepository(Protocol):
3340
"""消息查询用例依赖的异步持久化端口。"""
3441

@@ -169,23 +176,32 @@ def _add_media_info(
169176
aggregate_music_album
170177
and mediainfo.music_type == MUSIC_ENTITY_ALBUM
171178
)
179+
# 专辑场景以识别结果的专辑名为标题;整专年份以识别结果为准,
180+
# 逐文件场景沿用 meta 解析年份,保证文件级年份优先。
181+
year = (
182+
mediainfo.year
183+
if (is_album_context and mediainfo.year)
184+
else (context.get("year") or mediainfo.year)
185+
)
172186
if is_album_context and mediainfo.album:
173-
title = cls.__convert_invalid_characters(mediainfo.album)
187+
title = cls.__strip_album_trailing_year(
188+
cls.__convert_invalid_characters(mediainfo.album), year
189+
)
174190
else:
175191
title = context.get("title") or cls.__convert_invalid_characters(mediainfo.title)
176192
artists = context.get("artists") or [
177193
cls.__convert_invalid_characters(item) for item in mediainfo.artists
178194
]
179195
artist = context.get("artist") or cls.__convert_invalid_characters(mediainfo.artist)
180-
album = context.get("album") or cls.__convert_invalid_characters(mediainfo.album)
196+
# 标签/目录名自带的尾部年份会被重命名模板的 `({{year}})` 再次追加,
197+
# 统一剥离避免生成 "专辑 (2018) (2018)" 这类重复年份目录(issue #6355)
198+
album = cls.__strip_album_trailing_year(
199+
context.get("album") or cls.__convert_invalid_characters(mediainfo.album),
200+
year,
201+
)
181202
album_artist = context.get("album_artist") or cls.__convert_invalid_characters(
182203
mediainfo.album_artist
183204
)
184-
year = (
185-
mediainfo.year
186-
if (is_album_context and mediainfo.year)
187-
else (context.get("year") or mediainfo.year)
188-
)
189205
disc_number = context.get("disc_number") or mediainfo.disc_number
190206
track_number = (
191207
None
@@ -305,7 +321,10 @@ def _add_episode_details(
305321
"title": cls.__convert_invalid_characters(meta.title),
306322
"artists": [cls.__convert_invalid_characters(item) for item in meta.artists],
307323
"artist": cls.__convert_invalid_characters(meta.artist),
308-
"album": cls.__convert_invalid_characters(meta.album),
324+
# 标签专辑名常自带尾部年份,与模板独立追加的年份去重(issue #6355)
325+
"album": cls.__strip_album_trailing_year(
326+
cls.__convert_invalid_characters(meta.album), meta.year
327+
),
309328
"album_artist": cls.__convert_invalid_characters(meta.album_artist),
310329
"year": meta.year,
311330
"disc_number": meta.disc_number,
@@ -513,6 +532,21 @@ def __convert_invalid_characters(filename: str):
513532
filename = filename.replace(char, char.translate(translation_table))
514533
return filename
515534

535+
@staticmethod
536+
def __strip_album_trailing_year(
537+
album: Optional[str], year: Optional[int]
538+
) -> Optional[str]:
539+
"""
540+
去除专辑名尾部的括号年份标记(如 ``欲望反光 (2018)`` -> ``欲望反光``)。
541+
542+
音频标签或下载目录常把发行年份写进专辑名,而重命名模板会独立追加
543+
``({{year}})``,两者叠加会生成 "专辑 (2018) (2018)" 这类重复年份目录;
544+
仅当存在可独立渲染的年份时才剥离,避免丢失只存在于专辑名中的年份信息。
545+
"""
546+
if not album or not year:
547+
return album
548+
return _ALBUM_TRAILING_YEAR_RE.sub("", album) or album
549+
516550

517551
class TemplateHelper(metaclass=SingletonClass):
518552
"""

tests/test_template_context_builder.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,59 @@ def test_build_exposes_music_audio_specs_for_notifications() -> None:
189189
assert context["audio_specs"] == "FLAC · 24-bit · 96 kHz · 2,304 kbps"
190190
assert context["bitrate_kbps"] == 2304
191191
assert context["sample_rate_khz"] == "96"
192+
193+
194+
def _build_music_context(album, year):
195+
"""
196+
构造音乐重命名上下文:标签专辑名带年份、文件/目录年份可独立渲染的场景。
197+
"""
198+
meta = MetaMusic(
199+
title="欲望反光",
200+
album=album,
201+
artists=["萧敬腾"],
202+
album_artist="萧敬腾",
203+
year=year,
204+
track_number=1,
205+
audio_format="FLAC",
206+
)
207+
return TemplateContextBuilder().build(meta=meta)
208+
209+
210+
def test_music_rename_strips_duplicate_album_year() -> None:
211+
"""专辑名尾部的年份标记不应与模板追加的年份重复(issue #6355)。"""
212+
context = _build_music_context(album="欲望反光 (2018)", year=2018)
213+
214+
assert context["album"] == "欲望反光"
215+
assert context["year"] == 2018
216+
217+
218+
def test_music_rename_strips_album_year_different_from_media_year() -> None:
219+
"""标签专辑名年份与识别年份不一致时,仅保留模板追加的识别年份。"""
220+
context = _build_music_context(
221+
album="洛克先生Mr.Rock演唱会Live纪实 (2010)", year=2009
222+
)
223+
224+
assert context["album"] == "洛克先生Mr.Rock演唱会Live纪实"
225+
assert context["year"] == 2009
226+
227+
228+
def test_music_rename_strips_repeated_album_year_suffixes() -> None:
229+
"""历史整理已生成的重复年份目录再次重命名时,所有尾部年份都应被剥离。"""
230+
context = _build_music_context(album="爱的时刻自选辑 (2009) (2009)", year=2015)
231+
232+
assert context["album"] == "爱的时刻自选辑"
233+
assert context["year"] == 2015
234+
235+
236+
def test_music_rename_keeps_album_year_when_no_standalone_year() -> None:
237+
"""没有独立年份可渲染时保留专辑名自带的年份,避免信息丢失。"""
238+
context = _build_music_context(album="欲望反光 (2018)", year=None)
239+
240+
assert context["album"] == "欲望反光 (2018)"
241+
242+
243+
def test_music_rename_keeps_plain_album_title() -> None:
244+
"""不含年份的普通专辑名不受影响。"""
245+
context = _build_music_context(album="叶惠美", year=2003)
246+
247+
assert context["album"] == "叶惠美"

0 commit comments

Comments
 (0)