Files
beefy-player/tests/test_bandcamp.py
T
beefcarver e30ca09627 refactor!: rename somafm-tui to beefy-player
The app is now a general internet-radio player rather than a SomaFM client, so the package, console script, MPRIS identity, mpv namespaces, macOS tap names, XDG directories and docs all move to beefy-player.

Environment variables are now BEEFY_* (the old SOMAFM_* spellings are still honoured) and an existing ~/.config/somafm-tui/config.json is migrated on first run, so favourites and volume survive the rename.

The visible interface is untouched: same layout, same logo, same keys.
2026-11-03 16:10:25 -05:00

276 lines
9.1 KiB
Python

from __future__ import annotations
import json
import httpx
import pytest
from beefy_player import bandcamp
TRACK_PAYLOAD = {
"auto": {
"results": [
{
"type": "t",
"name": "Rauschebart",
"band_name": "Jens Buchert",
"album_name": "Gravity",
"item_url_root": "https://jensbuchert.bandcamp.com",
"item_url_path": (
"https://jensbuchert.bandcamp.com/track/rauschebart?from=search"
),
}
]
}
}
ALBUM_PAYLOAD = {
"auto": {
"results": [
{
"type": "a",
"name": "Gravity",
"band_name": "Jens Buchert",
"item_url_root": "https://jensbuchert.bandcamp.com",
"item_url_path": (
"https://jensbuchert.bandcamp.com/album/gravity?from=search"
),
}
]
}
}
ALBUM_PAGE_HTML = """
<html><body>
<div data-tralbum='{"trackinfo": [
{"title": "Rauschebart", "title_link": "/track/rauschebart?from=album", "track_id": 1},
{"title": "Other Track", "title_link": "/track/other-track", "track_id": 2}
]}'></div>
</body></html>
"""
def test_parse_search_payload() -> None:
results = bandcamp.parse_search_payload(TRACK_PAYLOAD)
assert len(results) == 1
result = results[0]
assert result.title == "Rauschebart"
assert result.artist == "Jens Buchert"
assert result.album == "Gravity"
assert result.item_type == "track"
# Tracking parameters are removed.
assert result.url == "https://jensbuchert.bandcamp.com/track/rauschebart"
def test_parse_search_payload_accepts_custom_domains_and_skips_other_types() -> None:
payload = {
"auto": {
"results": [
{
"type": "t",
"name": "Rauschebart",
"band_name": "Jens Buchert",
"album_name": "Gravity",
"item_url_path": "https://music.example.com/track/rauschebart",
},
{
"type": "b",
"name": "Jens Buchert",
"item_url_root": "https://jensbuchert.bandcamp.com",
},
{"type": "t", "name": "", "item_url_path": "https://x.test/track/x"},
]
}
}
results = bandcamp.parse_search_payload(payload)
assert [result.url for result in results] == [
"https://music.example.com/track/rauschebart"
]
def test_parse_album_tracks_from_tralbum() -> None:
results = bandcamp.parse_album_tracks(
ALBUM_PAGE_HTML, "https://jensbuchert.bandcamp.com/album/gravity"
)
assert [entry.title for entry in results] == ["Rauschebart", "Other Track"]
assert results[0].url == "https://jensbuchert.bandcamp.com/track/rauschebart"
def test_parse_album_tracks_from_json_ld_and_anchor_fallback() -> None:
json_ld = """
<script type="application/ld+json">
{"@type": "MusicAlbum", "track": [
{"@type": "MusicRecording", "name": "Rauschebart",
"url": "https://jensbuchert.bandcamp.com/track/rauschebart"}
]}
</script>
"""
results = bandcamp.parse_album_tracks(
json_ld, "https://jensbuchert.bandcamp.com/album/gravity"
)
assert results[0].url == "https://jensbuchert.bandcamp.com/track/rauschebart"
anchors = """
<html><body>
<a href="/track/rauschebart">Rauschebart</a>
<a href="/album/gravity">Gravity</a>
</body></html>
"""
results = bandcamp.parse_album_tracks(
anchors, "https://jensbuchert.bandcamp.com/album/gravity"
)
assert [entry.title for entry in results] == ["Rauschebart"]
def test_pick_track_and_album_ignore_mismatches() -> None:
results = bandcamp.parse_search_payload(
{
"auto": {
"results": [
{
"type": "a",
"name": "Gravity",
"band_name": "Jens Buchert",
"item_url_path": "https://x.bandcamp.com/album/gravity",
},
{
"type": "t",
"name": "Rauschebart",
"band_name": "Someone Else",
"album_name": "Other",
"item_url_path": "https://other.bandcamp.com/track/rauschebart",
},
]
}
}
)
assert bandcamp.pick_track(results, "Jens Buchert", "Gravity", "Rauschebart") is None
assert bandcamp.pick_album(results, "Jens Buchert", "Gravity") == (
"https://x.bandcamp.com/album/gravity"
)
assert bandcamp.pick_album(results, "Someone Else", "Gravity") is None
def test_build_query_normalises_for_autocomplete() -> None:
assert bandcamp.build_query(
" Diskette Park ", "Dreams — [Extended Mix] (Live)!"
) == "diskette park dreams extended mix live!"
assert bandcamp.build_query("Don't Stop", "AC/DC") == "dont stop acdc"
def test_build_prefixes_grows_in_small_typing_groups() -> None:
assert bandcamp.build_prefixes("diskette park dreams") == [
"dis",
"disket",
"diskette ",
"diskette par",
"diskette park d",
"diskette park dreams",
]
assert bandcamp.build_prefixes("abcde") == ["abcde"]
assert bandcamp.build_prefixes("") == []
def test_matches_is_case_and_whitespace_insensitive() -> None:
assert bandcamp._matches("Jens Buchert", "jens buchert")
assert bandcamp._matches("Rauschebart", "RAUSCHEBART (Live)")
assert not bandcamp._matches("Dreams", "Something Else")
@pytest.mark.asyncio
async def test_find_track_uses_track_search_first() -> None:
requests: list[httpx.Request] = []
match_text = "jens buchert ra"
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
assert request.method == "POST"
body = json.loads(request.content)
assert body["search_filter"] == "t"
assert body["full_page"] is True
assert body["fan_id"] is None
payload = TRACK_PAYLOAD if body["search_text"] == match_text else {
"auto": {"results": []}
}
return httpx.Response(200, json=payload)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
url = await bandcamp.find_track(client, "Jens Buchert", "Gravity", "Rauschebart")
assert url == "https://jensbuchert.bandcamp.com/track/rauschebart"
assert [json.loads(request.content)["search_text"] for request in requests] == [
"jen",
"jens b",
"jens buch",
"jens buchert",
"jens buchert ra",
]
@pytest.mark.asyncio
async def test_find_track_falls_back_to_album_then_track_on_page() -> None:
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
if request.url.path == "/api/bcsearch_public_api/1/autocomplete_elastic":
body = json.loads(request.content)
if body["search_filter"] == "t":
return httpx.Response(200, json={"auto": {"results": []}})
if body["search_filter"] == "a":
payload = (
ALBUM_PAYLOAD
if body["search_text"] == "jens buchert"
else {"auto": {"results": []}}
)
return httpx.Response(200, json=payload)
if request.url.path == "/album/gravity":
return httpx.Response(200, text=ALBUM_PAGE_HTML)
return httpx.Response(404)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
url = await bandcamp.find_track(
client, "Jens Buchert", "Gravity", "Rauschebart"
)
assert url == "https://jensbuchert.bandcamp.com/track/rauschebart"
search_bodies = [
json.loads(request.content)
for request in requests
if request.method == "POST"
]
assert [body["search_text"] for body in search_bodies] == [
"jen",
"jens b",
"jens buch",
"jens buchert",
"jens buchert ra",
"jens buchert rausc",
"jens buchert rauscheb",
"jens buchert rauschebart",
"jen",
"jens b",
"jens buch",
"jens buchert",
]
assert [body["search_filter"] for body in search_bodies] == ["t"] * 8 + [
"a"
] * 4
assert requests[-1].method == "GET"
assert requests[-1].url.path == "/album/gravity"
@pytest.mark.asyncio
async def test_find_track_returns_none_for_challenge_or_no_results() -> None:
requests: list[httpx.Request] = []
def challenge(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, text="<html><title>Client Challenge</title></html>")
async with httpx.AsyncClient(transport=httpx.MockTransport(challenge)) as client:
assert await bandcamp.find_track(client, "A", "B", "C") is None
assert len(requests) == len(bandcamp.build_prefixes("a c")) + len(
bandcamp.build_prefixes("a b")
)