Fix: Tile proxy for black globe + radio voicestats modal
- Globe was black because Radio Garden CDN (rg-tiles.b-cdn.net) returns 403 without Referer: radio.garden header. Added server-side tile proxy /api/radio/tile/:z/:x/:y with in-memory cache (max 500 tiles). - Added radio_voicestats SSE broadcast (every 5s) with voice ping, gateway ping, status, channel name, and connected-since timestamp. - Added clickable "Verbunden" connection indicator in RadioTab bottom bar with live ping display and connection details modal (matching soundboard's existing modal pattern). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b9a9347356
commit
63afc55836
4 changed files with 286 additions and 5 deletions
|
|
@ -14,6 +14,15 @@ import {
|
|||
resolveStreamUrl, getPlacesCount,
|
||||
} from './api.js';
|
||||
|
||||
// ── Tile proxy cache (in-memory) ──
|
||||
const TILE_CDN = 'https://rg-tiles.b-cdn.net';
|
||||
const tileCache = new Map<string, { data: Buffer; contentType: string }>();
|
||||
const TILE_CACHE_MAX = 500; // max cached tiles (~15-25 MB)
|
||||
|
||||
// ── Voice connection tracking ──
|
||||
const connectedSince = new Map<string, string>();
|
||||
let voiceStatsInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
// ── Types ──
|
||||
interface GuildRadioState {
|
||||
stationId: string;
|
||||
|
|
@ -68,6 +77,7 @@ function stopStream(guildId: string): void {
|
|||
try { state.player.stop(true); } catch {}
|
||||
try { getVoiceConnection(guildId, 'radio')?.destroy(); } catch {}
|
||||
guildRadioState.delete(guildId);
|
||||
connectedSince.delete(guildId);
|
||||
broadcastState(guildId);
|
||||
console.log(`[Radio] Stopped stream in guild ${guildId}`);
|
||||
}
|
||||
|
|
@ -195,6 +205,25 @@ const radioPlugin: Plugin = {
|
|||
|
||||
async onReady(ctx) {
|
||||
console.log(`[Radio] Discord ready – ${ctx.client.guilds.cache.size} Guild(s)`);
|
||||
|
||||
// Voice stats broadcast every 5s
|
||||
voiceStatsInterval = setInterval(() => {
|
||||
if (guildRadioState.size === 0) return;
|
||||
for (const [gId, st] of guildRadioState) {
|
||||
const conn = getVoiceConnection(gId, 'radio');
|
||||
const status = conn?.state?.status ?? 'unknown';
|
||||
if (status === 'ready' && !connectedSince.has(gId)) connectedSince.set(gId, new Date().toISOString());
|
||||
const ch = ctx.client.channels.cache.get(st.channelId);
|
||||
sseBroadcast({
|
||||
type: 'radio_voicestats', plugin: 'radio', guildId: gId,
|
||||
voicePing: (conn?.ping as any)?.ws ?? null,
|
||||
gatewayPing: ctx.client.ws.ping,
|
||||
status,
|
||||
channelName: ch && 'name' in ch ? (ch as any).name : null,
|
||||
connectedSince: connectedSince.get(gId) ?? null,
|
||||
});
|
||||
}
|
||||
}, 5_000);
|
||||
},
|
||||
|
||||
registerRoutes(app: express.Application, ctx: PluginContext) {
|
||||
|
|
@ -231,6 +260,44 @@ const radioPlugin: Plugin = {
|
|||
}
|
||||
});
|
||||
|
||||
// ── Tile Proxy (Radio Garden CDN requires Referer) ──
|
||||
app.get('/api/radio/tile/:z/:x/:y', async (req, res) => {
|
||||
const { z, x, y } = req.params;
|
||||
const key = `${z}/${x}/${y}`;
|
||||
|
||||
// Serve from cache
|
||||
const cached = tileCache.get(key);
|
||||
if (cached) {
|
||||
res.set('Content-Type', cached.contentType);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(cached.data);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${TILE_CDN}/${key}.jpg`, {
|
||||
headers: { Referer: 'https://radio.garden/' },
|
||||
});
|
||||
if (!resp.ok) { res.status(resp.status).end(); return; }
|
||||
|
||||
const buf = Buffer.from(await resp.arrayBuffer());
|
||||
const ct = resp.headers.get('content-type') ?? 'image/jpeg';
|
||||
|
||||
// Cache (evict oldest if full)
|
||||
if (tileCache.size >= TILE_CACHE_MAX) {
|
||||
const first = tileCache.keys().next().value;
|
||||
if (first) tileCache.delete(first);
|
||||
}
|
||||
tileCache.set(key, { data: buf, contentType: ct });
|
||||
|
||||
res.set('Content-Type', ct);
|
||||
res.set('Cache-Control', 'public, max-age=86400');
|
||||
res.send(buf);
|
||||
} catch {
|
||||
res.status(502).end();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Verfügbare Guilds + Voice Channels ──
|
||||
app.get('/api/radio/guilds', (_req, res) => {
|
||||
const guilds = ctx.client.guilds.cache.map(g => ({
|
||||
|
|
@ -356,6 +423,7 @@ const radioPlugin: Plugin = {
|
|||
},
|
||||
|
||||
async destroy() {
|
||||
if (voiceStatsInterval) clearInterval(voiceStatsInterval);
|
||||
for (const guildId of guildRadioState.keys()) {
|
||||
stopStream(guildId);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue