diff --git a/musicfetch b/musicfetch index fe90979..f33a993 100755 --- a/musicfetch +++ b/musicfetch @@ -900,6 +900,42 @@ def get_artist_from_metadata(meta: dict) -> str: return "Unknown Artist" +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.""" + r = odesli_resolve(url) + if r is None: + raise OdesliError(url) + query = f"{r.artist} - {r.title}".strip(" -") + hits = lidarr_search(query, limit) + 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})] + return query, hits + + +def handle_link(url: str, root: str, quality: str, dry_run: bool, + noninteractive: bool, yt_first: bool, limit: int) -> None: + """CLI path for a non-direct link: resolve via Odesli, then run the normal + Lidarr-first pick/dispatch with the exact YouTube track as fallback.""" + try: + query, hits = resolve_link_hits(url, limit) + except OdesliError: + err(f"Couldn't resolve {url}. Try the direct YouTube/SoundCloud link.") + return + if not hits: + err(f"No Lidarr or YouTube source found for '{query}'.") + return + chosen = pick(hits, query, noninteractive, yt_first) + if not chosen: + print("Nothing selected.") + return + _dispatch_chosen(chosen, hits, root, quality, dry_run, False, False) + + def handle_url(url: str, root: str, quality: str, dry_run: bool): kind, title, hits = probe_url(url) if kind == "playlist": diff --git a/tests/test_odesli.py b/tests/test_odesli.py index 62415a5..c2edcb2 100644 --- a/tests/test_odesli.py +++ b/tests/test_odesli.py @@ -86,3 +86,60 @@ def test_is_direct_url_other_platforms_false(): def test_is_direct_url_youtube_playlist_true(): assert mf._is_direct_url("https://www.youtube.com/playlist?list=PLabc") + + +def _resolved(yt="https://music.youtube.com/watch?v=YYY"): + return mf.Resolved(title="Bloom", artist="ODESZA", + thumb="https://img/cover.jpg", youtube_url=yt) + + +def test_resolve_link_hits_builds_query_and_exact_yt(monkeypatch): + 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]) + 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" + + +def test_resolve_link_hits_no_yt_link_lidarr_only(monkeypatch): + 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]) + query, hits = mf.resolve_link_hits("https://open.spotify.com/track/abc", 10) + assert all(h.source == "lidarr" for h in hits) + + +def test_resolve_link_hits_odesli_miss_raises(monkeypatch): + import pytest + monkeypatch.setattr(mf, "odesli_resolve", lambda url: None) + with pytest.raises(mf.OdesliError): + mf.resolve_link_hits("https://open.spotify.com/track/abc", 10) + + +def test_handle_link_miss_prints_and_returns(monkeypatch, capsys): + monkeypatch.setattr(mf, "odesli_resolve", lambda url: None) + mf.handle_link("https://open.spotify.com/track/abc", "/m", "best", + False, True, False, 10) + out = capsys.readouterr() + assert "Couldn't resolve" in (out.err + out.out) + + +def test_handle_link_dispatches_chosen(monkeypatch): + monkeypatch.setattr(mf, "odesli_resolve", lambda url: _resolved()) + monkeypatch.setattr(mf, "lidarr_search", lambda q, limit: []) + chosen = {} + monkeypatch.setattr(mf, "pick", lambda hits, q, ni, yf: hits[0]) + monkeypatch.setattr(mf, "_dispatch_chosen", + lambda c, hits, root, quality, dry, lo, sa: chosen.update(c=c, root=root)) + mf.handle_link("https://open.spotify.com/track/abc", "/m", "best", + False, True, False, 10) + assert chosen["c"].source == "youtube" + assert chosen["root"] == "/m"