269 lines
12 KiB
Python
269 lines
12 KiB
Python
"""Library-aware Lidarr behaviour: never add a duplicate artist, never grab a
|
|
random album, and describe a release in words a person understands."""
|
|
import server.mf # noqa: F401 — loads musicfetch, registers musicfetch_core
|
|
import musicfetch_core as mf
|
|
|
|
|
|
JACKO = {"id": 3, "artistName": "Michael Jackson", "foreignArtistId": "mj-mbid"}
|
|
|
|
|
|
def _no_add(*_a, **_kw):
|
|
raise AssertionError("add_artist must not be called for an existing artist")
|
|
|
|
|
|
# --- find_library_artist / ensure_artist -----------------------------------
|
|
def test_find_library_artist_matches_name_case_insensitively(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
assert mf.find_library_artist(name="michael jackson") == JACKO
|
|
|
|
|
|
def test_find_library_artist_matches_mbid_when_name_differs(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
assert mf.find_library_artist(name="Jacko", foreign_id="mj-mbid") == JACKO
|
|
|
|
|
|
def test_find_library_artist_returns_none_when_absent(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
assert mf.find_library_artist(name="Overgrown") is None
|
|
|
|
|
|
def test_ensure_artist_never_adds_a_duplicate(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
monkeypatch.setattr(mf, "add_artist", _no_add)
|
|
res = mf.ensure_artist({"artistName": "Michael Jackson"}, "/media/music", False, False)
|
|
assert res["state"] == "existing"
|
|
assert res["artist"] == JACKO
|
|
assert res["name"] == "Michael Jackson"
|
|
|
|
|
|
def test_ensure_artist_adds_when_missing(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [])
|
|
monkeypatch.setattr(mf, "add_artist",
|
|
lambda meta, root, search_all, dry_run: {"id": 9, "artistName": "Overgrown"})
|
|
res = mf.ensure_artist({"artistName": "Overgrown", "foreignArtistId": "og"},
|
|
"/media/music", False, False)
|
|
assert res["state"] == "added"
|
|
assert res["artist"]["id"] == 9
|
|
|
|
|
|
def test_ensure_artist_reports_failure(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [])
|
|
monkeypatch.setattr(mf, "add_artist", lambda *a, **kw: None)
|
|
res = mf.ensure_artist({"artistName": "Overgrown"}, "/media/music", False, False)
|
|
assert res["state"] == "failed"
|
|
assert res["artist"] is None
|
|
|
|
|
|
def test_act_lidarr_artist_does_not_add_existing(monkeypatch, capsys):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
monkeypatch.setattr(mf, "add_artist", _no_add)
|
|
hit = mf.Hit(source="lidarr", kind="artist", title="Michael Jackson",
|
|
artist="Michael Jackson", payload={"artist": {"artistName": "Michael Jackson"}})
|
|
assert mf.act_lidarr_artist(hit, "/media/music", False, False) is True
|
|
assert "already in Lidarr" in capsys.readouterr().out
|
|
|
|
|
|
# --- album matching --------------------------------------------------------
|
|
LIB_ALBUMS = [
|
|
{"id": 11, "title": "Off the Wall", "foreignAlbumId": "otw"},
|
|
{"id": 12, "title": "Thriller (Deluxe Edition)", "foreignAlbumId": "thr-deluxe"},
|
|
]
|
|
|
|
|
|
def test_match_album_prefers_the_musicbrainz_id():
|
|
got = mf._match_album(LIB_ALBUMS, {"title": "Something Else", "foreignAlbumId": "otw"})
|
|
assert got["id"] == 11
|
|
|
|
|
|
def test_match_album_matches_title_ignoring_case_and_punctuation():
|
|
got = mf._match_album(LIB_ALBUMS, {"title": "off the wall!"})
|
|
assert got["id"] == 11
|
|
|
|
|
|
def test_match_album_ignores_an_edition_suffix():
|
|
got = mf._match_album(LIB_ALBUMS, {"title": "Thriller"})
|
|
assert got["id"] == 12
|
|
|
|
|
|
def test_match_album_returns_none_instead_of_a_random_album():
|
|
assert mf._match_album(LIB_ALBUMS, {"title": "Bad"}) is None
|
|
|
|
|
|
def test_ensure_album_reports_missing_album_rather_than_guessing(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
monkeypatch.setattr(mf, "_artist_albums", lambda artist_id: LIB_ALBUMS)
|
|
res = mf.ensure_album({"title": "Bad", "artist": {"artistName": "Michael Jackson"}},
|
|
"/media/music", False, False)
|
|
assert res["album"] is None
|
|
assert res["album_state"] == "missing"
|
|
assert res["artist_state"] == "existing"
|
|
|
|
|
|
def test_ensure_album_monitors_an_unmonitored_album(monkeypatch):
|
|
album = {"id": 11, "title": "Off the Wall", "monitored": False}
|
|
puts = []
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
monkeypatch.setattr(mf, "_artist_albums", lambda artist_id: [album])
|
|
monkeypatch.setattr(mf, "lidarr_put", lambda path, payload, **kw: puts.append(payload) or {})
|
|
res = mf.ensure_album({"title": "Off the Wall", "artist": {"artistName": "Michael Jackson"}},
|
|
"/media/music", False, False)
|
|
assert res["album_state"] == "monitored"
|
|
assert res["album_id"] == 11
|
|
assert puts == [{"albumIds": [11], "monitored": True}]
|
|
|
|
|
|
def test_ensure_album_leaves_a_monitored_album_alone(monkeypatch):
|
|
album = {"id": 11, "title": "Off the Wall", "monitored": True}
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [JACKO])
|
|
monkeypatch.setattr(mf, "_artist_albums", lambda artist_id: [album])
|
|
monkeypatch.setattr(mf, "lidarr_put", lambda *a, **kw: (_ for _ in ()).throw(
|
|
AssertionError("must not re-monitor")))
|
|
res = mf.ensure_album({"title": "Off the Wall", "artist": {"artistName": "Michael Jackson"}},
|
|
"/media/music", False, False)
|
|
assert res["album_state"] == "existing"
|
|
|
|
|
|
# --- releases --------------------------------------------------------------
|
|
def _release(**kw):
|
|
base = {"guid": "g1", "indexerId": 2, "title": "Overgrown-Lay_Cold-WEB-2023",
|
|
"size": 312 * 1024 * 1024, "seeders": 24, "indexer": "Redacted",
|
|
"quality": {"quality": {"name": "FLAC"}}}
|
|
base.update(kw)
|
|
return base
|
|
|
|
|
|
def test_release_label_is_readable():
|
|
label = mf.release_label(_release())
|
|
assert label == "FLAC, 312 MB, 24 seeders, Redacted"
|
|
|
|
|
|
def test_release_label_uses_gigabytes_for_big_files():
|
|
assert "2.0 GB" in mf.release_label(_release(size=2 * 1024 * 1024 * 1024))
|
|
|
|
|
|
def test_release_label_flags_a_rejected_release():
|
|
label = mf.release_label(_release(rejected=True, rejections=["Not a preferred word"]))
|
|
assert "not recommended" in label
|
|
assert "Not a preferred word" in label
|
|
|
|
|
|
def test_release_label_falls_back_to_the_raw_title():
|
|
assert mf.release_label({"title": "Weird.Release"}) == "Weird.Release"
|
|
|
|
|
|
def test_sort_releases_puts_rejected_last_and_best_seeded_first():
|
|
low = _release(guid="low", seeders=2)
|
|
high = _release(guid="high", seeders=90)
|
|
bad = _release(guid="bad", seeders=500, rejected=True)
|
|
order = [r["guid"] for r in mf.sort_releases([low, bad, high])]
|
|
assert order == ["high", "low", "bad"]
|
|
|
|
|
|
def test_list_releases_skips_a_call_without_a_real_album_id():
|
|
assert mf.list_releases(0) == []
|
|
assert mf.list_releases(None) == []
|
|
|
|
|
|
def test_grab_release_posts_the_exact_guid_and_indexer(monkeypatch):
|
|
posted = {}
|
|
monkeypatch.setattr(mf, "lidarr_post",
|
|
lambda path, payload, **kw: posted.update(path=path, payload=payload) or {})
|
|
assert mf.grab_release(_release()) is True
|
|
assert posted["path"] == "/api/v1/release"
|
|
assert posted["payload"] == {"guid": "g1", "indexerId": 2}
|
|
|
|
|
|
def test_grab_release_refuses_an_incomplete_release(monkeypatch):
|
|
monkeypatch.setattr(mf, "lidarr_post", lambda *a, **kw: (_ for _ in ()).throw(
|
|
AssertionError("must not post")))
|
|
assert mf.grab_release({"title": "no guid"}) is False
|
|
|
|
|
|
# --- album dispatch --------------------------------------------------------
|
|
def _album_hit():
|
|
return mf.Hit(source="lidarr", kind="album", title="Lay Cold", album="Lay Cold",
|
|
artist="Overgrown",
|
|
payload={"album": {"title": "Lay Cold",
|
|
"artist": {"artistName": "Overgrown"}}})
|
|
|
|
|
|
def test_act_lidarr_album_grabs_the_best_release(monkeypatch, capsys):
|
|
grabbed = {}
|
|
monkeypatch.setattr(mf, "ensure_album", lambda *a, **kw: {
|
|
"album": {"id": 5}, "album_id": 5, "artist_name": "Overgrown",
|
|
"album_title": "Lay Cold", "artist_state": "existing", "album_state": "existing"})
|
|
monkeypatch.setattr(mf, "list_releases", lambda album_id, **kw: [_release(guid="best")])
|
|
monkeypatch.setattr(mf, "grab_release",
|
|
lambda rel, dry_run=False: grabbed.update(guid=rel["guid"]) or True)
|
|
assert mf.act_lidarr_album(_album_hit(), "/media/music", False, False) is True
|
|
assert grabbed["guid"] == "best"
|
|
out = capsys.readouterr().out
|
|
assert "Overgrown is already in Lidarr." in out
|
|
assert "download client" in out
|
|
|
|
|
|
def test_act_lidarr_album_falls_through_when_no_indexer_has_it(monkeypatch, capsys):
|
|
monkeypatch.setattr(mf, "ensure_album", lambda *a, **kw: {
|
|
"album": {"id": 5}, "album_id": 5, "artist_name": "Overgrown",
|
|
"album_title": "Lay Cold", "artist_state": "added", "album_state": "monitored"})
|
|
monkeypatch.setattr(mf, "list_releases", lambda album_id, **kw: [])
|
|
assert mf.act_lidarr_album(_album_hit(), "/media/music", False, False) is False
|
|
out = capsys.readouterr().out
|
|
assert "Added Overgrown to Lidarr." in out
|
|
assert "Now monitoring" in out
|
|
|
|
|
|
def test_act_lidarr_album_uses_the_picked_release(monkeypatch):
|
|
grabbed = {}
|
|
monkeypatch.setattr(mf, "ensure_album", lambda *a, **kw: {
|
|
"album": {"id": 5}, "album_id": 5, "artist_name": "Overgrown",
|
|
"album_title": "Lay Cold", "artist_state": "existing", "album_state": "existing"})
|
|
monkeypatch.setattr(mf, "list_releases",
|
|
lambda album_id, **kw: [_release(guid="a"), _release(guid="b")])
|
|
monkeypatch.setattr(mf, "pick_release", lambda releases: releases[1])
|
|
monkeypatch.setattr(mf, "grab_release",
|
|
lambda rel, dry_run=False: grabbed.update(guid=rel["guid"]) or True)
|
|
assert mf.act_lidarr_album(_album_hit(), "/media/music", False, False,
|
|
choose_release=True) is True
|
|
assert grabbed["guid"] == "b"
|
|
|
|
|
|
def test_act_lidarr_album_sends_nothing_when_the_user_quits(monkeypatch):
|
|
monkeypatch.setattr(mf, "ensure_album", lambda *a, **kw: {
|
|
"album": {"id": 5}, "album_id": 5, "artist_name": "Overgrown",
|
|
"album_title": "Lay Cold", "artist_state": "existing", "album_state": "existing"})
|
|
monkeypatch.setattr(mf, "list_releases", lambda album_id, **kw: [_release()])
|
|
monkeypatch.setattr(mf, "pick_release", lambda releases: None)
|
|
monkeypatch.setattr(mf, "grab_release", lambda *a, **kw: (_ for _ in ()).throw(
|
|
AssertionError("must not grab")))
|
|
assert mf.act_lidarr_album(_album_hit(), "/media/music", False, False,
|
|
choose_release=True) is True
|
|
|
|
|
|
# --- guards ----------------------------------------------------------------
|
|
def test_norm_title_keeps_accented_letters():
|
|
assert mf._norm_title("Björk") == "björk"
|
|
assert mf._norm_title("AC/DC") == "ac dc"
|
|
|
|
|
|
def test_base_title_of_an_all_bracket_title_matches_nothing():
|
|
assert mf._base_title("(Untitled)") == ""
|
|
assert mf._match_album([{"id": 1, "title": "(Whatever)"}], {"title": "(Untitled)"}) is None
|
|
|
|
|
|
def test_set_album_monitored_refuses_an_album_without_a_library_id(monkeypatch):
|
|
monkeypatch.setattr(mf, "lidarr_put", lambda *a, **kw: (_ for _ in ()).throw(
|
|
AssertionError("must not PUT")))
|
|
assert mf.set_album_monitored({"title": "Lay Cold"}, False) is False
|
|
|
|
|
|
def test_ensure_album_stops_when_lidarr_gives_no_artist_id(monkeypatch):
|
|
monkeypatch.setattr(mf, "library_artists", lambda: [])
|
|
monkeypatch.setattr(mf, "add_artist", lambda *a, **kw: {"artistName": "Overgrown"})
|
|
monkeypatch.setattr(mf, "_artist_albums", lambda artist_id: (_ for _ in ()).throw(
|
|
AssertionError("must not list every album")))
|
|
res = mf.ensure_album({"title": "Lay Cold", "artist": {"artistName": "Overgrown"}},
|
|
"/media/music", False, False)
|
|
assert res["album"] is None
|
|
assert res["album_state"] == "missing"
|