npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

radio-now-playing

v1.0.0

Published

What is playing on an internet radio station, right now. Reads ICY stream metadata or a Shoutcast/Icecast status endpoint, hangs up immediately, and caches so a hundred callers cost one request. Zero dependencies.

Readme

radio-now-playing

npm ci zero dependencies

What is playing on an internet radio station, right now.

Reads the track title from a Shoutcast or Icecast station — either out of the stream's own ICY metadata or from its status endpoint — and hangs up as soon as it has the answer, so a lookup does not sit in one of the station's listener slots. Ships a cache that collapses any number of callers into one request upstream.

Zero dependencies. Native fetch. TypeScript types included. ESM and CommonJS.

npm install radio-now-playing

Quick start

import { nowPlaying } from 'radio-now-playing';

const track = await nowPlaying('https://example.com/stream');
console.log(track);
// {
//   title: 'Miles Davis - So What',
//   artist: 'Miles Davis',
//   song: 'So What',
//   listeners: null,
//   source: 'icy',
//   fetchedAt: 1755960000000
// }

null means the station answered and nothing is playing. A station that is unreachable, or that carries no metadata at all, throws a RadioError — those are different situations and you usually want to treat them differently.

Serving it to a web page

Do not call nowPlaying per visitor. A page polled by a hundred people would be a hundred requests to a station that has a few hundred listener slots total. Use the client:

import { NowPlayingClient } from 'radio-now-playing';

const radio = new NowPlayingClient({ ttlMs: 15_000 });

app.get('/api/now', async (req, res) => {
  const { track, stale, checkedAt } = await radio.get(station);
  res.json({ title: track?.title ?? null, stale, checkedAt });
});
  • At most one request upstream per station per ttlMs, however many callers arrive — including callers that arrive while a request is in flight.
  • get() never rejects. If the station is down you get the last title you had, with stale: true and the failure in error. A dropped request does not mean the music stopped, and a UI that blanks every time a station hiccups is worse than one that is fifteen seconds behind.

Use the status endpoint when the station has one

Reading the stream, however briefly, takes one of the station's listener slots. A status endpoint does not. If you know the station's, give it:

// Shoutcast v2
await nowPlaying({
  url: 'https://example.com/stream',
  statsUrl: 'http://example.com:8000/stats?sid=1&json=1',
});

// Icecast — one endpoint serves every mount, so name yours
await nowPlaying({
  url: 'https://example.com/live',
  statsUrl: 'http://example.com:8000/status-json.xsl',
  mount: '/live',
});

The two formats are told apart by the shape of the response, not by the URL, so you do not have to declare which server the station runs.

Status endpoints also report listener counts, which ICY never does.

If the status endpoint fails, this throws rather than quietly reading the stream instead — configuring statsUrl is how you say "do not take one of my listener slots", and undoing that at the exact moment the station is already struggling is not helpful. Opt in with { fallbackToStream: true } if you would rather have the title.

API

nowPlaying(station, options?): Promise<NowPlaying | null>

station is a URL string, or { url, statsUrl?, mount? }.

| Option | Default | | |---|---|---| | timeoutMs | 6000 | Abandon the request after this long. | | maxBytes | 262144 | Stop reading audio while waiting for a metadata block. | | encoding | 'auto' | 'auto', 'utf-8' or 'windows-1252'. See below. | | userAgent | package name | Some stations reject a blank one. | | signal | — | An AbortSignal of your own, combined with timeoutMs. | | fallbackToStream | false | Read the stream if the status endpoint fails. |

new NowPlayingClient(options?)

Takes everything above plus ttlMs (default 15000).

  • get(station) → Promise<CacheEntry>, never rejects
  • peek(station) → CacheEntry | undefined, no lookup
  • clear(station?) → forget one station, or all of them
type CacheEntry = {
  track: NowPlaying | null;
  stale: boolean;          // the last attempt failed; track is older than checkedAt
  error: RadioError | null;
  checkedAt: number;       // when an attempt last finished, successful or not
};

Parsing helpers

Exported because plenty of people already have the bytes and only need them read properly:

import { parseStreamTitle, splitTitle, decodeMetadata } from 'radio-now-playing';

parseStreamTitle("StreamTitle='Guns N' Roses - November Rain';");
// → "Guns N' Roses - November Rain"

splitTitle('Miles Davis - So What');
// → { artist: 'Miles Davis', song: 'So What' }

RadioError

Thrown for transport and protocol failures, never for "nothing is playing". error.code is one of:

| code | meaning | |---|---| | timeout | No answer, or no metadata block, in time. | | http | The station answered with an error status (see error.status). | | no-metadata | The stream sends no icy-metaint at all. Retrying will not help. | | bad-response | Connection failure, or a status endpoint that did not return JSON. |

Things that will bite you, and what this does about them

ICY has no encoding field. Stations send UTF-8 or Windows-1252 and there is no header saying which, so naive UTF-8 decoding turns Björk into Bj�rk. The default 'auto' decodes as UTF-8, and falls back to Windows-1252 if the result contains a replacement character — valid UTF-8 essentially never produces one, and Windows-1252 text with accented letters essentially always does.

"Artist - Song" is a convention, not a format. splitTitle splits on the first spaced separator, so Jay-Z and re-mastered stay intact but Scarborough Fair - Canticle keeps its dash in the song rather than the artist. There is no parse that is right for every station, which is why the raw title is always there too — show that, and treat artist/song as a hint.

Stations park junk in the title between songs. Unknown, N/A, - and friends are reported as nothing playing so you can keep showing the last real track.

A zero-length metadata block is not an error. It is the station saying nothing has changed, and it means "no title right now".

Requirements and limits

  • Node 20.3+. Uses fetch, AbortSignal.any and TextDecoder. The bundled types reference AbortSignal, so a TypeScript consumer needs @types/node or "lib": ["dom"] — any Node project already has the first. No browser build: browsers cannot set the Icy-MetaData header or read a cross-origin stream body, so this is a server-side library by nature.
  • Shoutcast v1 servers that answer ICY 200 OK instead of an HTTP status line cannot be read by fetch at all — nothing built on the platform HTTP client can. Modern Shoutcast (v2, which answers HTTP/1.0 200 OK) and every Icecast are fine. For a genuine v1 server, use its /stats?json=1 endpoint.
  • Playlist URLs (.m3u, .pls) are not resolved. Point at the stream.

Prior art

icy and icecast-parser give you a streaming client that stays connected and emits metadata as it changes. That is the right tool if you are playing or relaying the audio; it is the wrong one for a "what's playing" endpoint, because it holds a listener slot for as long as it runs. node-internet-radio does the same one-shot job as this, with callbacks and a dependency on the deprecated request.

Contributing

npm install
npm run check     # typecheck, 46 unit tests, 7 packaging tests

The tests run against a real HTTP server on loopback rather than a mocked fetch — this is a protocol reader, and mocking the transport would test the mock. test/station.ts is a fake station that serves genuine ICY-interleaved audio and the specific ways real stations misbehave.

npm run check also runs in prepublishOnly, so a broken build cannot be published by accident.

Licence

MIT