Ondographics

The app's own guide to the hub, published as it is. Source: ondographics-local/HUB.md

The hub and its free catalogue

Ondographics is a music hub and front end. It brings several ways of listening into one place: a free catalogue it serves itself, YouTube, and YouTube Music, Spotify, Deezer and SoundCloud, each opening in a window of its own. The hub is where you browse and play the free catalogue and YouTube, and it is the app's home.

You get to it with Menu → Music → Open the player, or tray → Music.

What it is

A catalogue of music its own artists have licensed for exactly this, served straight to the app's own front end. There is no login, no sign-up, no account to keep alive, no listening history to build, and nothing between pressing play and hearing the song.

You get shelves to browse (Featured, Trending this week, Just released, Chill, Lo-fi, Focus, Electronic, Synthwave, Hip-hop, Rock, Jazz, Classical, Ambient, Cinematic, World, Piano), a search over the whole catalogue, a queue, a seek bar, a volume control, and a live spectrum. Everything you play feeds the app's visualiser, so the forty scenes react to this music exactly as they react to YouTube Music.

Several services are read this way, and they are combined into one catalogue rather than switched between: Audius, a streaming network of independent artists, Jamendo, the licensed catalogue above, the Internet Archive, which holds a very large amount of public-domain and Creative Commons audio, from netlabel releases to live concert recordings to 78 rpm shellac, and YouTube, which is described below because it is the one that is not open by construction. A search asks all of them at once and interleaves the answers, so the catalogue reads as one library; Browse is their shelves merged into one list. None of the first three asks for an account or a key. Pages are walked rather than capped, so Load more keeps going and the shelf you are browsing is the whole catalogue rather than the first page of it.

YouTube, in the hub's own player

YouTube Music is the one service here the hub cannot simply open a page for. music.youtube.com refuses to be framed (X-Frame-Options: SAMEORIGIN), and an embed served from the app's own address would not carry your session anyway, so it would play with adverts and none of your library. The way through both is yt-dlp: a small program that reads a YouTube page the way a browser does and prints the media URL behind it. That URL is a plain HTTPS file that answers range requests, so the audio is proxied through the app's own server and plays in the hub's player, which also means the hub's spectrum and the app's visualiser are a reading of YouTube's music rather than a decoration beside it.

It downloads the program the first time you use this catalogue. About 17 MB, from the official yt-dlp release on GitHub, into the app's own data folder. Two things are worth knowing about that choice. It is deliberately not bundled in the installer, because yt-dlp has to be replaced often (YouTube changes underneath it) and a copy frozen inside a signed installer is a copy that stops working, and because it keeps a second unsigned executable out of the download you verify a checksum for. If you would rather it never touched the network, set sources.ytDlpAutoInstall to false; if you already have yt-dlp, point sources.ytDlpPath at it and nothing is downloaded. Either way the app works without it: the other catalogues and every service are unaffected, and the YouTube row simply reports that it is offline.

What it offers, signed out: three shelves (New music, Live sessions, Mixes and sets) and a search that joins in with the others. The three shelves are searched for at YouTube Music, through its own Songs filter, so a shelf is music and only music: a plain YouTube search for "new music this week" is answered with game streams and trailers beside the songs, and no rule over a title tells them apart. The search box is still the whole of YouTube, deliberately, because somebody looking for a video they know by name has to be able to reach it.

Signed in, three more appear from your own library, Liked on YouTube, Watch later and Subscriptions, because for the length of one run yt-dlp is given the cookies of the app's own YouTube session. Those three are your library rather than a catalogue of music - they hold whatever you saved, which is often a video and not a song - so they are offered under their own names and left out of Discover, which is a pile of music drawn at random. Only youtube.com, youtube-nocookie.com and google.com cookies are ever written to that file, it exists in a temporary folder for the seconds the run takes, and it is deleted as soon as the program exits.

