From ed38d60b18c4ded0c710212151e9108e564ea8b6 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 7 Jun 2026 19:45:10 -0700 Subject: [PATCH] =?UTF-8?q?Lyrics=20Filler:=20convert=20track=20duration?= =?UTF-8?q?=20ms=E2=86=92s=20for=20LRClib=20(exact-match=20was=20silently?= =?UTF-8?q?=20defeated)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second lock-in catch: tracks.duration is stored in MILLISECONDS (schema), but the scan passed it to LRClib as SECONDS. LRClib's exact-match-by-duration strategy would never hit (215000s vs the real 215s), silently falling back to the fuzzier title/artist search and storing the wrong duration in the finding. Now divides by 1000 (guards against 0/garbage). Lyrics were still being found via the fallback, so no track was missed — just less precise matching and a wrong stored value. Test pins 215000ms → 215s. --- core/repair_jobs/missing_lyrics.py | 7 ++++++- tests/test_missing_lyrics_job.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/repair_jobs/missing_lyrics.py b/core/repair_jobs/missing_lyrics.py index 4cf02a93..1cf9d95d 100644 --- a/core/repair_jobs/missing_lyrics.py +++ b/core/repair_jobs/missing_lyrics.py @@ -114,8 +114,13 @@ class MissingLyricsJob(RepairJob): # Option A: only flag tracks LRClib actually has lyrics for. An # instrumental returns nothing here and is silently skipped (never # re-flagged on future scans). + # tracks.duration is stored in MILLISECONDS (schema) — LRClib's + # exact-match wants SECONDS, so convert (else exact-match never + # hits and it silently falls back to a fuzzier search). try: - duration_s = int(duration) if duration else None + duration_s = int(int(duration) / 1000) if duration else None + if duration_s is not None and duration_s <= 0: + duration_s = None except (TypeError, ValueError): duration_s = None try: diff --git a/tests/test_missing_lyrics_job.py b/tests/test_missing_lyrics_job.py index fe7038e2..3adfbd9c 100644 --- a/tests/test_missing_lyrics_job.py +++ b/tests/test_missing_lyrics_job.py @@ -112,6 +112,23 @@ def test_scan_flags_only_tracks_with_available_lyrics(tmp_path, monkeypatch): assert findings[0]["details"]["track_title"] == "Song" # the instrumental was skipped +def test_scan_converts_duration_ms_to_seconds(tmp_path, monkeypatch): + # tracks.duration is milliseconds; LRClib wants seconds. The scan must + # convert before querying (215000ms → 215s) and store seconds in the finding. + t1 = tmp_path / "song.flac"; t1.write_bytes(b"x") + rows = [(1, "Song", "Artist", "Album", str(t1), 215000)] # 215000 ms = 215 s + seen = {} + fake_client = SimpleNamespace( + api=object(), + has_remote_lyrics=lambda title, artist, album, dur: seen.update(dur=dur) or True) + monkeypatch.setattr("core.lyrics_client.lyrics_client", fake_client) + + findings = [] + MissingLyricsJob().scan(_ctx(_DB(rows), findings)) + assert seen["dur"] == 215 # converted to seconds + assert findings[0]["details"]["duration"] == 215 + + def test_scan_skips_tracks_that_already_have_lrc(tmp_path, monkeypatch): t1 = tmp_path / "song.flac"; t1.write_bytes(b"x") (tmp_path / "song.lrc").write_text("[00:01]hi") # already has lyrics