feat(providers): add nine catalogues and the Radio Browser directory
This commit is contained in:
1 parent
a3ad693132
commit
49f7efe0d2
27 files changed
+1695
-22
No files matched your search
@@ -0,0 +1,29 @@
|
||||
"""Independent stations: one relay each, with the metadata the relay offers.
|
||||
|
||||
Each module here is a thin subclass of
|
||||
:class:`beefy_player.providers.community.base.CommunityProvider`, which knows
|
||||
how to read an Icecast status document or a radio.co status, keep a rolling
|
||||
history and (for Dublab) follow a published schedule.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .anonradio import AnonradioProvider
|
||||
from .cashmere import CashmereProvider
|
||||
from .datafruits import DatafruitsProvider
|
||||
from .dublab import DublabProvider
|
||||
from .lot import LotProvider
|
||||
from .lyl import LylProvider
|
||||
from .refuge import RefugeProvider
|
||||
from .rinse import RinseProvider
|
||||
|
||||
__all__ = [
|
||||
"AnonradioProvider",
|
||||
"CashmereProvider",
|
||||
"DatafruitsProvider",
|
||||
"DublabProvider",
|
||||
"LotProvider",
|
||||
"LylProvider",
|
||||
"RefugeProvider",
|
||||
"RinseProvider",
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
"""aNONradio: SDF's community station, on Icecast since the dial was young."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class AnonradioProvider(CommunityProvider):
|
||||
id = "anonradio"
|
||||
name = "aNONradio"
|
||||
homepage = "anonradio.net"
|
||||
brand = "aNONradio"
|
||||
tagline = "SDF community radio"
|
||||
station_source = "anonradio.net icecast"
|
||||
history_source = "icecast title (rolling)"
|
||||
|
||||
station_title = "aNONradio"
|
||||
station_description = (
|
||||
"community station of the SDF public-access UNIX system"
|
||||
)
|
||||
station_genre = "eclectic|talk"
|
||||
streams = (("https://anonradio.net:8443/anonradio", "mp3", "high", "192"),)
|
||||
status_url = "https://anonradio.net:8443/status-json.xsl"
|
||||
|
||||
wordmark = "ANONRADIO"
|
||||
hero_lines = ("SDF COMMUNITY RADIO", "PUBLIC ACCESS UNIX")
|
||||
@@ -0,0 +1,336 @@
|
||||
"""Community stations: one relay each, sometimes with a status document.
|
||||
|
||||
Most independent stations are a single stream on Icecast, radio.co or Airtime,
|
||||
plus — if you are lucky — a status document that names what is on. This module
|
||||
turns that into a provider: the station, its relays, and a rolling history of
|
||||
whatever the relay has announced while you were listening.
|
||||
|
||||
A subclass only carries data. Stations with a schedule or a multi-station
|
||||
catalogue (Dublab, Rinse, Radio Paradise) override a hook or two.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from ... import config
|
||||
from ...models import Broadcast, Song, Station, parse_date, parse_last_playing, to_int
|
||||
from ..base import RadioProvider, fetch_json
|
||||
from ..wordmark import render
|
||||
|
||||
#: ``public.radio.co`` status document: keyless, no station id in the body.
|
||||
RADIO_CO_STATUS = "https://public.radio.co/stations/{station}/status"
|
||||
|
||||
#: How many shows/tracks LAST PLAYED remembers.
|
||||
HISTORY_LIMIT = 20
|
||||
|
||||
|
||||
class CommunityProvider(RadioProvider):
|
||||
"""A catalogue that broadcasts one stream (or a fixed handful)."""
|
||||
|
||||
# -- catalogue data (set by each station module) ---------------------- #
|
||||
|
||||
station_title = ""
|
||||
station_description = ""
|
||||
station_genre = "eclectic"
|
||||
station_image = ""
|
||||
|
||||
#: ``(url, format, quality, bitrate)`` — the relays, best first.
|
||||
streams: tuple[tuple[str, str, str, str], ...] = ()
|
||||
#: Keyless Icecast ``status-json.xsl``, when the relay publishes one.
|
||||
status_url = ""
|
||||
#: ``public.radio.co`` station id, when the relay is radio.co.
|
||||
radio_co_id = ""
|
||||
#: Wordmark the header spells; empty leaves the provider without a hero.
|
||||
wordmark = ""
|
||||
|
||||
# Independent stations are not Bandcamp catalogues.
|
||||
bandcamp = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
if self.wordmark:
|
||||
self.hero = render(self.wordmark)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# station
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _playlists(self) -> list[dict[str, str]]:
|
||||
return [
|
||||
{"url": url, "format": fmt, "quality": quality, "bitrate": bitrate}
|
||||
for url, fmt, quality, bitrate in self.streams
|
||||
]
|
||||
|
||||
def station(self) -> Station:
|
||||
return Station(
|
||||
id=self.id,
|
||||
title=self.station_title or self.name,
|
||||
description=self.station_description or self.tagline,
|
||||
genre=self.station_genre,
|
||||
image=self.station_image,
|
||||
playlists=self._playlists(),
|
||||
provider=self.id,
|
||||
)
|
||||
|
||||
def stations(self) -> list[Station]:
|
||||
return [self.station()]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# what the relay says is on
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@property
|
||||
def has_status(self) -> bool:
|
||||
return bool(self.status_url or self.radio_co_id)
|
||||
|
||||
async def _status(self, client: httpx.AsyncClient, attempts: int = 2) -> Any:
|
||||
url = (
|
||||
RADIO_CO_STATUS.format(station=self.radio_co_id)
|
||||
if self.radio_co_id
|
||||
else self.status_url
|
||||
)
|
||||
return await fetch_json(client, url, attempts=attempts)
|
||||
|
||||
def _listeners(self, payload: Any) -> int:
|
||||
if not isinstance(payload, dict):
|
||||
return 0
|
||||
if self.radio_co_id:
|
||||
return to_int(payload.get("listeners"))
|
||||
stats = payload.get("icestats")
|
||||
source = stats.get("source") if isinstance(stats, dict) else None
|
||||
sources = (
|
||||
source
|
||||
if isinstance(source, list)
|
||||
else [source]
|
||||
if isinstance(source, dict)
|
||||
else []
|
||||
)
|
||||
return sum(
|
||||
to_int(item.get("listeners")) for item in sources if isinstance(item, dict)
|
||||
)
|
||||
|
||||
def _icecast_title(self, payload: Any) -> str:
|
||||
stats = payload.get("icestats") if isinstance(payload, dict) else None
|
||||
source = stats.get("source") if isinstance(stats, dict) else None
|
||||
sources = (
|
||||
source
|
||||
if isinstance(source, list)
|
||||
else [source]
|
||||
if isinstance(source, dict)
|
||||
else []
|
||||
)
|
||||
for item in sources:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
for key in ("display-title", "title", "yp_currently_playing"):
|
||||
value = item.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return ""
|
||||
|
||||
def _now(self, payload: Any) -> Song | None:
|
||||
"""The relay's current track/show, as a :class:`Song`."""
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
if self.radio_co_id:
|
||||
track = payload.get("current_track")
|
||||
if not isinstance(track, dict):
|
||||
return None
|
||||
title = str(track.get("title") or "").strip()
|
||||
if not title:
|
||||
return None
|
||||
song = parse_last_playing(title) or Song(title=title)
|
||||
song.art_url = str(track.get("artwork_url") or "").strip()
|
||||
song.played_at = parse_date(track.get("start_time"))
|
||||
return song
|
||||
|
||||
title = self._icecast_title(payload)
|
||||
if not title:
|
||||
return None
|
||||
return parse_last_playing(title)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# the schedule, when the catalogue has one (Dublab)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _scheduled_now(self) -> Broadcast | None:
|
||||
now = datetime.now(timezone.utc)
|
||||
for entry in self.load_cached_schedule():
|
||||
if entry.start is None or entry.end is None:
|
||||
continue
|
||||
if entry.start <= now < entry.end:
|
||||
return entry
|
||||
return None
|
||||
|
||||
def _mark_live(self, entries: list[Broadcast]) -> list[Broadcast]:
|
||||
"""Flag the slot that is running now, so the pane can say ON AIR."""
|
||||
now = datetime.now(timezone.utc)
|
||||
marked: list[Broadcast] = []
|
||||
for entry in entries:
|
||||
if (
|
||||
entry.start is not None
|
||||
and entry.end is not None
|
||||
and entry.start <= now < entry.end
|
||||
):
|
||||
marked.append(
|
||||
Broadcast(
|
||||
name=entry.name,
|
||||
dj=entry.dj,
|
||||
location=entry.location,
|
||||
start=entry.start,
|
||||
end=entry.end,
|
||||
live=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
marked.append(entry)
|
||||
return marked
|
||||
|
||||
def _scheduled_song(self) -> Song | None:
|
||||
entry = self._scheduled_now()
|
||||
if entry is None:
|
||||
return None
|
||||
return Song(artist=self.name, title=entry.name)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# caching
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _station_cache(self) -> Path:
|
||||
return config.channels_cache_path(self.id)
|
||||
|
||||
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"
|
||||
|
||||
def _songs_to_cache(self, songs: list[Song]) -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
"title": song.title,
|
||||
"artist": song.artist,
|
||||
"art": song.art_url,
|
||||
"played_at": song.played_at.isoformat() if song.played_at else "",
|
||||
}
|
||||
for song in songs
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# RadioProvider
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
async def fetch_stations(self, client: httpx.AsyncClient) -> list[Station]:
|
||||
stations = self.stations()
|
||||
payload: Any = None
|
||||
if self.has_status:
|
||||
try:
|
||||
payload = await self._status(client)
|
||||
except RuntimeError:
|
||||
payload = None
|
||||
if payload is None:
|
||||
payload = self._cached_status()
|
||||
else:
|
||||
config.write_json(
|
||||
self._station_cache(), {"fetched": time.time(), "status": payload}
|
||||
)
|
||||
|
||||
song = self._now(payload) if payload is not None else None
|
||||
if song is None:
|
||||
song = self._scheduled_song()
|
||||
listeners = self._listeners(payload) if payload is not None else 0
|
||||
entry = self._scheduled_now()
|
||||
|
||||
for station in stations:
|
||||
if song is not None:
|
||||
station.now_playing = song
|
||||
station.last_playing = (
|
||||
f"{song.artist} - {song.title}" if song.artist else song.title
|
||||
)
|
||||
if listeners:
|
||||
station.listeners = listeners
|
||||
if entry is not None:
|
||||
station.presenter = (
|
||||
f"live from {entry.location}" if entry.location else "live now"
|
||||
)
|
||||
return stations
|
||||
|
||||
def _cached_status(self) -> Any:
|
||||
raw = config.read_json(self._station_cache())
|
||||
return raw.get("status") if isinstance(raw, dict) else None
|
||||
|
||||
def load_cached_stations(self) -> list[Station]:
|
||||
stations = self.stations()
|
||||
payload = self._cached_status()
|
||||
song = self._now(payload) if payload is not None else None
|
||||
if song is None:
|
||||
song = self._scheduled_song()
|
||||
listeners = self._listeners(payload) if payload is not None else 0
|
||||
for station in stations:
|
||||
if song is not None:
|
||||
station.now_playing = song
|
||||
if listeners:
|
||||
station.listeners = listeners
|
||||
return stations
|
||||
|
||||
def fallback_stations(self) -> list[Station]:
|
||||
return self.stations()
|
||||
|
||||
async def fetch_history(
|
||||
self, client: httpx.AsyncClient, station_id: str
|
||||
) -> list[Song]:
|
||||
current: Song | None = None
|
||||
if self.has_status:
|
||||
try:
|
||||
payload = await self._status(client, attempts=1)
|
||||
except RuntimeError:
|
||||
payload = None
|
||||
if payload is not None:
|
||||
current = self._now(payload)
|
||||
if current is None:
|
||||
current = self._scheduled_song()
|
||||
if current is None:
|
||||
return self.load_cached_history(station_id)
|
||||
|
||||
history = self.load_cached_history(station_id)
|
||||
now = datetime.now(timezone.utc)
|
||||
if history and history[0].key() == current.key():
|
||||
# Same show or track: keep the time we first heard it.
|
||||
current.played_at = history[0].played_at
|
||||
history = [current, *history[1:]]
|
||||
else:
|
||||
current.played_at = current.played_at or now
|
||||
history = [current, *[s for s in history if s.key() != current.key()]]
|
||||
history = history[:HISTORY_LIMIT]
|
||||
|
||||
config.write_json(
|
||||
self._songs_cache(station_id),
|
||||
{"fetched": time.time(), "songs": self._songs_to_cache(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(),
|
||||
art_url=str(entry.get("art") or "").strip(),
|
||||
played_at=parse_date(entry.get("played_at")),
|
||||
)
|
||||
if song.title or song.artist:
|
||||
songs.append(song)
|
||||
return songs
|
||||
|
||||
def fallback_urls(self, station: Station) -> list[str]:
|
||||
return [url for url, _format, _quality, _bitrate in self.streams]
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Cashmere Radio: Berlin's experimental station, on an Airtime relay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class CashmereProvider(CommunityProvider):
|
||||
id = "cashmere"
|
||||
name = "Cashmere Radio"
|
||||
homepage = "cashmereradio.com"
|
||||
brand = "CASHMERE"
|
||||
tagline = "experimental radio · berlin"
|
||||
station_source = "cashmereradio.com"
|
||||
history_source = ""
|
||||
|
||||
station_title = "Cashmere Radio"
|
||||
station_description = "experimental radio from Berlin"
|
||||
station_genre = "experimental|eclectic"
|
||||
streams = (
|
||||
("https://cashmereradio.out.airtime.pro/cashmereradio_a", "ogg", "high", "192"),
|
||||
("https://cashmereradio.out.airtime.pro/cashmereradio_b", "mp3", "high", "192"),
|
||||
)
|
||||
|
||||
wordmark = "CASHMERE"
|
||||
hero_lines = ("EXPERIMENTAL RADIO", "BERLIN")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Datafruits: the netlabel's internet radio, relayed by Streampusher."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class DatafruitsProvider(CommunityProvider):
|
||||
id = "datafruits"
|
||||
name = "Datafruits"
|
||||
homepage = "datafruits.fm"
|
||||
brand = "DATAFRUITS"
|
||||
tagline = "netlabel radio · experimental"
|
||||
station_source = "streampusher relay + icecast status"
|
||||
history_source = "icecast title (rolling)"
|
||||
|
||||
station_title = "Datafruits"
|
||||
station_description = "internet radio from the Datafruits netlabel"
|
||||
station_genre = "electronic|experimental"
|
||||
streams = (("https://streampusher-relay.club/datafruits.mp3", "mp3", "high", "128"),)
|
||||
status_url = "https://streampusher-relay.club/status-json.xsl"
|
||||
|
||||
wordmark = "DATAFRUITS"
|
||||
hero_lines = ("NETLABEL RADIO", "EXPERIMENTAL ELECTRONIC")
|
||||
@@ -0,0 +1,108 @@
|
||||
"""dublab: Los Angeles community radio, with a public weekly schedule."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from ... import config
|
||||
from ...models import Broadcast, parse_date
|
||||
from ..base import fetch_json
|
||||
from .base import CommunityProvider
|
||||
|
||||
SCHEDULE_URL = "https://dublab.com/.netlify/functions/schedule"
|
||||
|
||||
|
||||
def parse_schedule(payload: Any) -> list[Broadcast]:
|
||||
"""The site's own calendar: ``[{startTime, endTime, summary, ...}]``."""
|
||||
if not isinstance(payload, list):
|
||||
return []
|
||||
broadcasts: list[Broadcast] = []
|
||||
for entry in payload:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = str(entry.get("summary") or entry.get("title") or "").strip()
|
||||
start = parse_date(entry.get("startTime") or entry.get("start"))
|
||||
end = parse_date(entry.get("endTime") or entry.get("end"))
|
||||
if not name or start is None:
|
||||
continue
|
||||
broadcasts.append(Broadcast(name=name, start=start, end=end))
|
||||
broadcasts.sort(key=lambda item: item.start or datetime.now(timezone.utc))
|
||||
return broadcasts
|
||||
|
||||
|
||||
class DublabProvider(CommunityProvider):
|
||||
id = "dublab"
|
||||
name = "dublab"
|
||||
homepage = "dublab.com"
|
||||
brand = "DUBLAB"
|
||||
tagline = "community radio · los angeles · since 1999"
|
||||
station_source = "dublab.com schedule"
|
||||
history_source = "dublab.com schedule (rolling)"
|
||||
|
||||
station_title = "dublab"
|
||||
station_description = "non-profit community radio from Los Angeles, since 1999"
|
||||
station_genre = "eclectic|experimental"
|
||||
streams = (("https://dublab.out.airtime.pro/dublab_a", "mp3", "high", "192"),)
|
||||
|
||||
# The schedule is the only metadata dublab publishes, so it drives both the
|
||||
# left pane and "what is on".
|
||||
has_schedule = True
|
||||
|
||||
wordmark = "DUBLAB"
|
||||
hero_lines = ("LOS ANGELES", "SINCE 1999")
|
||||
|
||||
async def fetch_schedule(self, client: httpx.AsyncClient) -> list[Broadcast]:
|
||||
try:
|
||||
payload = await fetch_json(client, SCHEDULE_URL, attempts=1)
|
||||
except RuntimeError:
|
||||
return self.load_cached_schedule()
|
||||
entries = self._mark_live(parse_schedule(payload))
|
||||
if not entries:
|
||||
return self.load_cached_schedule()
|
||||
config.write_json(
|
||||
config.schedule_cache_path(self.id),
|
||||
{
|
||||
"fetched": time.time(),
|
||||
"schedule": [
|
||||
{
|
||||
"name": item.name,
|
||||
"start": item.start.isoformat() if item.start else "",
|
||||
"end": item.end.isoformat() if item.end else "",
|
||||
}
|
||||
for item in entries
|
||||
],
|
||||
},
|
||||
)
|
||||
return entries
|
||||
|
||||
async def fetch_history(self, client: httpx.AsyncClient, station_id: str):
|
||||
# The schedule is the only "what is on" dublab publishes, so make sure
|
||||
# we have one before the base class looks for the running show.
|
||||
if not self.load_cached_schedule():
|
||||
with contextlib.suppress(Exception):
|
||||
await self.fetch_schedule(client)
|
||||
return await super().fetch_history(client, station_id)
|
||||
|
||||
def load_cached_schedule(self) -> list[Broadcast]:
|
||||
raw = config.read_json(config.schedule_cache_path(self.id))
|
||||
entries = raw.get("schedule") if isinstance(raw, dict) else None
|
||||
if not isinstance(entries, list):
|
||||
return []
|
||||
broadcasts: list[Broadcast] = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
start = parse_date(entry.get("start"))
|
||||
name = str(entry.get("name") or "").strip()
|
||||
if not name or start is None:
|
||||
continue
|
||||
broadcasts.append(
|
||||
Broadcast(name=name, start=start, end=parse_date(entry.get("end")))
|
||||
)
|
||||
broadcasts.sort(key=lambda item: item.start or datetime.now(timezone.utc))
|
||||
return broadcasts
|
||||
@@ -0,0 +1,31 @@
|
||||
"""The Lot Radio: live from a shipping container in Brooklyn (HLS)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class LotProvider(CommunityProvider):
|
||||
id = "lot"
|
||||
name = "The Lot Radio"
|
||||
homepage = "thelotradio.com"
|
||||
brand = "THELOT"
|
||||
tagline = "live from the lot · brooklyn"
|
||||
station_source = "thelotradio.com (HLS)"
|
||||
history_source = ""
|
||||
|
||||
station_title = "The Lot Radio"
|
||||
station_description = "live radio from a shipping container in Brooklyn"
|
||||
station_genre = "eclectic|house"
|
||||
# The station streams HLS; mpv plays the playlist directly.
|
||||
streams = (
|
||||
(
|
||||
"https://livepeercdn.studio/hls/85c28sa2o8wppm58/index.m3u8",
|
||||
"aac",
|
||||
"high",
|
||||
"",
|
||||
),
|
||||
)
|
||||
|
||||
wordmark = "THELOT"
|
||||
hero_lines = ("BROOKLYN", "LIVE FROM THE LOT")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""LYL Radio: independent webradio from Lyon and Paris."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class LylProvider(CommunityProvider):
|
||||
id = "lyl"
|
||||
name = "LYL Radio"
|
||||
homepage = "lyl.live"
|
||||
brand = "LYL"
|
||||
tagline = "we're the rest · lyon · paris"
|
||||
station_source = "icecast.lyl.live"
|
||||
history_source = ""
|
||||
|
||||
station_title = "LYL Radio"
|
||||
station_description = "independent webradio from Lyon and Paris"
|
||||
station_genre = "eclectic|experimental"
|
||||
streams = (("https://icecast.lyl.live/live", "mp3", "high", "192"),)
|
||||
|
||||
wordmark = "LYL"
|
||||
hero_lines = ("WE'RE THE REST", "LYON · PARIS")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Refuge Worldwide: Berlin community radio on a radio.co relay."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import CommunityProvider
|
||||
|
||||
|
||||
class RefugeProvider(CommunityProvider):
|
||||
id = "refuge"
|
||||
name = "Refuge Worldwide"
|
||||
homepage = "refugeworldwide.com"
|
||||
brand = "REFUGE"
|
||||
tagline = "community radio · berlin"
|
||||
station_source = "radio.co status"
|
||||
history_source = "radio.co now playing (rolling)"
|
||||
|
||||
station_title = "Refuge Worldwide"
|
||||
station_description = "community radio from Berlin"
|
||||
station_genre = "eclectic|electronic"
|
||||
streams = (("https://streaming.radio.co/s3699c5e49/listen", "mp3", "high", "192"),)
|
||||
radio_co_id = "s3699c5e49"
|
||||
|
||||
wordmark = "REFUGE"
|
||||
hero_lines = ("COMMUNITY RADIO", "BERLIN")
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Rinse FM: London's station family, a proxy mount each."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...models import Station
|
||||
from .base import CommunityProvider
|
||||
|
||||
#: (station id, title, genre, mount, description)
|
||||
_STATIONS: tuple[tuple[str, str, str, str, str], ...] = (
|
||||
(
|
||||
"rinse_uk",
|
||||
"Rinse UK",
|
||||
"electronic|house",
|
||||
"rinse_uk",
|
||||
"London's underground dance station, on air since 1994",
|
||||
),
|
||||
(
|
||||
"kool",
|
||||
"Kool London",
|
||||
"jungle|drum & bass",
|
||||
"kool",
|
||||
"Kool London: jungle, drum & bass and roots",
|
||||
),
|
||||
(
|
||||
"swu",
|
||||
"SWU",
|
||||
"soul|eclectic",
|
||||
"swu",
|
||||
"SWU: soul, funk and everything south west",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RinseProvider(CommunityProvider):
|
||||
id = "rinse"
|
||||
name = "Rinse FM"
|
||||
homepage = "rinse.fm"
|
||||
brand = "RINSE"
|
||||
tagline = "london · since 1994"
|
||||
station_source = "rinse.fm streams"
|
||||
history_source = ""
|
||||
|
||||
station_genre = "electronic"
|
||||
|
||||
wordmark = "RINSE"
|
||||
hero_lines = ("LONDON", "SINCE 1994")
|
||||
|
||||
def stations(self) -> list[Station]:
|
||||
return [
|
||||
Station(
|
||||
id=station_id,
|
||||
title=title,
|
||||
description=description,
|
||||
genre=genre,
|
||||
playlists=[
|
||||
{
|
||||
"url": f"https://admin.stream.rinse.fm/proxy/{mount}/stream",
|
||||
"format": "aacp",
|
||||
"quality": "high",
|
||||
"bitrate": "128",
|
||||
}
|
||||
],
|
||||
provider=self.id,
|
||||
)
|
||||
for station_id, title, genre, mount, description in _STATIONS
|
||||
]
|
||||
Reference in new issue
Block a user