A social internet radio platform built on AT Protocol. atradio.fm
atproto radio
13

Configure Feed

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

at main 2 folders 4 files
README.md

atradio media proxy#

A small, stateless reverse-proxy (Gleam on the BEAM) for the bits of the web app that need a CORS-friendly, mixed-content-safe upstream. Extracted from the AppView (apps/api) so the long-lived, bandwidth-heavy streaming workload scales and fails independently of discovery + the Connect hub.

Routes#

Route Purpose
GET /api/stream?url= Reverse-proxy an audio stream, piped chunk-by-chunk so the Rockbox wasm decoder can fetch it cross-origin (keeps the DSP/EQ in the signal path). Unwraps .pls/.m3u; forwards ICY metadata + Range; follows redirects.
GET /api/tunein/* Proxy opml.radiotime.com/* (TuneIn sends no CORS).
GET /api/image?url= Proxy http:// station artwork (mixed-content). Only image responses are relayed.
GET /api/icy?url= ICY "now playing" ({ "title": … }). Bounded gun read of the interleaved metadata blocks; unwraps playlists first.
GET /healthz Liveness.

Every route is public + read-only, so CORS is a blanket origin: * with the ICY headers exposed.

Configuration#

Env var Default Meaning
PORT 7081 HTTP listen port.
RATE_LIMIT 120 Max /api/* requests per IP per window (429 over it).
RATE_WINDOW 60 Rate-limit window, seconds.

Develop#

gleam run    # start the proxy (PORT env, default 7081)
gleam test   # unit tests (playlist parsing + port config)
PORT=9000 gleam run

Quick check:

curl localhost:7081/healthz                     # ok
curl "localhost:7081/api/stream?url=https://..." # piped audio

Deploy#

Run from source under the toolchain (mirrors the api/jetstream units) — see systemd/atradio-media-proxy.service. For a self-contained release instead:

gleam export erlang-shipment
./build/erlang-shipment/entrypoint.sh run

Layout#

src/
├─ atradio_media_proxy.gleam   # mist server + router + CORS + PORT env
├─ media_proxy/config.gleam    # env config (PORT)
├─ media_proxy/stream.gleam    # /stream — streaming reverse-proxy
├─ media_proxy/simple.gleam    # /tunein, /image, /icy handlers
├─ media_proxy/icy.gleam       # ICY StreamTitle reader (via the FFI)
├─ media_proxy/playlist.gleam  # .pls/.m3u unwrapping
├─ media_proxy/cache.gleam     # per-node TTL cache (TuneIn 300s, ICY 20s)
├─ proxy_gun_ffi.erl           # gun: streaming pipe + bounded ICY read
└─ proxy_cache_ffi.erl         # ETS-backed TTL cache

TuneIn + ICY responses are cached in a node-local ETS table (no Redis); a cache hit is served with x-cache: HIT.

The gun FFI is the one piece Node hands you free (stream.pipe): a pump process owns the upstream connection and forwards each chunk to the Gleam Subject that drives mist.chunked.

Web wiring#

Point the web app at this service instead of the AppView for the proxy routes (e.g. VITE_MEDIA_PROXY), then delete apps/api/src/proxy/. The AppView keeps XRPC + Jetstream + the /connect hub + /health.