Heart the track a Roon zone is playing, and add it (or its album) to your Roon library, from your own Node.js program.
Roon's official extension API can't do either of these. This small library talks to the Core the way the Roon desktop app does, over Roon's internal protocol on port 9332. It works today (tested on Roon 2.73), but it is unofficial, unsupported and experimental. Please read the warning below before you use it.
Built for the Roon: Dialed Up Stream Deck plugin and Roon: Toasted. Not affiliated with or endorsed by Roon Labs.
Warning
This library uses an internal Roon protocol that Roon does not support. Read this before you use it.
Brian Luczkiewicz of Roon Labs, in the forum thread this work builds on: "This protocol is engineered as an internal interface to be used by trusted code, and is not hardened for 3rd party use." He also said the protocol is meant to be internal, that Roon changes it freely, and that misuse of it can easily cause memory or compute leaks in the Core. That applies to this library too.
- Any Roon update can break it, without warning. It is tested on Roon 2.73 and nothing else. Don't build anything on it that you can't live without.
- Used carelessly, it can hurt your Core. Every connection is a full session on your Core, and the Core holds the objects it sends you until the session ends. This library cannot promise to cover every case Roon had in mind. Don't leave it running unattended against a Core you can't afford to restart, and watch your Core's memory the first time you use it.
- It is an optional extra, not a foundation. Make it opt-in or give your users an off switch, and when it fails, hide the controls instead of retrying.
- Keep it on your LAN. There is no authentication on this protocol, so never expose port 9332 beyond your own network.
- A deliberately small surface. It reads what a zone is playing, hearts and un-hearts, and adds to the library. It cannot ban, remove or delete anything, and nothing else is exposed.
- No blind retries. Changes wait for the Core to confirm them, the library never retries calls on its own, and when the Core turns it down it throws
UnsupportedCoreErrorso your app can stop and hide the feature. - Sessions that don't pile up. One connection per program. The client pings every 10 seconds so a quiet session isn't dropped and reopened over and over, and you should close and reconnect once a day (or when
objectCountgets large) so the Core can let go of what it sent. See Good to know. - Backing off is yours to do. The library never reconnects by itself. Roon: Dialed Up and Roon: Toasted both retry after 5 s, 15 s, 60 s, then every 5 minutes.
- Reports what each zone is playing, with its heart and in library state, live (the Core pushes changes to you, including hearts set in Roon itself).
- Heart / un-heart the playing track.
- Add to library: the playing track on its own, or its whole album (Roon's own "+" adds the album; many people prefer single songs, others don't want loose tracks in their library, so both are here).
- Heart / un-heart the album.
On purpose it does not ban, remove from the library, or delete anything.
npm install github:Vulkandr/roon-library-controls
Full reference, with every option, method, event and error: docs/API.md.
Node 18 or newer. No dependencies.
You need two things every Roon extension already has: the Core's address and its Core id (the unique_id from SOOD discovery, or core_id from the extension API). Zone ids are the same strings the extension API gives you.
const { RoonLibraryClient } = require('roon-library-controls');
const roon = new RoonLibraryClient({
host: '192.168.0.68',
coreId: 'fafb763c-9ad0-4f07-887d-44e19b8374e0',
});
roon.on('track', (zoneId, track) => {
// fires on connect, whenever a zone's track changes, and whenever its heart / library state changes
console.log(zoneId, track?.title, track?.favorite ? 'hearted' : 'not hearted');
});
roon.on('disconnected', (reason) => console.log('lost the Core', reason?.message));
await roon.connect();
const track = roon.trackForZone(zoneId); // TrackInfo or null
await roon.setFavorite(zoneId, true); // heart; adds a single track to the library first if needed
await roon.setFavorite(zoneId, false); // un-heart
await roon.addToLibrary(zoneId, 'album'); // 'track' or 'album'
await roon.setAlbumFavorite(zoneId, true); // heart the album (it must be in the library)
roon.close();setFavorite(zoneId, true, { addToLibrary: 'album' }) adds the whole album first instead of the single track; { addToLibrary: false } throws NotInLibraryError instead of adding.
{
zoneId: string,
title: string,
trackId: string | null, // Roon's id for the track; changes when it is added to the library
inLibrary: boolean,
favorite: boolean,
banned: boolean,
source: 'local' | 'streaming' | null,
album: { title, roonAlbumId, inLibrary, favorite } | null
}UnsupportedCoreError: the Core turned us down. Wrong Core id, or a Roon version whose protocol this library doesn't speak. This is the one to catch to hide your heart button and tell the user "not supported with this Roon version".NothingPlayingError: the zone has nothing loaded (or the zone id is unknown).NotInLibraryError: you asked to heart without adding, and the track isn't in the library.CallError: the Core answered a call with a failure status.- Plain
Error: network trouble, timeouts, or the Core never confirming a change.
- Roon only hearts tracks that are in the library, which is why hearting adds first.
- Hearts are per Roon profile. The client uses the profile the Core hands it on connect.
- The Core keeps every object it has sent us for as long as the session lasts, and it sends a few for every track that plays. A long-running program should call
close()andconnect()again now and then (say once a day, or whenobjectCountgets large) so the Core can let go of them. - Keep one connection per program; don't open a new one per button press.
- Everything happens on your own network, against your own Core, with no credentials involved. There is no authentication on this protocol locally, which is also why you should never expose port 9332 beyond your LAN.
The Roon desktop app talks to the Core over a binary remoting protocol (Sooloos.Broker.Remoting) on TCP 9332. After a short handshake (the "server broker id" is just the Core id in .NET GUID byte order) the Core streams its object graph: zones, what they're playing, tracks and albums with their heart and library flags. Hearting is Library::FavoriteOrBan on the track object; adding is Library::AddToLibrary. The protocol was reverse-engineered by Arthur Soares; this package is a small, cleaned-up port of the parts needed for these features. Brian Luczkiewicz, Roon Labs founder, said in the forum thread: "We don’t mind people tinkering with their own systems, and reverse engineering for the purpose of interoperability is generally not restricted legally unless encryption was broken, which is not the case here, so we are not going to do anything to interfere with this project." He was just as clear about the risks, which are in the warning at the top of this page. Read it before building on this.
npm install
npm test # compiles and runs the unit tests (they use bytes captured from a real Core)
ROON_HOST=192.168.0.68 ROON_CORE_ID=<your core id> npm run example
The example lists every zone's track with its state and then watches for changes; node dist/examples/heart-now-playing.js heart <zoneId> hearts what that zone is playing (unheart, add <zoneId> track|album, heartalbum, unheartalbum likewise).
MIT. Portions ported from roon-api-reverse-engineering by Arthur Soares, also MIT; see NOTICE. Roon is a trademark of Roon Labs LLC.