omFM Radio website example/template https://play.omfm.ru
  • Vue 45.1%
  • CSS 32.9%
  • JavaScript 12.5%
  • TypeScript 9.2%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-27 14:38:55 +03:00
app milkdrop: change preset every 30 s instead of 20 s 2026-09-27 10:52:23 +03:00
content README на двух языках + локальная обложка статьи 2026-08-25 19:58:26 +03:00
i18n/locales Update Nuxt (3.14), deps, i18n folder structure, icons local 2024-11-10 08:35:52 +03:00
public add logos 2026-09-19 18:23:04 +03:00
server about: список станций из реестра + плейлисты .m3u 2026-08-28 23:10:42 +03:00
.dockerignore change 2024-08-25 11:01:26 +03:00
.env.example 1st 2024-08-24 19:39:20 +03:00
.gitignore to nuxt4 2026-08-25 17:49:38 +03:00
.npmrc update dependencies 2026-08-25 18:58:13 +03:00
.prettierrc.json 1st 2024-08-24 19:39:20 +03:00
content.config.ts to nuxt4 2026-08-25 17:49:38 +03:00
docker-compose.dev.yml 1st 2024-08-24 19:39:20 +03:00
docker-compose.yml muddy waters color 2024-09-03 19:12:08 +03:00
Dockerfile update dependencies 2026-08-25 18:58:13 +03:00
Dockerfile.dev changes 2024-08-28 23:41:30 +03:00
nuxt.config.ts ashes: радиал как у terra + dev перечитывает реестр 2026-08-29 10:17:34 +03:00
package-lock.json add milkdrop 2026-09-18 18:55:47 +03:00
package.json add milkdrop 2026-09-18 18:55:47 +03:00
README.md docs: update station features and MilkDrop readmes 2026-09-27 14:38:55 +03:00
README.ru.md docs: update station features and MilkDrop readmes 2026-09-27 14:38:55 +03:00
tailwind.config.cjs stage 1 - создан реестр станций 2026-08-25 14:51:45 +03:00
tsconfig.json 1st 2024-08-24 19:39:20 +03:00
vercel.json . 2025-01-26 22:30:13 +03:00

omFM

A radio station network that runs on one array.

Live at omfm.ru · Русская версия


What this is

A multi-station internet radio front end built with Nuxt 4. Ten stations, three now-playing providers, HLS and Icecast playback, a real-time spectrum analyzer, a full-screen MilkDrop visualizer, a 10-band equalizer, cover art lookup, on-air schedules, and a UI that repaints itself to match the artwork of whatever is playing right now.

The interesting part is not the feature list — it's that adding a station takes one object in one file. No new pages, no new components, no wiring. Add it to the registry and the site grows a page, a card, a menu entry, a player thumbnail, an SSE subscription and its own SEO metadata.

omFM omFM omFM omFM omFM

Adding a station

Everything lives in app/config/stations.ts. Append one object:

{
  id: 'jazz',                 // player mount + key in every store
  slug: 'jazz',               // /streams/jazz
  provider: 'azuracast',      // where now-playing comes from
  channel: 'station:jazz',    // channel name on that provider

  hls: 'https://radio.example.com/hls/jazz/live.m3u8',
  icecast: 'https://stream.example.com:8443/jazz',

  text: {
    menu: 'Jazz @ omFM',      // stream picker + card button
    card: 'Jazz',             // caption under the card
    hero: 'JazzFM',           // station page heading
    tagline: 'smoky rooms and brushed drums',
    logo: 'Jazz',             // <sup> next to the header logo
    panel: 'JazzFM',          // player slide-out title
    thumb: 'Jazz',            // player swiper bubble
    tab: 'Jazz',              // tab on /streams
  },

  look: {
    font: "'Tenor Sans', sans-serif",           // any CSS font-family
    radial: 'radial-gradient(#0056fb29, #ff00fb26)',   // interface tint
    accent: 'border border-amber-500/50',       // active card border
  },

  images: {
    heroLight: '/jazz.jpg',
    heroDark: '/jazz.jpg',
    card: '/jazz.jpg',
    thumb: 'rock-00-thumb.jpg',                 // from app/assets/img/
  },

  visualizer: {},               // omit for the built-in colours
  showNext: true,
  artSource: 'station',         // 'itunes' to look covers up instead
  historyCount: 5,
}

Run npm run generate. That's the whole procedure.

Optional fields, for when a station needs more than the basics:

field what it does
text.heroExtra extra lines under the tagline; a string or an array of strings, one line each
text.nowPlayingLabel caption above the track in the mini player (defaults to text.menu)
text.playlistFallback what to show in the "Show:" line when the playlist name is empty
look.homeButton Tailwind gradient (from-… via-… to-…) for the "Home" button on the station page
look.heroTitleClass extra class for the hero title, e.g. a neon text shadow
look.dimmed dim the card image more — for artwork that is too bright
itunesFallback with artSource: 'station': if the track has no embedded cover, try iTunes before the placeholder
schedule the station's on-air grid — see below

Using a font of your own? Declare it with @font-face and put the family in look.font — no CSS edits needed, the theme variables are generated from the registry.

Schedule

Give a station a schedule and its page grows a day strip with the current block marked; clicking it opens a drawer with the full grid. Hours are written in the station's time zone, not the visitor's — the broadcast runs on radio time:

