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

upnext-adapter-nowplaying

v0.3.1

Published

Read and control whatever macOS is playing — a browser tab, VLC, Doppler, anything that publishes to Control Center.

Downloads

566

Readme

upnext-adapter-nowplaying

npm license

Read and control whatever your machine is playing — including a browser tab — without an extension. macOS and Linux.

Both platforms keep a system-wide register of what is playing: on macOS the one Control Center shows and your keyboard's play/pause key talks to, on Linux the MPRIS interface players publish on D-Bus. Anything that publishes to it is reachable — a YouTube tab in Chrome, a podcast in Safari or Firefox, VLC, Doppler, Music, Spotify.

Same entry, same code, either OS. A host should not have to care which one it is on, so this package answers that question itself.

import { readNowPlaying, sendTransport } from 'upnext-adapter-nowplaying';

await readNowPlaying();
// { bundleId: 'com.google.Chrome', label: 'Google Chrome', playing: true,
//   title: 'Acquired — Jensen Huang', artist: 'YouTube',
//   elapsedMs: 60000, durationMs: 3600000 }

await sendTransport('pause');   // pauses it, whatever it is

On Linux the same call answers from MPRIS instead, and the reading has the same shape — bundleId is the player name (firefox, vlc, spotify) rather than a bundle identifier.

That works with no browser extension, no accessibility permission, and no per-site integration. On Linux it needs playerctl installed; on macOS it needs nothing at all.

If you specifically want one register rather than whichever this machine has, readMediaRemote / sendMediaRemote and readMpris / sendMpris are exported under their own names.

As a queue entry

The interesting use isn't observation — it's not interrupting people.

import { Runtime } from 'upnext-core';
import { NowPlayingAdapter, NOW_PLAYING_URI } from 'upnext-adapter-nowplaying';

const runtime = new Runtime({ adapters: [new NowPlayingAdapter(), ...others] });

runtime.enqueue(NOW_PLAYING_URI);                 // let their podcast finish
runtime.enqueue({ title: 'Bad Habit' });          // then take over

await runtime.play();

An agent adding to someone's listening instead of cutting across it. When their episode ends, the queue moves on by itself.

And if they skip to something else in that tab, the runtime notices and adopts it — their choice wins, which is the default everywhere in upnext.

What it can and cannot do

This adapter answers to exactly one entry, nowplaying:current, meaning the thing that is on. It cannot be handed a track, because there is no way to ask the register to start one — so it scores zero for every other locator rather than claiming it could.

{
  endOfTrack:      'poll',           // the register does not notify; we sample it
  position:        'authoritative',  // elapsed comes from the player itself
  pause:           true,
  seek:            false,            // the bridge cannot pass seek options cleanly
  volume:          false,            // the register exposes no volume
  search:          false,
  externalControl: true,             // the strongest case in the library
}

externalControl has never meant more than it does here. The runtime is a guest: the item was chosen by somebody else, in an app it does not control, and it can change underneath at any moment.

stop() pauses rather than quitting. The queue moving on is not a reason to take somebody's podcast away from them.

How it works, and the honest caveats

Two registers, one shape. sourceFor() picks by platform at init(), and everything above it — the adapter, the URI, the reading — is identical.

Linux: MPRIS, through playerctl

MPRIS is a D-Bus interface that most Linux players implement: browsers, VLC, mpv with a plugin, Spotify's Linux client. playerctl is its standard command-line client, and this shells out to it for the same reason the macOS side shells out to osascript — the protocol is somebody else's moving target, and a process boundary is the right place to keep it.

One thing makes that safe rather than fragile: playerctl takes a --format template, so the output shape is ours. Rather than parsing whatever a tool decided to print, this asks for exactly the fields it wants, in exactly the order it wants them, separated by ASCII 31 — because real track titles contain every printable delimiter worth using.

That is also why it can be tested honestly. CI runs a real playerctl against a real MPRIS player on a real session bus, so a template typo fails the build rather than a user's machine. Without a player on the bus, playerctl exits on "No players found" before it ever reads the template — which means a green test run with no player proves nothing, and this one does not do that. See scripts/mpris-ci.sh.

Install it with apt install playerctl, dnf install playerctl, or your distribution's equivalent. Without it, init() says so in a sentence.

macOS: MediaRemote

It reaches MediaRemote, a private Apple framework, through JXA's Objective-C bridge. Private API is normally a bad trade, so:

  • There is no public equivalent. MPNowPlayingInfoCenter publishes your own app's state. Nothing public reads another app's.
  • Apple began gating MediaRemote behind an entitlement in macOS 15.4, which broke the usual command-line tools. The gate applies to processes asking directly; script execution through osascript still resolves it. Verified working on macOS 26.5.2.
  • The alternative is worse. Shipping a helper binary that borrows a system binary's entitlements is a far larger and more fragile dependency than a script you can read in full — and you can: it's in src/mediaremote.ts, about forty lines.
  • Every failure answers null. The day Apple closes this, the adapter reports itself unavailable, the registry excludes it, and the rest of your queue carries on.

Windows is not implemented. It has an equivalent — SMTC — and the adapter's shape would carry over, but nobody has written it. On any unsupported platform init() fails cleanly and getState().adapters shows available: false with the reason, rather than the adapter pretending and returning nothing.

It filters out things that aren't media

The now-playing client is whatever last made a sound. A received voice message leaves Messages sitting there as the now-playing app, with no title and a seven-second "track". Requiring a title and a real duration is what separates something is playing from something made a noise once. Both platforms get the same filter, because both have the same problem.

Compared with the other adapters

| | starts tracks | who chose it | can be changed under you | |---|:---:|---|:---:| | upnext-adapter-local | ✅ | you | no | | upnext-adapter-browser | ✅ | you | no | | upnext-adapter-spotify | ✅ | you, mostly | yes | | upnext-adapter-nowplaying | ❌ | somebody else | yes |

This is the far end of the capability spectrum the whole library is built around — and a useful proof that the contract stretches that far without bending.


Full docs: https://github.com/tothienbao6a0/upnext · Apache-2.0