[READ-ONLY] Mirror of https://github.com/aaronateataco/SonicBridge. proxy for web radios sonicradio.vercel.app
0

Configure Feed

Select the types of activity you want to include in your feed.

Refactor API_BASE URL and improve error messages; add new station handling endpoints

aaronateataco (Jul 15, 2026, 7:00 PM +0100) 87765fa5 d259bea7

+108 -3
+61
api/[station_id].py
··· 1 + import json 2 + from http.server import BaseHTTPRequestHandler 3 + import urllib.request 4 + import urllib.error 5 + 6 + # Verified working Akamai live stream pools for global access 7 + STATIONS = { 8 + "radio1": {"name": "BBC Radio 1", "pool": "pool_01505109", "slug": "bbc_radio_one"}, 9 + "radio1xtra": {"name": "BBC Radio 1Xtra", "pool": "pool_92079267", "slug": "bbc_1xtra"}, 10 + "radio2": {"name": "BBC Radio 2", "pool": "pool_74208725", "slug": "bbc_radio_two"}, 11 + "radio3": {"name": "BBC Radio 3", "pool": "pool_23461179", "slug": "bbc_radio_three"}, 12 + "radio4": {"name": "BBC Radio 4", "pool": "pool_55057080", "slug": "bbc_radio_fourfm"}, 13 + "radio4extra": {"name": "BBC Radio 4 Extra", "pool": "pool_26173715", "slug": "bbc_radio_four_extra"}, 14 + "radio5": {"name": "BBC Radio 5 Live", "pool": "pool_89021708", "slug": "bbc_radio_five_live"}, 15 + "6music": {"name": "BBC Radio 6 Music", "pool": "pool_81827798", "slug": "bbc_6music"} 16 + } 17 + 18 + class handler(BaseHTTPRequestHandler): 19 + def do_GET(self): 20 + # Extract station token from path 21 + path_parts = self.path.strip('/').split('/') 22 + station_token = path_parts[1] if len(path_parts) > 1 else None 23 + 24 + if not station_token: 25 + self.send_error(404) 26 + return 27 + 28 + # Strip file extensions 29 + token = station_token.lower().replace('.flac', '').replace('.ogg', '') 30 + 31 + if token not in STATIONS: 32 + self.send_error(404) 33 + return 34 + 35 + config = STATIONS[token] 36 + 37 + # Build the BBC stream URL 38 + # This is the actual Akamai stream endpoint that BBC uses 39 + stream_url = f"https://a.files.bbci.co.uk/mediaconnection/live/akamai/{config['pool']}/output/index.m3u" 40 + 41 + try: 42 + # Fetch the actual stream URL 43 + with urllib.request.urlopen(stream_url, timeout=10) as response: 44 + stream_data = response.read() 45 + 46 + # Return the stream data 47 + self.send_response(200) 48 + self.send_header('Content-type', 'application/vnd.apple.mpegurl') 49 + self.send_header('Access-Control-Allow-Origin', '*') 50 + self.send_header('Content-Length', len(stream_data)) 51 + self.end_headers() 52 + self.wfile.write(stream_data) 53 + except Exception as e: 54 + self.send_error(502, f"Failed to fetch stream: {str(e)}") 55 + 56 + def do_OPTIONS(self): 57 + self.send_response(200) 58 + self.send_header('Access-Control-Allow-Origin', '*') 59 + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 60 + self.send_header('Access-Control-Allow-Headers', 'Content-Type') 61 + self.end_headers()
+43
api/stations.py
··· 1 + from http.server import BaseHTTPRequestHandler 2 + import asyncio 3 + from fastapi import FastAPI 4 + from fastapi.responses import JSONResponse 5 + from fastapi.middleware.cors import CORSMiddleware 6 + 7 + # Verified working Akamai live stream pools for global access 8 + STATIONS = { 9 + "radio1": {"name": "BBC Radio 1", "pool": "pool_01505109", "slug": "bbc_radio_one"}, 10 + "radio1xtra": {"name": "BBC Radio 1Xtra", "pool": "pool_92079267", "slug": "bbc_1xtra"}, 11 + "radio2": {"name": "BBC Radio 2", "pool": "pool_74208725", "slug": "bbc_radio_two"}, 12 + "radio3": {"name": "BBC Radio 3", "pool": "pool_23461179", "slug": "bbc_radio_three"}, 13 + "radio4": {"name": "BBC Radio 4", "pool": "pool_55057080", "slug": "bbc_radio_fourfm"}, 14 + "radio4extra": {"name": "BBC Radio 4 Extra", "pool": "pool_26173715", "slug": "bbc_radio_four_extra"}, 15 + "radio5": {"name": "BBC Radio 5 Live", "pool": "pool_89021708", "slug": "bbc_radio_five_live"}, 16 + "6music": {"name": "BBC Radio 6 Music", "pool": "pool_81827798", "slug": "bbc_6music"} 17 + } 18 + 19 + class handler(BaseHTTPRequestHandler): 20 + def do_GET(self): 21 + self.send_response(200) 22 + self.send_header('Content-type', 'application/json') 23 + self.send_header('Access-Control-Allow-Origin', '*') 24 + self.end_headers() 25 + 26 + stations = [] 27 + for token, config in STATIONS.items(): 28 + stations.append({ 29 + "id": token, 30 + "name": config["name"], 31 + "description": f"Live {config['name']} stream", 32 + "image": f"https://images.unsplash.com/photo-1516280440614-37939bbacd81?auto=format&fit=crop&w=600&q=80" 33 + }) 34 + 35 + import json 36 + self.wfile.write(json.dumps(stations).encode()) 37 + 38 + def do_OPTIONS(self): 39 + self.send_response(200) 40 + self.send_header('Access-Control-Allow-Origin', '*') 41 + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 42 + self.send_header('Access-Control-Allow-Headers', 'Content-Type') 43 + self.end_headers()
+2 -2
src/App.jsx
··· 1 1 import { useEffect, useMemo, useRef, useState } from "react"; 2 2 3 - const API_BASE = (import.meta.env.VITE_API_BASE || "https://sonicbridge.onrender.com").replace(/\/+$/, ""); 3 + const API_BASE = (import.meta.env.VITE_API_BASE || "/api").replace(/\/+$/, ""); 4 4 5 5 // Decorative dial numbers only - not real broadcast frequencies. Gives each 6 6 // card the "tuner" feel the rest of the UI is built around. ··· 113 113 {status === "error" && ( 114 114 <p className="status-msg status-error"> 115 115 Couldn't reach the backend at <code>{API_BASE}</code>. Set{" "} 116 - <code>VITE_API_BASE</code> to your deployed Render URL. 116 + <code>VITE_API_BASE</code> to your backend URL. 117 117 </p> 118 118 )} 119 119
+1 -1
src/index.css
··· 1 - @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"); 1 + @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap&display=swap"); 2 2 3 3 :root { 4 4 --bg: #0d0f14;
+1
vercel.json
··· 1 1 { 2 2 "rewrites": [ 3 + { "source": "/api/(.*)", "destination": "/api/$1" }, 3 4 { "source": "/(.*)", "destination": "/index.html" } 4 5 ] 5 6 }