mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
fix(audio): ignore stale CUE sheets whose referenced files are gone
Adopt a CUE only when at least one FILE reference resolves to a real file in the audio directory. Legacy WAV-era cue sheets shipped inside FLAC split-track releases previously blocked every track transfer with "CUE 引用的文件名与当前音频不一致" (and broken-index cues did the same); they are now ignored with an info log and organization falls back to the files' own tags. Cross-directory references and cues that still point at existing files keep the current strict behavior. Refs: https://github.com/jxxghp/MoviePilot/issues/6888
This commit is contained in:
@@ -174,6 +174,28 @@ def _cue_mentions_audio(text: str, path: Path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _cue_file_references(text: str) -> list[str]:
|
||||
"""从原始 CUE 文本提取 FILE 引用名,供解析失败时判断引用是否仍然存在。"""
|
||||
references = []
|
||||
for line in text.splitlines():
|
||||
match = re.match(r'\s*FILE\s+(?:"([^"\r\n]+)"|(\S+))', line, re.IGNORECASE)
|
||||
if match:
|
||||
references.append(match.group(1) or match.group(2))
|
||||
return references
|
||||
|
||||
|
||||
def _cue_references_existing_audio(path: Path, names: Any) -> bool:
|
||||
"""CUE 至少引用一个目录中真实存在的音频才可采信;跨目录引用保守视为存在。"""
|
||||
for name in names:
|
||||
try:
|
||||
reference = _cue_reference_name(name)
|
||||
except ValueError:
|
||||
return True
|
||||
if (path.parent / reference).is_file():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _cue_candidates(path: Path) -> list[Path]:
|
||||
"""只扫描音频同目录的有限 CUE,不遍历全集或其它目录。"""
|
||||
candidates = []
|
||||
@@ -206,10 +228,14 @@ def _find_audio_cue(path: Path) -> Optional[tuple[Path, MusicCueSheet]]:
|
||||
try:
|
||||
sheet = parse_music_cue(text)
|
||||
except ValueError as error:
|
||||
if candidate.stem.casefold() == path.stem.casefold() or _cue_mentions_audio(text, path):
|
||||
mentions = candidate.stem.casefold() == path.stem.casefold() or _cue_mentions_audio(text, path)
|
||||
if mentions and _cue_references_existing_audio(path, _cue_file_references(text)):
|
||||
raise ValueError(f"关联 CUE 无法解析:{candidate.name} - {error}") from error
|
||||
continue
|
||||
references = {track.file_name for track in sheet.tracks}
|
||||
if not _cue_references_existing_audio(path, references):
|
||||
logger.info(f"CUE 引用的音频文件均已不存在,按陈旧索引忽略:{candidate}")
|
||||
continue
|
||||
related = [name for name in references if PurePosixPath(name.replace("\\", "/")).stem.casefold() == path.stem.casefold()]
|
||||
if not related:
|
||||
continue
|
||||
|
||||
@@ -122,9 +122,9 @@ def test_image_cue_never_runs_recording_fingerprint(tmp_path, monkeypatch, async
|
||||
async_fingerprint.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_reference", ["../image.flac", "image.wav", "IMAGE.flac", "C:\\music\\image.flac"])
|
||||
@pytest.mark.parametrize("bad_reference", ["../image.flac", "C:\\music\\image.flac"])
|
||||
def test_cue_reference_errors_block_even_without_category_folders(tmp_path, bad_reference):
|
||||
"""跨目录、错误后缀和大小写等引用不能靠关闭分类目录绕过检查。"""
|
||||
"""跨目录引用不能靠关闭分类目录绕过检查;同目录引用交给存在性判定。"""
|
||||
audio, _ = _image_pair(tmp_path, text=CUE.replace('"image.flac"', f'"{bad_reference}"'))
|
||||
meta = AudioMetadataHelper.read(audio)
|
||||
task = TransferTask(fileitem=make_fileitem(str(audio)), meta=meta, mediainfo=MusicInfo.from_meta(meta),
|
||||
@@ -135,6 +135,58 @@ def test_cue_reference_errors_block_even_without_category_folders(tmp_path, bad_
|
||||
assert meta.organization_error
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stale_reference", ["image.wav"])
|
||||
def test_cue_reference_missing_from_directory_falls_back_to_tags(tmp_path, stale_reference):
|
||||
"""引用文件已不存在的陈旧索引(转制未更新扩展名等)不得阻断整理,回退标签。"""
|
||||
audio, _ = _image_pair(tmp_path, text=CUE.replace('"image.flac"', f'"{stale_reference}"'))
|
||||
tags = FLAC(audio)
|
||||
tags.update(title=["独立单曲"], artist=["测试歌手"], album=["专辑示例"], tracknumber=["2"])
|
||||
tags.save()
|
||||
|
||||
meta = AudioMetadataHelper.read(audio)
|
||||
|
||||
assert not meta.organization_error
|
||||
assert meta.music_layout != "cue_invalid"
|
||||
assert meta.title == "独立单曲"
|
||||
assert meta.track_number == 2
|
||||
|
||||
|
||||
def test_stale_wav_cue_does_not_block_flac_split_album(tmp_path):
|
||||
"""WAV 时代留档 CUE 全部引用不存在的 .wav 时,FLAC 分轨按自带标签正常整理。"""
|
||||
source = tmp_path / "source"
|
||||
source.mkdir()
|
||||
for name in ("01 - 以父之名.flac", "02 - 懦夫.flac"):
|
||||
shutil.copyfile(Path(__file__).parent / "fixtures/audio/silence.flac", source / name)
|
||||
(source / "album.cue").write_text('''PERFORMER "周杰倫"
|
||||
TITLE "葉惠美"
|
||||
FILE "01 - 以父之名.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "以父之名"
|
||||
INDEX 01 00:00:00
|
||||
FILE "02 - 懦夫.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "懦夫"
|
||||
INDEX 01 00:00:00
|
||||
''')
|
||||
|
||||
meta = AudioMetadataHelper.read(source / "01 - 以父之名.flac")
|
||||
|
||||
assert not meta.organization_error
|
||||
assert meta.music_layout != "cue_invalid"
|
||||
assert not meta.cue_tracks
|
||||
|
||||
|
||||
def test_broken_index_without_existing_references_is_ignored(tmp_path):
|
||||
"""结构损坏且引用文件全部不存在的 CUE 同样按陈旧索引忽略,不再阻断。"""
|
||||
audio, _ = _image_pair(tmp_path, text=CUE.replace('"image.flac"', '"image.wav"')
|
||||
.replace(" INDEX 01 00:01:00\n", ""))
|
||||
|
||||
meta = AudioMetadataHelper.read(audio)
|
||||
|
||||
assert not meta.organization_error
|
||||
assert meta.music_layout != "cue_invalid"
|
||||
|
||||
|
||||
def test_single_audio_collects_nonmatching_stem_cue_companion(tmp_path, monkeypatch):
|
||||
"""FILE 关联有效时,CUE 文件名无需与音频同主干,且应在音频后加入归档。"""
|
||||
audio, cue = _image_pair(tmp_path, cue_name="album-index.cue")
|
||||
|
||||
Reference in New Issue
Block a user