137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
"""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()]
|