"""Radio Paradise: five hand-curated mixes, listener-supported since 2000. Unlike the community stations, Radio Paradise publishes a proper now-playing API for every mix, with album and cover art — so this provider is a small catalogue with real track metadata rather than a single relay. """ from __future__ import annotations import re import time from datetime import datetime, timezone from pathlib import Path import httpx from ... import config from ...models import Song, Station from ..base import RadioProvider from ..wordmark import render from . import api __all__ = ["RadioParadiseProvider"] #: How many tracks LAST PLAYED remembers per mix. HISTORY_LIMIT = 20 class RadioParadiseProvider(RadioProvider): id = "radioparadise" name = "Radio Paradise" homepage = "radioparadise.com" brand = "RADIO PARADISE" tagline = "listener-supported · curated mixes" station_source = "api.radioparadise.com/now_playing" history_source = "api.radioparadise.com (rolling)" hero = render("PARADISE") hero_lines = ("RADIO PARADISE", "LISTENER SUPPORTED", "PARADISE, CALIFORNIA") @property def user_agent(self) -> str: return f"{config.USER_AGENT}; +https://radioparadise.com" def stations(self) -> list[Station]: return [ Station( id=station_id, title=title, description=f"Radio Paradise — {title}", genre=genre, playlists=api.playlists(names), provider=self.id, ) for station_id, _chan, title, genre, names in api.CHANNELS ] async def fetch_stations(self, client: httpx.AsyncClient) -> list[Station]: return self.stations() def load_cached_stations(self) -> list[Station]: return self.stations() def fallback_stations(self) -> list[Station]: return self.stations() def _songs_cache(self, station_id: str) -> Path: safe = re.sub(r"[^A-Za-z0-9_.-]", "_", station_id or self.id) return config.SONGS_CACHE_DIR / f"{self.id}-{safe}.json" async def fetch_history( self, client: httpx.AsyncClient, station_id: str ) -> list[Song]: chan = api.CHANNEL_BY_STATION.get(station_id) if chan is None: return self.load_cached_history(station_id) current = await api.fetch_now(client, chan) if current is None: return self.load_cached_history(station_id) history = self.load_cached_history(station_id) if history and history[0].key() == current.key(): current.played_at = history[0].played_at history = [current, *history[1:]] else: current.played_at = datetime.now(timezone.utc) history = [current, *[song for song in history if song.key() != current.key()]] history = history[:HISTORY_LIMIT] config.write_json( self._songs_cache(station_id), { "fetched": time.time(), "songs": [ { "title": song.title, "artist": song.artist, "album": song.album, "art": song.art_url, "played_at": song.played_at.isoformat() if song.played_at else "", } for song in history ], }, ) return history def load_cached_history(self, station_id: str) -> list[Song]: raw = config.read_json(self._songs_cache(station_id)) entries = raw.get("songs") if isinstance(raw, dict) else None if not isinstance(entries, list): return [] songs: list[Song] = [] for entry in entries: if not isinstance(entry, dict): continue song = Song( title=str(entry.get("title") or "").strip(), artist=str(entry.get("artist") or "").strip(), album=str(entry.get("album") or "").strip(), art_url=str(entry.get("art") or "").strip(), played_at=None, ) if isinstance(entry.get("played_at"), str) and entry["played_at"]: from ...models import parse_date song.played_at = parse_date(entry["played_at"]) if song.title or song.artist: songs.append(song) return songs def fallback_urls(self, station: Station) -> list[str]: return [entry["url"] for entry in station.sorted_playlists()]