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:
JontyLee
2026-10-05 20:20:34 +08:00
parent 5f4eb47712
commit 5cc0375ec1
2 changed files with 81 additions and 3 deletions

View File

@@ -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

View File

@@ -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")