test: add test suite for player, api and widgets
This commit is contained in:
1 parent
237de2efe9
commit
8eb22ea52c
11 files changed
+1069
No files matched your search
@@ -0,0 +1,208 @@
|
||||
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() == []
|
||||
Reference in new issue
Block a user