from __future__ import annotations import html import json import re import pytest from beefy_player import config from beefy_player.models import Song, Station from beefy_player.providers import PROVIDERS, available_providers, get_provider from beefy_player.providers.base import RadioProvider from beefy_player.providers.somafm import SomaFMProvider def rendered_text(app) -> str: svg = html.unescape(app.export_screenshot()).replace("\xa0", " ") return re.sub(r"<[^>]+>", " ", svg) def test_registry_lists_somafm_as_the_default() -> None: assert available_providers() == ("somafm",) assert isinstance(get_provider(), SomaFMProvider) assert get_provider("somafm") is get_provider() # a stale slug in the config file must not stop the app from starting assert get_provider("no-such-catalogue") is get_provider() def test_somafm_provider_describes_its_catalogue() -> None: provider = get_provider() assert provider.name == "SomaFM" assert provider.brand == "SOMA FM" assert provider.station_source == "somafm.com/channels.json" assert "somafm.com" in provider.user_agent stations = provider.fallback_stations() assert len(stations) > 40 station = stations[0] assert station.provider == "somafm" assert station.uid == f"somafm:{station.id}" assert all("somafm.com" in url for url in provider.fallback_urls(station)) class FakeProvider(RadioProvider): """A second catalogue, as small as the interface allows.""" id = "fake" name = "Fake FM" homepage = "fake.test" brand = "FAKE FM" tagline = "not real radio" station_source = "fake.test/stations.json" history_source = "recent.json" def __init__(self) -> None: self.station = Station( id="one", title="Fake One", genre="test", provider=self.id ) async def fetch_stations(self, client): return [self.station] def load_cached_stations(self): return [self.station] def fallback_stations(self): return [self.station] async def fetch_history(self, client, station_id): return [Song(title="Tune", artist="Someone")] def load_cached_history(self, station_id): return [] @pytest.mark.asyncio async def test_app_runs_against_a_different_catalogue(monkeypatch) -> None: """The UI must not know which catalogue it is showing.""" from beefy_player.app import BeefyPlayerApp fake = FakeProvider() monkeypatch.setitem(PROVIDERS, "fake", fake) app = BeefyPlayerApp(provider="fake", autoplay=False, offline=True, audio=False) assert app.provider is fake async with app.run_test(size=(100, 30)) as pilot: await pilot.pause() assert app.current is not None assert app.current.uid == "fake:one" app.query_one("StagePanel").refresh_stage() await pilot.pause() text = rendered_text(app) assert "FAKE ONE" in text assert "Fake FM" in app.title def test_config_remembers_the_provider() -> None: assert config.Config.load().provider == "somafm" config.Config(provider="fake").save() assert config.Config.load().provider == "fake" def test_legacy_config_is_migrated_once(monkeypatch) -> None: legacy = config.LEGACY_CONFIG_PATH legacy.parent.mkdir(parents=True, exist_ok=True) legacy.write_text( json.dumps({"favorites": ["defcon"], "volume": 33, "last_station": "dronezone"}) ) loaded = config.Config.load() assert loaded.favorites == ["defcon"] assert loaded.volume == 33 assert loaded.last_station == "dronezone" assert config.CONFIG_PATH.exists() # the new file wins from then on config.Config(volume=11).save() legacy.write_text(json.dumps({"volume": 99})) assert config.Config.load().volume == 11 def test_environment_variables_prefer_the_beefy_prefix(monkeypatch) -> None: monkeypatch.setenv("BEEFY_IMAGES", "tgp") monkeypatch.setenv("SOMAFM_IMAGES", "0") assert config.env_var("IMAGES") == "tgp" monkeypatch.delenv("BEEFY_IMAGES") assert config.env_var("IMAGES") == "0" # the old spelling still works monkeypatch.delenv("SOMAFM_IMAGES") assert config.env_var("IMAGES") == ""