The audio itself is read without your session, and that is deliberate rather than an oversight. The file behind a YouTube video is the same file for a signed-in listener and for an anonymous one. What the cookies change is which answer YouTube gives, and the signed-in answer is one that has to be decoded by a JavaScript runtime before any of its formats can be used. There is no JavaScript runtime here, so attaching the session is what stopped tracks playing: signed out, every video resolved; signed in, they came back "Requested format is not available". So playback reads the song's own stream rather than YouTube's player, which is also why there is no ad break inside it either way. What being signed in buys you here is your own library, and a second attempt for the videos that only play for an account.

The honest bit, again. This is the app reading YouTube for you rather than using YouTube's own player. It is not a YouTube API and it is not sanctioned the way the Spotify or Deezer windows are, and YouTube's terms do not cover separating the audio from the page it appears on. It is offered because you are already entitled to the music and the catalogue you paid for should be the catalogue you can use, and it is worth knowing what it is: use it for the music you would listen to anyway, and keep using YouTube Music's own window for anything that needs YouTube's own features.

The one catalogue that is this app's own

The Our Hub row is different from the rest, and the difference is not cleverness on this app's part. It is that the app serves that catalogue itself, from music whose own artists have licensed it for exactly this.

That distinction matters, so the platform row in the player says which row is which rather than leaving it to be discovered:

RowWhat actually happens
Our HubServed by this app from a catalogue it reads itself (independent artist networks, open libraries and a public cultural archive). No account, no sign-up, instant playback.
Video MusicOpens this app's own video music window, signed in as you, with whatever account you already have there.
Streaming ServiceOpens in a window of this app, with your own account. What you get there is that service's to decide, and this app does not change it.
Music ServiceOpens in a window of this app, with your own account. As above: this app does not alter what that service serves.
Creator PlatformOpens in a window of this app, with your own account. Free listening there is that platform's own.

This app does not strip anything out of a service you sign in to, and it never claims to. Those services license their music on their own terms and serve it with their own players. What the app offers instead is the one row it runs end to end, and a plain statement about what the others are.

Every service opens in a window of this app's own rather than in your desktop browser, so a click never throws you out of the app you are listening in. One caveat is worth knowing in advance: a service that plays through Widevine DRM will load and sign in and then refuse the audio, because Electron ships no Widevine. The video music and creator platform windows play because their audio is not encrypted; the two general services are the ones to watch.

Each catalogue's audio is served from this origin, and a playlist is joined up first. Every stream the hub plays is proxied through the app's own server, which is what makes the visualiser's spectrum a reading of the music rather than a decoration beside it. It is also where a track that arrives as an HLS playlist is turned back into a file. SoundCloud hands most of its tracks over as a playlist now, an audio element cannot decode one, and so the parts are fetched and joined into the one fragmented MP4 they already are (src/main/hls-audio.js): nothing is transcoded, the audio is the 96 kbps AAC SoundCloud sent, and the joined file is served with byte ranges, so it plays, reports its real length and seeks. A track that is a live stream, or is sent as segmented transport streams, or runs past 256 MB, is refused with a reason rather than fetched forever.

Why it is a window of its own

The hub is served by the app's own companion server rather than opened from disk, and that is a technical decision with a visible consequence.

A media element playing a cross-origin URL cannot be read by the Web Audio API at all. createMediaElementSource on one yields silence, with no error and no warning, which is why so many music visualisers are driven by a timer and look like they are dancing to nothing. So the audio is fetched from a route on the app's own server instead, which makes it same-origin, which means the page can actually read the samples it is playing. The spectrum in the player is a real reading of the music, and the levels it publishes are the same reading, handed to the visualiser.

The route only ever fetches a URL that came back from the catalogue API for a track id that has already been checked to be digits, so it cannot be pointed anywhere else on the network. It is guarded by the same token as every other data endpoint: if you have set one in Mixer and extras, the player carries it.

Because the player is a served page, it needs the companion server. That server comes up with the app and runs in the background, so picking the hub player never lands on a page explaining that the server is missing: it is the same server the overlay OBS reads, and opening the player will switch it on if something has switched it off.

Radio, taste, and where a song came from

