From 47f348219234a72ff38a94c48a0582f04837f586 Mon Sep 17 00:00:00 2001 From: zebra Date: Sat, 13 Jun 2026 22:14:57 -0700 Subject: [PATCH] fix: Odesli links 'not found' when Odesli omits YouTube link Root cause: Odesli's linksByPlatform frequently lacks youtube/youtubeMusic (confirmed live for many Spotify tracks). resolve_link_hits only added a YouTube hit when Odesli supplied one, so with no Lidarr match the hit list was empty -> server 404 'No results found'. Fix: always run a normal youtube_search (via build_combined_hits) for the fallback; when Odesli DID return a link, insert its exact track as the first YouTube hit. Lidarr-first ordering preserved. Co-Authored-By: Claude Opus 4.8 --- musicfetch | 18 +++++++++++------- tests/test_odesli.py | 30 ++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/musicfetch b/musicfetch index 688deed..28af6ec 100755 --- a/musicfetch +++ b/musicfetch @@ -898,18 +898,22 @@ def get_artist_from_metadata(meta: dict) -> str: def resolve_link_hits(url: str, limit: int) -> tuple[str, list[Hit]]: """Resolve a non-YouTube/SoundCloud link via Odesli into a search query plus - hits: Lidarr album candidates for "Artist - Title", followed by the EXACT - YouTube track from the shared link (not a fuzzy re-search). Raises OdesliError - if the link can't be resolved.""" + Lidarr-first, YouTube-fallback hits for "Artist - Title". Odesli frequently + omits a YouTube link, so we always run a normal youtube_search for the + fallback; when Odesli DID supply a link, its exact track is inserted as the + preferred (first) YouTube hit. Raises OdesliError if the link can't resolve.""" r = odesli_resolve(url) if r is None: raise OdesliError(url) query = f"{r.artist} - {r.title}".strip(" -") - hits = lidarr_search(query, limit) + hits = build_combined_hits(query, limit, yt_first=False, + lidarr_only=False, yt_only=False) if r.youtube_url: - hits = hits + [Hit(source="youtube", kind="track", title=r.title, - artist=r.artist, thumbnail=r.thumb, - payload={"url": r.youtube_url})] + exact = Hit(source="youtube", kind="track", title=r.title, + artist=r.artist, thumbnail=r.thumb, + payload={"url": r.youtube_url}) + idx = next((i for i, h in enumerate(hits) if h.source == "youtube"), len(hits)) + hits = hits[:idx] + [exact] + hits[idx:] return query, hits diff --git a/tests/test_odesli.py b/tests/test_odesli.py index c95f702..219c63b 100644 --- a/tests/test_odesli.py +++ b/tests/test_odesli.py @@ -107,28 +107,38 @@ def _resolved(yt="https://music.youtube.com/watch?v=YYY"): thumb="https://img/cover.jpg", youtube_url=yt) -def test_resolve_link_hits_builds_query_and_exact_yt(monkeypatch): +def _ytsearch_hit(): + return mf.Hit(source="youtube", kind="track", title="Bloom (search)", + artist="ODESZA", payload={"videoId": "srch"}) + + +def test_resolve_link_hits_lidarr_first_then_exact_then_search(monkeypatch): + # Odesli supplies a YouTube link -> exact track is the FIRST youtube hit, + # ahead of the fuzzy youtube_search results, and after the Lidarr hits. monkeypatch.setattr(mf, "odesli_resolve", lambda url: _resolved()) lid = mf.Hit(source="lidarr", kind="album", title="A Moment Apart", artist="ODESZA", album="A Moment Apart", year="2017", payload={"album": {"id": 9}}) monkeypatch.setattr(mf, "lidarr_search", lambda q, limit: [lid]) + monkeypatch.setattr(mf, "youtube_search", lambda q, limit: [_ytsearch_hit()]) query, hits = mf.resolve_link_hits("https://open.spotify.com/track/abc", 10) assert query == "ODESZA - Bloom" assert hits[0].source == "lidarr" - yt = hits[-1] - assert yt.source == "youtube" and yt.kind == "track" - assert yt.title == "Bloom" and yt.artist == "ODESZA" - assert yt.payload["url"] == "https://music.youtube.com/watch?v=YYY" + yt = [h for h in hits if h.source == "youtube"] + assert yt[0].payload.get("url") == "https://music.youtube.com/watch?v=YYY" # exact first + assert any(h.payload.get("videoId") == "srch" for h in yt) # search fallback present -def test_resolve_link_hits_no_yt_link_lidarr_only(monkeypatch): +def test_resolve_link_hits_no_odesli_yt_uses_search_fallback(monkeypatch): + # Regression: Odesli often omits YouTube links. With no Lidarr match and no + # Odesli YouTube link, a normal youtube_search must still yield hits (not empty). monkeypatch.setattr(mf, "odesli_resolve", lambda url: _resolved(yt="")) - lid = mf.Hit(source="lidarr", kind="album", title="X", artist="ODESZA", - payload={"album": {"id": 9}}) - monkeypatch.setattr(mf, "lidarr_search", lambda q, limit: [lid]) + monkeypatch.setattr(mf, "lidarr_search", lambda q, limit: []) + monkeypatch.setattr(mf, "youtube_search", lambda q, limit: [_ytsearch_hit()]) query, hits = mf.resolve_link_hits("https://open.spotify.com/track/abc", 10) - assert all(h.source == "lidarr" for h in hits) + assert hits, "must not be empty when YouTube search finds the track" + assert all(h.source == "youtube" for h in hits) + assert not any(h.payload.get("url") for h in hits) # no exact Odesli hit injected def test_resolve_link_hits_odesli_miss_raises(monkeypatch):