schedule: {
  timezone: 'Europe/Moscow',
  note: 'Themed hours break the rotation.',       // caption under the heading
  slots: [
    { from: '00:00', to: '07:00', title: 'Night', description: 'Dark ambient and drone' },
    { from: '07:00', to: '21:00', title: 'Day', description: 'EBM and synthpop' },
    { from: '21:00', to: '22:00', title: 'Organ', days: [1, 2, 4, 7] },   // ISO weekdays
    { from: '21:00', to: '22:00', title: 'Romantic', days: [3, 5, 6] },
    { from: '22:00', to: '00:00', title: 'Night' },  // end before start = past midnight
  ],
},

A slot may also carry its own color; without one, blocks get opacity steps in order of appearance, the same step for the same title. No schedule — no strip.

If your station is on AzuraCast, it's even easier

AzuraCast already publishes now-playing over SSE in exactly the shape this project expects, so you only fill in the station's own details — the provider handles the rest:

provider: 'azuracast',
channel: 'station:<shortcode>',    // the AzuraCast station shortcode

Point the azuracast provider at your installation once, in the same file:

export const providers = {
  azuracast: {
    sseUrl: 'https://your-azuracast.example.com/api/live/nowplaying/sse',
    dedupeBy: 'sh_id',
    historyOffset: 0,
    coverFallback: 'stationArt',
  },
}

Every station on that server now shares one SSE connection. Ten stations, one socket.

Another backend?

Add a provider next to azuracast. This project ships with three: AzuraCast and two Centrifugo bridges (centrifugo for omFM's own server and centrifugo386 for the one at 386.su). A provider describes the four things that actually differ between backends:

field what it answers
sseUrl where to subscribe
dedupeBy how to tell that the track changed (sh_id or the track text)
historyOffset does the history list start with the current track or the previous one
coverFallback when a cover lookup fails — use the station's art or a placeholder

Stations then simply say which provider they belong to. Mixed setups work: right now six stations come from AzuraCast, two from omFM's Centrifugo and two from the 386.su one, each with its own quirks, and no component knows the difference.


What you get per station, for free

  • Its own page at /streams/<slug> — hero, play button, spectrum analyzer, now playing, next track, history — plus prerendered SEO title and description, in all three locales.
  • A card on the home page and a slide in the "Top stations" carousel.
  • Entries in the header menu, the mobile menu, the stream picker and the /streams tabs.
  • A bubble in the player's swiper and in the mobile drop-up.
  • An SSE subscription, grouped with the other stations of the same provider.
  • Playlists for external players: /playlists/<slug>.m3u (Icecast) and /playlists/<slug>-hls.m3u (HLS), listed with the stream URLs on /about, plus the combined omfm-all.m3u and omfm-all-hls.m3u.
  • A schedule strip, if the station has a schedule.
  • Themed UI: the interface font, the radial glow and the accent colour follow the selected station.

Features

Playback — HLS via hls.js with an Icecast fallback, switchable at runtime. Volume, mute and the 10-band equalizer run through the Web Audio graph and persist between visits. Media keys and the OS now-playing widget are wired through the Media Session API.

Live data — one SSE connection per backend, automatic reconnect, per-track progress bars that keep ticking between updates.

Cover art — per station: either the artwork the backend sends with the track (artSource: 'station', optionally backed by iTunes via itunesFallback) or an iTunes lookup with caching (artSource: 'itunes'); when nothing is found, the provider decides between the station's own art and a placeholder. The dominant colour of the current cover is extracted and used to tint the player and the now-playing cards, with the text colour picked for contrast.

Visuals — canvas spectrum bars and a three-band waveform, plus optional film-grain effects (radial tint, noise, scanlines, flicker), all toggleable and remembered.

MilkDrop — a full-screen WebGL visualizer on butterchurn, the web port of Winamp's MilkDrop:

  • Turning it on and off — the atom button in the header (it turns green while MilkDrop is on) or the MilkDrop button at the top of the Effects panel. Both toggle the same switch.
  • Presets — the stock butterchurn-presets pack. A random one loads on start, and every 30 seconds the visualizer blends into a random preset over about 2.7 s. There is no manual preset picker.
  • Sound — taken from the player's analyser after the equalizer, so the picture follows what you hear; the stream itself is untouched. Turning MilkDrop on creates the audio context if the player hasn't yet; without sound the picture stands still.
  • While it runs — the canvas covers the page under the header and the player, which stay on top and usable; the header drops its background, the desktop nav links hide, and page scroll is locked.
  • Performance — the pixel ratio is capped at 1 on screens under 768 px and at 2 otherwise. Turning it off stops rendering; the WebGL context is kept for the next time.
  • Not remembered — every visit starts with MilkDrop off, on purpose: it is heavy on the GPU. A one-time "Now with MilkDrop!" hint points at the button until it is closed or the button is pressed.

Theme — dark and light, switched with a circular View Transition that expands from the click, and respecting prefers-reduced-motion.

Content — a blog and static pages on Nuxt Content v3, in English, Russian and Spanish.

Settings are applied through attributes on <html> before hydration, so a static build and a server-rendered one behave identically — no flash of default state.


Stack

Nuxt 4 · Vue 3 · Pinia · Tailwind CSS · Nuxt UI · Nuxt Content v3 · Nuxt i18n · hls.js · butterchurn · chroma-js · colorthief · Swiper


Setup

npm install

Development server on http://localhost:3000:

npm run dev

The dev server watches app/config/stations.ts and app/config/playlists.ts and reloads the configuration when they change — theme CSS and playlists are built from the registry.

Requires Node 22.19+ (Nuxt 4). Nuxt Content v3 uses better-sqlite3, which is a native module — on a slim Linux image you'll need python3, make and g++ to build it.

Deployment

Static hosting (Plesk, cPanel, any web server):

npm run generate

Copy the contents of .output/public into your web root.

Node server:

npm run build
node .output/server/index.mjs

Docker:

docker compose up -d