"""The community-station providers: relays, status documents and schedules.""" from __future__ import annotations from datetime import datetime, timedelta, timezone import httpx import pytest from beefy_player.providers import PROVIDERS from beefy_player.providers.community import ( AnonradioProvider, CashmereProvider, DatafruitsProvider, DublabProvider, LotProvider, LylProvider, RefugeProvider, RinseProvider, dublab, ) ICECAST = { "icestats": { "source": [ { "listenurl": "https://anonradio.net:8443/anonradio", "bitrate": 192, "server_type": "audio/mpeg", "listeners": 13, "title": "repeating screwtape from 29-Nov-2023", } ] } } RADIO_CO = { "status": "online", "listeners": 41, "current_track": { "title": "Guest Show (r) - Ain Autavg", "start_time": "2026-11-04T00:00:02+00:00", "artwork_url": "https://images.radio.co/station.png", }, } def client_for(payload, status_code: int = 200) -> httpx.AsyncClient: def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(status_code, json=payload) return httpx.AsyncClient(transport=httpx.MockTransport(handler)) def test_every_community_provider_builds_a_playable_station() -> None: providers = ( AnonradioProvider, DatafruitsProvider, CashmereProvider, LylProvider, LotProvider, RefugeProvider, DublabProvider, RinseProvider, ) for cls in providers: provider = cls() stations = provider.fallback_stations() assert stations, cls.id for station in stations: assert station.provider == provider.id assert station.playlists, (cls.id, station.id) assert all(url.startswith(("http://", "https://")) for url in provider.fallback_urls(station)) # every one of them spells its name in the same font assert len(provider.hero) == 3, cls.id def test_rinse_is_a_family_of_three_stations() -> None: stations = RinseProvider().fallback_stations() assert [station.id for station in stations] == ["rinse_uk", "kool", "swu"] assert [station.title for station in stations] == ["Rinse UK", "Kool London", "SWU"] for station in stations: assert "admin.stream.rinse.fm/proxy/" in station.playlists[0]["url"] assert station.format_label.endswith("128k") def test_the_lot_streams_hls_which_the_player_owns() -> None: from beefy_player.providers.base import is_direct_stream, is_playlist url = LotProvider().fallback_urls(LotProvider().station())[0] assert url.endswith(".m3u8") assert is_playlist(url) is False assert is_direct_stream(url) is True @pytest.mark.asyncio async def test_icecast_status_gives_the_station_what_is_on() -> None: provider = AnonradioProvider() async with client_for(ICECAST) as client: station = (await provider.fetch_stations(client))[0] assert station.listeners == 13 assert station.now_playing is not None assert station.now_playing.title == "repeating screwtape from 29-Nov-2023" @pytest.mark.asyncio async def test_radio_co_status_gives_a_track_and_its_art() -> None: provider = RefugeProvider() async with client_for(RADIO_CO) as client: station = (await provider.fetch_stations(client))[0] history = await provider.fetch_history(client, "refuge") assert station.listeners == 41 assert station.now_playing is not None assert station.now_playing.album == "" assert station.now_playing.art_url.endswith("station.png") assert history[0].title == "Ain Autavg" # the show's own start time is used, not "just now" assert history[0].played_at == datetime( 2026, 11, 4, 0, 0, 2, tzinfo=timezone.utc ) @pytest.mark.asyncio async def test_history_is_rolling_and_keeps_the_first_sighting() -> None: provider = AnonradioProvider() async with client_for(ICECAST) as client: first = await provider.fetch_history(client, "anonradio") again = await provider.fetch_history(client, "anonradio") assert len(first) == 1 assert again[0].played_at == first[0].played_at @pytest.mark.asyncio async def test_a_station_without_metadata_still_carries_on() -> None: """Rinse, LYL, The Lot and Cashmere publish no status document.""" provider = RinseProvider() async with client_for({}, status_code=500) as client: stations = await provider.fetch_stations(client) history = await provider.fetch_history(client, "rinse_uk") assert len(stations) == 3 assert all(station.now_playing is None for station in stations) assert history == [] def test_dublab_schedule_parses_the_site_calendar() -> None: payload = [ { "startTime": "2026-11-03T16:00:00-08:00", "endTime": "2026-11-03T18:00:00-08:00", "summary": "LIVE // Version Sounds", }, { "startTime": "2026-11-03T18:00:00-08:00", "endTime": "2026-11-03T20:00:00-08:00", "summary": "PRE-REC // Sounds of Now", }, ] shows = dublab.parse_schedule(payload) assert [show.name for show in shows] == [ "LIVE // Version Sounds", "PRE-REC // Sounds of Now", ] assert shows[0].start == datetime( 2026, 11, 3, 16, tzinfo=timezone(timedelta(hours=-8)) ) assert dublab.parse_schedule({"nope": True}) == [] def test_the_running_slot_is_marked_live() -> None: provider = DublabProvider() now = datetime.now(timezone.utc) running = dublab.Broadcast( name="LIVE // Version Sounds", start=now - timedelta(minutes=30), end=now + timedelta(minutes=30), ) later = dublab.Broadcast( name="Later Show", start=now + timedelta(hours=1), end=now + timedelta(hours=2), ) marked = provider._mark_live([running, later]) assert [item.live for item in marked] == [True, False] assert marked[0].key() == running.key() @pytest.mark.asyncio async def test_dublab_history_follows_the_schedule() -> None: provider = DublabProvider() now = datetime.now(timezone.utc) payload = [ { "startTime": (now - timedelta(minutes=20)).isoformat(), "endTime": (now + timedelta(minutes=40)).isoformat(), "summary": "LIVE // Version Sounds", } ] def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json=payload) async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: await provider.fetch_schedule(client) history = await provider.fetch_history(client, "dublab") assert history and history[0].title == "LIVE // Version Sounds" def test_every_catalogue_is_registered_under_its_slug() -> None: assert set(PROVIDERS) == { "somafm", "tilderadio", "bassdrive", "radioparadise", "dublab", "rinse", "refuge", "lyl", "lot", "radiobrowser", "cashmere", "datafruits", "anonradio", }