radio: add Gensokyo Radio API poller for rich now-playing embeds
This commit is contained in:
1 parent
3d2d19b0b7
commit
d1536666f7
2 files changed
+155
-4
No files matched your search
+10
-4
@@ -9,7 +9,7 @@ from ..shared.constants import (
|
|||||||
)
|
)
|
||||||
from .views import _CategoryView
|
from .views import _CategoryView
|
||||||
from .icy import IcyMetadataPoller
|
from .icy import IcyMetadataPoller
|
||||||
|
from . import gensokyo
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -156,8 +156,14 @@ class RadioCommands:
|
|||||||
embed.set_footer(text="yuuka radio · use !stop to disconnect")
|
embed.set_footer(text="yuuka radio · use !stop to disconnect")
|
||||||
await interaction.followup.send(embed=embed)
|
await interaction.followup.send(embed=embed)
|
||||||
|
|
||||||
# Spawn ICY metadata poller if this station supports it
|
# Spawn gensokyo special metadata this snippet is for gensokyo radio only
|
||||||
if station["url"] in self._icy_urls:
|
if station["name"] == "Gensokyo Radio":
|
||||||
|
log.info("Starting Gensokyo API poller for %s", station["name"])
|
||||||
|
self._cancel_icy_poller(guild.id)
|
||||||
|
self._icy_tasks[guild.id] = asyncio.create_task(
|
||||||
|
gensokyo.poll(self, guild.id, station["name"])
|
||||||
|
)
|
||||||
|
elif station["url"] in self._icy_urls:
|
||||||
log.info("Starting ICY poller for %s", station["name"])
|
log.info("Starting ICY poller for %s", station["name"])
|
||||||
self._cancel_icy_poller(guild.id)
|
self._cancel_icy_poller(guild.id)
|
||||||
poller = IcyMetadataPoller(
|
poller = IcyMetadataPoller(
|
||||||
@@ -168,7 +174,7 @@ class RadioCommands:
|
|||||||
)
|
)
|
||||||
self._icy_tasks[guild.id] = asyncio.create_task(poller.run())
|
self._icy_tasks[guild.id] = asyncio.create_task(poller.run())
|
||||||
else:
|
else:
|
||||||
log.info("No ICY support for %s (%s not in set of %d URLs)",
|
log.info("No metadata support for %s (%s not in ICY set of %d URLs)",
|
||||||
station["name"], station["url"], len(self._icy_urls))
|
station["name"], station["url"], len(self._icy_urls))
|
||||||
|
|
||||||
# ── ICY now-playing embed ─────────────────────────────────────────────────
|
# ── ICY now-playing embed ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""Gensokyo Radio API — polls their REST endpoint for rich now-playing data."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from ..shared.constants import C_RADIO
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_API_URL = "https://gensokyoradio.net/api/station/playing/"
|
||||||
|
_ART_BASE = "https://gensokyoradio.net/images/albums/500"
|
||||||
|
_POLL_SECS = 15
|
||||||
|
|
||||||
|
|
||||||
|
async def poll(cog: Any, guild_id: int, station_name: str) -> None:
|
||||||
|
"""Poll Gensokyo Radio's API and post embeds on track change.
|
||||||
|
|
||||||
|
``cog`` is the Music cog instance (needed for bot, _radio_channels, etc.).
|
||||||
|
Stores itself in ``cog._icy_tasks`` so ``_cancel_icy_poller`` handles cleanup.
|
||||||
|
"""
|
||||||
|
last_song_id = None
|
||||||
|
async with aiohttp.ClientSession(
|
||||||
|
timeout=aiohttp.ClientTimeout(total=10),
|
||||||
|
headers={"User-Agent": "yuuka-radio/1.0"},
|
||||||
|
) as session:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with session.get(_API_URL) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
log.warning("Gensokyo API returned %d", resp.status)
|
||||||
|
await asyncio.sleep(_POLL_SECS)
|
||||||
|
continue
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
song_id = data["SONGDATA"]["SONGID"]
|
||||||
|
if song_id == last_song_id:
|
||||||
|
await asyncio.sleep(_POLL_SECS)
|
||||||
|
continue
|
||||||
|
last_song_id = song_id
|
||||||
|
|
||||||
|
await _send_now_playing(cog, guild_id, station_name, data)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
log.warning("Gensokyo API poller error", exc_info=True)
|
||||||
|
|
||||||
|
await asyncio.sleep(_POLL_SECS)
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_now_playing(
|
||||||
|
cog: Any, guild_id: int, station_name: str, data: dict
|
||||||
|
) -> None:
|
||||||
|
"""Build and post a rich embed from Gensokyo Radio API JSON."""
|
||||||
|
channel_id = cog._radio_channels.get(guild_id)
|
||||||
|
if channel_id is None:
|
||||||
|
return
|
||||||
|
guild = cog.bot.get_guild(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
return
|
||||||
|
channel = guild.get_channel_or_thread(channel_id)
|
||||||
|
if channel is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
song = data.get("SONGINFO", {})
|
||||||
|
times = data.get("SONGTIMES", {})
|
||||||
|
sdata = data.get("SONGDATA", {})
|
||||||
|
misc = data.get("MISC", {})
|
||||||
|
server = data.get("SERVERINFO", {})
|
||||||
|
|
||||||
|
title = song.get("TITLE", "Unknown")
|
||||||
|
artist = song.get("ARTIST", "")
|
||||||
|
album = song.get("ALBUM", "")
|
||||||
|
year = song.get("YEAR", "")
|
||||||
|
circle = song.get("CIRCLE", "")
|
||||||
|
duration = times.get("DURATION", 0)
|
||||||
|
rating = sdata.get("RATING")
|
||||||
|
times_rated = sdata.get("TIMESRATED", 0)
|
||||||
|
listeners = server.get("LISTENERS", 0)
|
||||||
|
|
||||||
|
# Bitrate — prefer highest quality stream
|
||||||
|
streams = server.get("STREAMS", {})
|
||||||
|
bitrate = ""
|
||||||
|
for sid in ("3", "2", "1"):
|
||||||
|
s = streams.get(sid, {})
|
||||||
|
if s.get("BITRATE"):
|
||||||
|
bitrate = str(s["BITRATE"])
|
||||||
|
break
|
||||||
|
|
||||||
|
# Album art
|
||||||
|
album_art = misc.get("ALBUMART", "")
|
||||||
|
album_art_url = f"{_ART_BASE}/{album_art}" if album_art else None
|
||||||
|
|
||||||
|
# Build embed
|
||||||
|
desc_parts = []
|
||||||
|
if artist:
|
||||||
|
desc_parts.append(f"**{artist}**")
|
||||||
|
if album:
|
||||||
|
album_str = album
|
||||||
|
if year:
|
||||||
|
album_str += f" ({year})"
|
||||||
|
desc_parts.append(album_str)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🎵 {title}",
|
||||||
|
description=" · ".join(desc_parts) if desc_parts else None,
|
||||||
|
color=C_RADIO,
|
||||||
|
)
|
||||||
|
|
||||||
|
if circle:
|
||||||
|
embed.add_field(name="Circle", value=circle, inline=True)
|
||||||
|
|
||||||
|
if duration > 0:
|
||||||
|
mins, secs = divmod(duration, 60)
|
||||||
|
embed.add_field(name="Duration", value=f"{mins}:{secs:02d}", inline=True)
|
||||||
|
|
||||||
|
if rating:
|
||||||
|
embed.add_field(
|
||||||
|
name="Rating",
|
||||||
|
value=f"{rating} ({times_rated} votes)",
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if bitrate or listeners:
|
||||||
|
parts = []
|
||||||
|
if bitrate:
|
||||||
|
parts.append(f"{bitrate} kbps")
|
||||||
|
if listeners:
|
||||||
|
parts.append(f"{listeners} listeners")
|
||||||
|
embed.add_field(name="Stream", value=" · ".join(parts), inline=True)
|
||||||
|
|
||||||
|
if album_art_url:
|
||||||
|
embed.set_thumbnail(url=album_art_url)
|
||||||
|
|
||||||
|
embed.set_footer(text=f"now on {station_name} · Gensokyo Radio")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
except Exception:
|
||||||
|
log.debug(
|
||||||
|
"Failed to send Gensokyo now-playing in guild %d", guild_id, exc_info=True
|
||||||
|
)
|
||||||
Reference in new issue
Block a user