The radio row is a set of mixes rather than fixed stations. Each click picks one of that mix's seeds at random, asks every catalogue at once at a random offset, shuffles what comes back and plays it, so the same button is a different run of songs every time. A mix that carries its own stream address is still played as a live station.

The two marks beside the title remember what you think of a song. A dislike is a promise rather than a score: the song is dropped from the grid, the queue and every future mix, so it cannot come back on its own. It is matched on the title and artist rather than on a catalogue id, so the same song offered by another platform is blocked with it. The link beside the marks opens the track's own page in your browser, which is how you check where a song actually came from before you keep it.

What the hub is playing is reported to the app, so the full-screen visualiser's HUD and the OBS overlay name the hub's track while the hub is the surface making the sound. It is not naming YouTube in the background. That report carries the service the song came from and its id there as well as the title, which is what lets the listening history (below) hold a song you can play again rather than only one you can read.

History, and playing a remembered song again

Everything heard is kept in one short list, however it was played: a track from the hub's own catalogues and a track from the app's YouTube window both arrive through the same now-playing state in main, so the Hub's History view is the whole of what has been on. The list is newest first, one row a song, and hearing a song again moves its row to the top rather than adding a second one; the cap is 300 rows.

Each row has a play button when the app can rebuild a stream for it. It can when the service and the song's id in it were recorded, or when the row is a YouTube watch link, in which case the video id is read out of the address. Rows it cannot answer for (a song from your own folder, or a report that named neither) are still drawn, and simply offer no button. A row plays on its own: the history is not a queue and does not play end to end, and a song you disliked is refused with a word rather than dropped silently.

A Twitch or channel-point song request follows whichever surface you are listening on. With the hub active the request searches the catalogues, YouTube first, and the first match is inserted straight behind the song that is on; YouTube hands over to Audius, Jamendo, the Internet Archive and SoundCloud when it has nothing, or when the downloader it needs is not on the machine. Not at the end, because the hub's queue is your own listening: an hour-long mix is one row of it, so "add to queue" there is a song somebody paid for that they will not hear tonight.

It waits behind a song, and is heard at once behind anything else. A request behind a song plays when that song ends, so nobody's listening is cut into. Behind a row that is not a song - a mix or playlist upload past a quarter of an hour, or a live station - waiting would be an hour of silence for something already paid for, so the request starts now and the row it interrupted keeps its place behind it. A row whose length nobody reported is waited behind as well, and that half is deliberate: a song whose listing omitted its length is the ordinary case, and cutting one on a guess is worse than waiting behind a row that turns out to be long. The distinction is a real one from this stream: a redemption arrived behind a one-hour "R&B Playlist" upload that did report its hour, went next as the old rule said, and was still unplayed when the session ended - which also left it in the request list, so the same song redeemed again an hour later was refused as a duplicate.

The transport row then says who asked and where the song went, Joshtified asked for … · playing next or · playing now, because the request panel is another window and a row appearing silently in a queue you are not looking at is a request you cannot tell from one that never arrived. Several redemptions at once keep the order they were asked in: the request list hands them over one at a time and waits for the one ahead to be reached, exactly as it does in Play next mode. With the video player active it is the YouTube Music search-and-queue it always was, and there the panel's own mode decides, as it always has.

Your own song goes next the same way. Every list in the hub - a search result, a shelf, a playlist, an album - carries a Play next beside its play button, and it follows the same rule a request does: the row lands straight behind the one on air rather than at the back of a queue that can be two hundred songs long, the song that is playing is not cut off, and a row that is not a song - a mix, or a station - is not waited behind, so a song put next while one of those is on is heard now. The difference is whose it is: a request is marked and carried across a queue that gets replaced, because somebody paid for it, and your own song is yours to replace.

One transport, whichever surface is playing

Every door that presses something goes to the surface you are listening on: a Stream Deck button, the tray's own transport, the Playback menu, the media keys on the keyboard, a chat command from the one viewer allowed to use them, and the buttons in the request panel. The app decides from the surface it is reading, so switching between the hub and the video player changes what a button does without any of them being told; switching to Spotify, Deezer or SoundCloud, which play in a window of their own, leaves those buttons doing nothing rather than quietly skipping a YouTube page nobody can hear.

