import json as _json import server.mf # noqa: F401 — loads musicfetch, registers musicfetch_core import musicfetch_core as mf class _CP: def __init__(self, stdout): self.stdout = stdout self.returncode = 0 # ---- _is_youtube_playlist_url ---- def test_youtube_playlist_url_true(): assert mf._is_youtube_playlist_url("https://music.youtube.com/playlist?list=PLabc") is True assert mf._is_youtube_playlist_url("https://www.youtube.com/playlist?list=PLabc") is True def test_youtube_watch_with_list_is_not_playlist(): assert mf._is_youtube_playlist_url("https://www.youtube.com/watch?v=abc&list=PLx") is False def test_non_youtube_url_not_youtube_playlist(): # SoundCloud sets are not matched here — probe_url handles them via yt-dlp. assert mf._is_youtube_playlist_url("https://soundcloud.com/user/sets/mix") is False # ---- probe_url ---- def test_probe_url_youtube_playlist_uses_ytmusic(monkeypatch): h = mf.Hit(source="youtube", kind="track", title="A", artist="X", payload={"videoId": "1", "extractor": "youtube"}) monkeypatch.setattr(mf, "_ytmusic_playlist", lambda pid: ("My YT Mix", [h])) monkeypatch.setattr(mf, "YTMusic", object()) # non-None to enter ytmusic branch kind, title, hits = mf.probe_url("https://music.youtube.com/playlist?list=PLx") assert kind == "playlist" assert title == "My YT Mix" assert hits == [h] def test_probe_url_generic_playlist_via_ytdlp(monkeypatch): monkeypatch.setattr(mf, "YTMusic", None) payload = {"title": "SC Set", "_type": "playlist", "entries": [ {"id": "t1", "title": "One", "uploader": "DJ", "ie_key": "Soundcloud", "url": "https://soundcloud.com/dj/one"}, {"id": None, "url": None, "title": "skip"}, ]} monkeypatch.setattr(mf.subprocess, "run", lambda *a, **k: _CP(_json.dumps(payload))) kind, title, hits = mf.probe_url("https://soundcloud.com/dj/sets/sc-set") assert kind == "playlist" assert title == "SC Set" assert len(hits) == 1 assert hits[0].payload["extractor"] == "soundcloud" assert hits[0].payload["url"] == "https://soundcloud.com/dj/one" def test_probe_url_single_track(monkeypatch): monkeypatch.setattr(mf, "YTMusic", None) payload = {"title": "A Song", "extractor": "soundcloud"} # no entries -> single monkeypatch.setattr(mf.subprocess, "run", lambda *a, **k: _CP(_json.dumps(payload))) kind, title, hits = mf.probe_url("https://soundcloud.com/dj/one") assert kind == "track" assert title == "A Song" assert hits == [] def test_probe_url_failure_returns_track(monkeypatch): monkeypatch.setattr(mf, "YTMusic", None) def boom(*a, **k): raise mf.subprocess.CalledProcessError(1, "yt-dlp") monkeypatch.setattr(mf.subprocess, "run", boom) assert mf.probe_url("https://example.com/x") == ("track", "", []) # ---- download_hits ---- def test_download_hits_counts(monkeypatch): h1 = mf.Hit(source="youtube", kind="track", title="A", artist="X", payload={"videoId": "1"}) h2 = mf.Hit(source="youtube", kind="track", title="B", artist="Y", payload={"videoId": "2"}) h3 = mf.Hit(source="youtube", kind="track", title="C", artist="Z", payload={"videoId": "3"}) monkeypatch.setattr(mf, "act_youtube", lambda hit, root, quality, dry_run: hit.title != "B") assert mf.download_hits([h1, h2, h3], "/tmp", "best", False) == (2, 3) def test_download_hits_track_exception_is_failure(monkeypatch): h1 = mf.Hit(source="youtube", kind="track", title="A", artist="X", payload={"videoId": "1"}) h2 = mf.Hit(source="youtube", kind="track", title="B", artist="Y", payload={"videoId": "2"}) def fake_act(hit, root, quality, dry_run): if hit.title == "B": raise RuntimeError("boom") return True monkeypatch.setattr(mf, "act_youtube", fake_act) assert mf.download_hits([h1, h2], "/tmp", "best", False) == (1, 2) # ---- yt_download bool ---- def test_yt_download_returns_true_on_zero_exit(monkeypatch): monkeypatch.setattr(mf.os, "makedirs", lambda *a, **k: None) monkeypatch.setattr(mf.subprocess, "run", lambda *a, **k: _CP("")) assert mf.yt_download("u", "/tmp/x", "best", False) is True def test_yt_download_dry_run_returns_true(): assert mf.yt_download("u", "/tmp/x", "best", True) is True def test_yt_download_always_sets_album_default(monkeypatch): captured = {} monkeypatch.setattr(mf.os, "makedirs", lambda *a, **k: None) monkeypatch.setattr(mf.subprocess, "run", lambda cmd, **k: captured.update(cmd=cmd) or _CP("")) mf.yt_download("u", "/tmp/x", "best", False) assert "%(album|Unknown Album)s:%(meta_album)s" in captured["cmd"] def test_yt_download_single_word_tags_injected_literally(monkeypatch): # Regression: `--parse-metadata "Cochise:%(title)s"` makes yt-dlp treat the # bare word 'Cochise' as a FIELD name (field_to_template's r'[a-zA-Z_]+$'), # producing 'NA'. Single-word album/title must reach yt-dlp as literals. captured = {} monkeypatch.setattr(mf.os, "makedirs", lambda *a, **k: None) monkeypatch.setattr(mf.subprocess, "run", lambda cmd, **k: captured.update(cmd=cmd) or _CP("")) hit = mf.Hit(source="youtube", kind="track", title="Cochise", artist="Audioslave", album="Solid", payload={"videoId": "x"}) mf.yt_download("u", "/tmp/x", "best", False, hit=hit) cmd = captured["cmd"] joined = " ".join(cmd) # The buggy bare-word parse-metadata FROM must be gone. assert "Solid:%(album)s" not in joined assert "Cochise:%(title)s" not in joined # Literal values must be passed as literal args (immune to template parsing). assert "Solid" in cmd assert "Cochise" in cmd # A hit album must not be clobbered by the Unknown-Album default. assert "%(album|Unknown Album)s:%(meta_album)s" not in cmd