Follow-on to 07801aeb (the orphaned-file delete committed alone because the
git add aborted on the already-removed pathspec). Removes the two RetagDeps
tests in test_lyrics_reembed_from_sidecar (the dataclass was deleted with the
old Retag Tool) and a no-placeholder f-string in test_library_retag_job.
170 lines
5.8 KiB
Python
170 lines
5.8 KiB
Python
"""Tests for re-embedding lyrics from an existing sidecar file.
|
|
|
|
Discord report (Netti93): retag was clearing the LYRICS / USLT tag
|
|
without rewriting it. Cause was two-fold:
|
|
|
|
1. `core/library/retag.py:execute_retag` never called
|
|
`generate_lrc_file` after `enhance_file_metadata`. The download
|
|
pipeline does — retag was inconsistent.
|
|
2. Even with the call added, `lyrics_client.create_lrc_file` used to
|
|
short-circuit when an .lrc / .txt sidecar already existed (the
|
|
typical retag case — sidecar moved alongside the audio file).
|
|
Pre-fix: returned True without re-embedding USLT. Post-fix: reads
|
|
the existing sidecar and re-embeds the USLT tag.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_audio_file(tmp_path):
|
|
"""Build a minimal FLAC file with no LYRICS tag."""
|
|
fd, path = tempfile.mkstemp(suffix='.flac', dir=str(tmp_path))
|
|
os.close(fd)
|
|
minimal = (
|
|
b'fLaC'
|
|
+ b'\x80\x00\x00\x22'
|
|
+ b'\x00\x10\x00\x10'
|
|
+ b'\x00\x00\x00\x00\x00\x00'
|
|
+ b'\x0a\xc4\x42\xf0\x00\x00\x00\x00'
|
|
+ b'\x00' * 16
|
|
)
|
|
with open(path, 'wb') as f:
|
|
f.write(minimal)
|
|
yield path
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
# create_lrc_file — re-embed when sidecar present
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
|
|
def test_existing_lrc_sidecar_triggers_reembed(fake_audio_file):
|
|
"""The exact retag scenario — sidecar already exists alongside the
|
|
audio file (moved during retag), USLT got cleared by enrichment.
|
|
Helper should read the sidecar and re-embed without hitting LRClib."""
|
|
from core.lyrics_client import LyricsClient
|
|
|
|
sidecar_path = os.path.splitext(fake_audio_file)[0] + '.lrc'
|
|
with open(sidecar_path, 'w', encoding='utf-8') as f:
|
|
f.write('[00:01.00]Test lyric line\n[00:05.00]Second line')
|
|
|
|
client = LyricsClient()
|
|
client.api = MagicMock() # API stub — should NOT be called
|
|
client._embed_lyrics = MagicMock()
|
|
|
|
result = client.create_lrc_file(
|
|
audio_file_path=fake_audio_file,
|
|
track_name='Test',
|
|
artist_name='Artist',
|
|
)
|
|
|
|
assert result is True
|
|
# API never hit — sidecar shortcut
|
|
client.api.get_lyrics.assert_not_called()
|
|
client.api.search_lyrics.assert_not_called()
|
|
# USLT was re-embedded
|
|
client._embed_lyrics.assert_called_once()
|
|
call_args = client._embed_lyrics.call_args
|
|
assert call_args.args[0] == fake_audio_file
|
|
assert 'Test lyric line' in call_args.args[1]
|
|
|
|
|
|
def test_existing_txt_sidecar_also_triggers_reembed(fake_audio_file):
|
|
"""Same shape with .txt sidecar (plain lyrics, no timestamps)."""
|
|
from core.lyrics_client import LyricsClient
|
|
|
|
sidecar_path = os.path.splitext(fake_audio_file)[0] + '.txt'
|
|
with open(sidecar_path, 'w', encoding='utf-8') as f:
|
|
f.write('Just plain lyrics no timestamps')
|
|
|
|
client = LyricsClient()
|
|
client.api = MagicMock()
|
|
client._embed_lyrics = MagicMock()
|
|
|
|
result = client.create_lrc_file(
|
|
audio_file_path=fake_audio_file,
|
|
track_name='T', artist_name='A',
|
|
)
|
|
|
|
assert result is True
|
|
client._embed_lyrics.assert_called_once_with(
|
|
fake_audio_file, 'Just plain lyrics no timestamps'
|
|
)
|
|
|
|
|
|
def test_empty_sidecar_does_not_embed(fake_audio_file):
|
|
"""Defensive — if the sidecar exists but is empty, don't write an
|
|
empty USLT tag."""
|
|
from core.lyrics_client import LyricsClient
|
|
|
|
sidecar_path = os.path.splitext(fake_audio_file)[0] + '.lrc'
|
|
with open(sidecar_path, 'w', encoding='utf-8') as f:
|
|
f.write(' \n ') # whitespace only
|
|
|
|
client = LyricsClient()
|
|
client.api = MagicMock()
|
|
client._embed_lyrics = MagicMock()
|
|
|
|
result = client.create_lrc_file(
|
|
audio_file_path=fake_audio_file,
|
|
track_name='T', artist_name='A',
|
|
)
|
|
|
|
assert result is True
|
|
client._embed_lyrics.assert_not_called()
|
|
|
|
|
|
def test_unreadable_sidecar_swallows_error_returns_true(fake_audio_file):
|
|
"""If the sidecar is somehow unreadable, return True (don't try
|
|
LRClib again — the early-return contract holds), just skip the
|
|
embed silently."""
|
|
from core.lyrics_client import LyricsClient
|
|
|
|
sidecar_path = os.path.splitext(fake_audio_file)[0] + '.lrc'
|
|
with open(sidecar_path, 'wb') as f:
|
|
f.write(b'\xff\xfe\x00\x00') # invalid UTF-8
|
|
|
|
client = LyricsClient()
|
|
client.api = MagicMock()
|
|
client._embed_lyrics = MagicMock()
|
|
|
|
result = client.create_lrc_file(
|
|
audio_file_path=fake_audio_file,
|
|
track_name='T', artist_name='A',
|
|
)
|
|
|
|
assert result is True
|
|
client.api.get_lyrics.assert_not_called()
|
|
|
|
|
|
def test_no_sidecar_falls_through_to_lrclib(fake_audio_file):
|
|
"""No sidecar → original LRClib fetch path runs (download flow)."""
|
|
from core.lyrics_client import LyricsClient
|
|
|
|
client = LyricsClient()
|
|
fake_lyrics = MagicMock()
|
|
fake_lyrics.synced_lyrics = '[00:01.00]synced from api'
|
|
fake_lyrics.plain_lyrics = None
|
|
client.api = MagicMock()
|
|
client.api.get_lyrics.return_value = None
|
|
client.api.search_lyrics.return_value = [fake_lyrics]
|
|
client._embed_lyrics = MagicMock()
|
|
|
|
result = client.create_lrc_file(
|
|
audio_file_path=fake_audio_file,
|
|
track_name='T', artist_name='A',
|
|
)
|
|
|
|
assert result is True
|
|
client.api.search_lyrics.assert_called_once()
|
|
# Sidecar was created
|
|
lrc = os.path.splitext(fake_audio_file)[0] + '.lrc'
|
|
assert os.path.exists(lrc)
|
|
# And USLT was embedded
|
|
client._embed_lyrics.assert_called_once()
|