Files
beefy-player/tests/test_api.py
T

209 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import httpx
import pytest
from somafm_tui import api, config
CHANNELS_PAYLOAD = {
"channels": [
{
"id": "groovesalad",
"title": "Groove Salad",
"description": "A nicely chilled plate.",
"dj": "Rusty Hodge",
"genre": "ambient|electronic",
"image": "https://api.somafm.com/logos/120/groovesalad120.png",
"listeners": "1,873",
"lastPlaying": "Jens Buchert - Rauschebart",
"playlists": [
{"url": "https://api.somafm.com/groovesalad32.pls", "format": "aacp", "quality": "low"},
{"url": "https://api.somafm.com/groovesalad256.pls", "format": "mp3", "quality": "highest"},
{"url": "https://api.somafm.com/groovesalad130.pls", "format": "aac", "quality": "highest"},
],
},
{
"id": "bad",
"title": "No playlists",
"genre": "test",
},
{"title": "missing id"},
]
}
def test_parse_channels() -> None:
stations = api.parse_channels(CHANNELS_PAYLOAD)
assert len(stations) == 2
station = stations[0]
assert station.id == "groovesalad"
assert station.title == "Groove Salad"
assert station.listeners == 1873
assert station.genre_label == "ambient · electronic"
assert station.favorite is False
assert station.now_playing is not None
assert station.now_playing.artist == "Jens Buchert"
assert station.now_playing.title == "Rauschebart"
assert station.format_label == "mp3 · 256k"
assert next(pl["url"] for pl in station.sorted_playlists()).endswith("256.pls")
assert stations[1].id == "bad"
def test_parse_last_playing() -> None:
song = api.parse_last_playing("Jens Buchert - Rauschebart")
assert song is not None and song.artist == "Jens Buchert" and song.title == "Rauschebart"
en_dash = api.parse_last_playing("Tosca – Amber November")
assert en_dash is not None and en_dash.artist == "Tosca"
plain = api.parse_last_playing("Just A Title")
assert plain is not None and plain.title == "Just A Title" and plain.artist == ""
assert api.parse_last_playing("") is None
assert api.parse_last_playing(None) is None
def test_parse_pls() -> None:
text = """[playlist]
NumberOfEntries=2
File1=https://ice2.somafm.com/groovesalad-256-mp3
File2=https://ice6.somafm.com/groovesalad-256-mp3
Version=2
"""
assert api.parse_pls(text) == [
"https://ice2.somafm.com/groovesalad-256-mp3",
"https://ice6.somafm.com/groovesalad-256-mp3",
]
assert api.parse_pls("https://ice1.somafm.com/x-128-mp3\n# comment\n") == [
"https://ice1.somafm.com/x-128-mp3"
]
assert api.is_playlist("https://x/y.pls?foo=1") is True
assert api.is_direct_stream("https://ice1.somafm.com/x-128-mp3") is True
assert api.is_direct_stream("https://x/y.m3u") is False
def test_parse_songs_and_dates() -> None:
payload = {
"id": "groovesalad",
"songs": [
{
"title": "Rauschebart",
"artist": "Jens Buchert",
"album": "Gravity",
"albumArt": "https://example.com/art.jpg",
"date": "1793312765",
},
{
"title": "Room 505",
"artist": "Alex Cortiz",
"album": "The Chill Out Room",
"albumArt": "",
"date": 1793312438,
},
],
}
songs = api.parse_songs(payload)
assert len(songs) == 2
assert songs[0].title == "Rauschebart"
assert songs[0].art_url == "https://example.com/art.jpg"
assert songs[0].played_at is not None and songs[0].played_at.year == 2026
# newest first
assert songs[0].played_at > songs[1].played_at # type: ignore[operator]
assert api.parse_songs({"songs": [{"title": ""}]}) == []
def test_history_cache_roundtrip() -> None:
path = api._songs_cache_path("groovesalad")
config.write_json(
path,
{
"fetched": 1,
"songs": [
{
"title": "Eva",
"artist": "Heights Of Abraham",
"album": "Pork",
"albumArt": "",
"date": "2026-10-29T22:00:00+00:00",
}
],
},
)
songs = api.load_cached_songs("groovesalad")
assert len(songs) == 1
assert songs[0].artist == "Heights Of Abraham"
assert api.load_cached_songs("nope") == []
@pytest.mark.asyncio
async def test_stream_candidates_with_mock_transport() -> None:
pls = "[playlist]\nFile1=https://ice2.somafm.com/x-256-mp3\nFile2=https://ice6.somafm.com/x-256-mp3\n"
requests: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(str(request.url))
return httpx.Response(200, text=pls)
station = api.parse_channels(CHANNELS_PAYLOAD)[0]
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
urls = await api.stream_candidates(client, station)
assert urls[:2] == [
"https://ice2.somafm.com/x-256-mp3",
"https://ice6.somafm.com/x-256-mp3",
]
assert len(requests) == 3 # three playlist URLs, cached by URL
# second run hits the playlist cache
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
await api.stream_candidates(client, station)
assert len(requests) == 3
@pytest.mark.asyncio
async def test_fetch_channels_and_history_cache() -> None:
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/channels.json":
return httpx.Response(200, json=CHANNELS_PAYLOAD)
if request.url.path.endswith(".json"):
return httpx.Response(
200,
json={
"songs": [
{"title": "T", "artist": "A", "album": "B", "date": "1793312765"}
]
},
)
return httpx.Response(404)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
stations = await api.fetch_channels(client)
assert len(stations) == 2
assert config.CHANNELS_CACHE.exists()
assert len(api.load_cached_channels()) == 2
songs = await api.fetch_history(client, "groovesalad")
assert len(songs) == 1
assert api.load_cached_songs("groovesalad")[0].title == "T"
# a failing request still returns the cached history
def failing(request: httpx.Request) -> httpx.Response:
return httpx.Response(500)
async with httpx.AsyncClient(transport=httpx.MockTransport(failing)) as client:
songs = await api.fetch_history(client, "groovesalad")
assert songs[0].title == "T"
def test_config_roundtrip() -> None:
config.Config(favorites=["a"], volume=42, visualizer="wave", autoplay=True).save()
loaded = config.Config.load()
assert loaded.favorites == ["a"]
assert loaded.volume == 42
assert loaded.visualizer == "wave"
assert loaded.autoplay is True
assert loaded.is_favorite("a") is True
assert loaded.toggle_favorite("a") is False
assert loaded.toggle_favorite("b") is True
assert loaded.favorites == ["b"]
def test_channel_cache_missing() -> None:
assert api.load_cached_channels() == []