A Bhojpuri stage-night radio. The songs a hired आर्केस्ट्रा plays at a wedding in Bihar — in blocks, through the night, in the order a real programme runs them.
Not a music player with a Bihar skin. The structure of the night is the product.
The site reads your clock and plays what an orchestra would be playing at that hour. The scene retints with it.
| Local time | Block | Scene |
|---|---|---|
| 5–9 pm | साउंड चेक | Tubelights half-on, sky still blue |
| 9 pm–12 am | मंच पूजा · देवी गीत | Full tubelights, nobody standing yet |
| 12–1 am | शादी–तिलक नाइट | Par cans in, crowd up |
| 1–3 am | डीजे ब्लॉक | Strobes, thick beams |
| 3–5 am | दर्द ब्लॉक | Lights go warm and low |
| 5 am–5 pm | भोरहरी | Sky greys, tubelights coming down |
It's a default, not a cage — pick a playlist and the clock stops steering. And it never switches mid-song: the running order is rebuilt for the new block and the current track is allowed to finish.
Sibling sites in this genre show a live-looking listener number that is actually a random walk. A made-up number sitting next to a rights-and-takedown notice isn't a detail worth inheriting. The block name replaces it, and the block name is true — derived from your device clock and nothing else.
The stage is a hand-authored SVG: bamboo-and-plank deck, the tubelight ladder zip-tied across the front, column speakers, the magenta-and-cyan flex backdrop, a bamboo truss of par cans, dust in the beams, backs of heads in the crowd. It costs 7 KB inlined in the HTML instead of a 250 KB photograph, stays sharp at any size, retints by the hour, and the speaker cones move while the music plays.
The YouTube player is the LED wall behind the stage — visible, in the scene, graded to match the night. Real setups have one, and a visible player is what YouTube's terms contemplate.
The commercial arkestra trade has a well-documented cost: reporting by The Print, PARI, 101Reporters and others records widespread trafficking of women and girls into orchestra groups. Bihar Police rescued 162 girls from the so-called orchestra belt in the first half of 2025 alone.
That's not a reason to drop the homage. It's a reason to be precise about who it's to.
The protagonists here are the crew — the बजनिया at the octapad, the dholak player warming the skin over a fire so the pitch comes up, the anchor with his folded sheet of names, the light-wala, the generator man. Their craft is real, their labour is chosen, and celebrating them costs nobody anything.
So there are no dancers in the artwork and there will not be any. No item-song visual language, no notes showered on a performer, no firearms, no double-meaning songs in the catalogue, no phone digits anywhere. The footer separates the hundred-year Bahujan lineage — Bhikhari Thakur, बिदेसिया, Padma Shri Ramchandra Manjhi — from the post-2000 commercial mutation, and links the orchestra-free-marriage movement in West Champaran.
docs/PRD.md §3 has the full position. It shaped the artwork, not just the copy.
Astro 5, static output, one npm dependency. Vanilla CSS, vanilla JS, no framework runtime, no database, no backend.
JS 13.5 KB gz page total 26.8 KB gz
CSS 6.3 KB gz fonts 261 KB (subset, font-display: swap)
HTML 7.0 KB gz ← with the artwork already in it
Audio is a YouTube IFrame player. Nothing is hosted here.
This is why Astro rather than plain HTML: a radio whose music stops when you open another page is not a radio. It did not work for free —
transition:persiston an Astro component is silently dropped; it has to sit on the real root element inside the component file.- Persisting the node still isn't enough. Astro's default body swap moves persisted nodes into the new body, and moving an
<iframe>reloads it — the node survived and the video went back to CUED at 0:00.src/scripts/swap.jsreplaces the swap with one that keeps the existing<body>and never detaches anything persisted.
Verified in real Chrome over CDP: / → /songs → /playlists with YouTube state staying 1 (PLAYING) and playback time advancing across both navigations.
npm install
npm run dev # http://localhost:4321| Command | |
|---|---|
npm run build |
Static build into dist/ |
npm run tracks |
Rebuild the catalogue from the song seed (needs yt-dlp) |
npm run revalidate |
Check all 59 YouTube IDs are still alive and embeddable |
npm run fonts |
Re-download and re-subset the webfonts |
npm run check:fonts |
Fail if any Devanagari character is missing from the subset |
59 songs across five blocks, every ID resolved and verified — none guessed. scripts/build-tracks.mjs takes hand-written search queries, pulls five candidates each, scores them on artist-in-channel, title overlap (in both scripts — Bhojpuri uploads are titled in Devanagari and the first pass scored perfect matches at 0%), duration sanity and label channel, then dedupes, pre-flights embeddability and region, and verifies the cover art with a HEAD request.
Then a human reads all 59 rows, because that's the only real fix for the ~15% of searches that return a popular wrong song. Thirteen rows carry a hand-verified pinned ID.
29 candidates were dropped rather than guessed at — docs/rejected-songs.md records every one and the mechanism that caught it, including two "songs" that turned out to be lines inside other songs, and one whose real title the research had wrong.
src/data/songs.seed.jsis the source of truth, notpublic/data/catalogue.json. The catalogue is generated and will be overwritten.
The Devanagari here is a closed corpus — a wordmark, some labels, 44 anchor lines, 59 song titles, all authored. After changing any Devanagari copy:
npm run build && npm run fonts && npm run build && npm run check:fontsMiss it and new characters fall back to a system face mid-sentence.
docs/PRD.md |
The full spec — 19 sections |
docs/HANDOVER.md |
What was built, what's verified, what isn't, and the bugs worth knowing about |
docs/rejected-songs.md |
The 29 cuts and why |
docs/verification/ |
CDP scripts and their raw output |
Built and verified in desktop Chrome. Not yet deployed. Still to do: a real-device pass on iOS Safari and Chrome Android (autoplay unlock, playsinline, lock-screen), and confirming the production URL in src/data/site.js.
One iOS limitation with no workaround: audio stops when the screen locks. That's an OS restriction on web audio in an iframe — Media Session supplies metadata but not background rights, and installing as a PWA doesn't change it. The footer says so plainly.
Audio plays through YouTube's embedded player. Nothing is hosted here, and all rights stay with the labels, composers and performers. Takedowns: raysanjeev19@gmail.com



