radio: add Gensokyo Radio API poller for rich now-playing embeds

This commit is contained in:
esef committed 2026-07-25 12:57:39 +03:00
1 parent 3d2d19b0b7
commit d1536666f7
2 files changed
+155 -4

No files matched your search

+10 -4
View File
@@ -9,7 +9,7 @@ from ..shared.constants import (
)
from .views import _CategoryView
from .icy import IcyMetadataPoller
from . import gensokyo
log = logging.getLogger(__name__)
@@ -156,8 +156,14 @@ class RadioCommands:
embed.set_footer(text="yuuka radio · use !stop to disconnect")
await interaction.followup.send(embed=embed)
# Spawn ICY metadata poller if this station supports it
if station["url"] in self._icy_urls:
# Spawn gensokyo special metadata this snippet is for gensokyo radio only
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"])
self._cancel_icy_poller(guild.id)
poller = IcyMetadataPoller(
@@ -168,7 +174,7 @@ class RadioCommands:
)
self._icy_tasks[guild.id] = asyncio.create_task(poller.run())
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))
# ── ICY now-playing embed ─────────────────────────────────────────────────
+145
View File
@@ -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
)