The hub answers play, pause, toggle, next, previous, volume, volume-by, mute, seek-by, seek-to, like and dislike, its own liked and disliked lists being the last two. The video player answers everything it always did. Nothing is sent to the page that is not playing.

The client id

The catalogue API is called with a public application id, which travels in the clear in every request and is expected to be visible in client code. It lives in the config so a fork or a heavy user can register their own instead of sharing this one's rate limit:

{
  "sources": {
    "active": "hub",
    "jamendoClientId": "da4dde8f"
  }
}

A Stream Deck, or anything else that can open a URL

The companion server answers one more route than it used to, and it is the only one that changes anything: /api/control. A button on a deck holds a URL per key, so that is what a button is.

http://127.0.0.1:9863/api/control?action=next
http://127.0.0.1:9863/api/control?action=toggle
http://127.0.0.1:9863/api/control?action=volume&value=40

Copy control links for a deck or a remote in the tray menu puts the whole list on the clipboard, one per line, with what each one does in front of it, and with the token already inside every address when the server has one. That is the intended way to get them: nobody should be typing a port and a token ten times over.

The actions are play, pause, toggle, next, previous, like, dislike, mute, louder, quieter, volume, seek, show and hide. volume and seek read value, so a dial can be a dial instead of a pair of step buttons. The answer is JSON and carries whether it was taken and what is playing now, so a button that can read its answer can say what it did; a button that cannot still works.

Two things guard it. A request that carries an Origin header is refused, because that is a browser saying a page made the request rather than a person, and a page you happen to have open must not be able to pause your music. And the server's own token applies as it does everywhere else, so a server with a token set needs &token=... on every button. Neither is a substitute for the other: the port is on the loopback address only and the host header is checked, as it is for every other route.

The server has to be running for any of this, which is the same condition as the overlay and the hub itself. The tray item is greyed out when it is off.

Troubleshooting

Nothing plays, and the shelf is empty. The catalogue could not be reached. The page says so in the results area rather than showing an empty grid. Check your connection, and check that the client id above is the one you meant.

The window says the companion server is needed. It starts with the app, so this almost always means something else owns its port. Close whatever is using it, or pick another one under OBS in the tray menu, then open the player again.

A track skips after a moment. A stream that will not play is skipped rather than stopped, so a single bad entry does not end the listening session. If every track skips, the server is not reaching the storage host. A SoundCloud track that skipped for no visible reason was a playlist being served as though it were a file: SoundCloud hands most tracks over as an HLS playlist now and an audio element cannot decode one, so the app joins the playlist's parts into one file before serving it. A track that still skips is one the app refused by name - a live stream, a segmented transport stream, or a track past 256 MB - and the app's log says which.

The YouTube row says offline, or its shelves are empty. The program the catalogue reads YouTube with was not downloaded, which means one of three things: there is no network, automatic install was switched off (sources.ytDlpAutoInstall), or Windows refused the download. The folder it goes in is <user data>\bin\yt-dlp.exe, and pointing sources.ytDlpPath at a copy of yt-dlp you already have skips the download entirely. Everything else in the app works without it.

The hub and YouTube Music were both playing at once. Fixed. Two pages in this app can make a sound, and until recently nothing stopped the one being left behind: pressing YouTube Music while the hub was playing left both of them going, which is two songs in one room and reads as a muddle, or as a hollow echo when it was the same track from both. Choosing a surface now pauses the other, so opening the hub stops the player window and choosing YouTube Music stops the hub. Bringing the hub forward no longer reloads it either, so coming back to it finds the queue and the track where you left them rather than starting the song again.

The visualiser is not moving while the hub plays. The visualiser reads exactly one surface: whichever one you chose to listen through. Picking the hub makes the visualiser read the hub, and picking YouTube Music makes it read YouTube Music, so a service you did not choose never plays behind the one you did. If it is still, check that something in the hub is actually playing.