How It Connects
Independent grants, once each
Oxidify uses independent credentials for Web API coverage, optional personal acceleration, and local playback:
- The shared Web API app provides broad catalog and playlist coverage.
- Your optional personal Web API app handles supported playback, library, catalog, playlist creation, and owned or collaborative playlist requests without using the shared app’s quota. Complete playlist-library views and playlist-bearing search stay on the shared app so Spotify-owned results are not filtered out. Both Web API grants must verify as the same Spotify account.
- Streaming is actually playing audio on this computer, through librespot. This runs the same browser flow once against Spotify’s streaming client identity, after which librespot stores its own reusable credential. Premium is required, because that is what Spotify’s streaming protocol requires.
Local playback authorization stays separate from both Web API grants. Before Oxidify opens either browser flow, it starts the loopback receiver that accepts the redirect. The main sign-in waiting screen can also reopen or copy its URL. If another app holds the port (8989 for the Web API, 8898 for playback), the browser stays closed and the error stays on screen or in Settings until you close that app and try again. A saved playback approval is kept.
By default the Web API uses the shared public application also used by spotify-player, ncspot, and Omarchy Spotify, whose allowance Spotify divides among everyone running any of them. An application of your own adds a separate Development Mode quota; Make It Even Faster shows how to add one.
Playback state is checked every 15 seconds, every 20 while this computer’s player is active, every 30 while the window is minimised or in the tray and nothing plays, every 4 for ten seconds after a command or a switch to another device, once more just after a song on another device should end, and as soon as this computer’s player stops. The requests are the same ones as before, only less frequent; none of this applies to alternate local audio, which makes no playback request to Spotify.
What the client stores
- The shared and personal Web API refresh tokens and librespot’s reusable credential, owner-only, in the state directory (where).
- Downloaded audio, in the cache directory, within the budget you set.
- Artwork, in the cache directory, trimmed to 256 MiB at each start by removing the oldest covers first.
- Lyrics, in the cache directory, for a month.
- The first page of full Top songs (up to 50 tracks), in
top-tracks.jsonin the cache directory. This single file stores one account’s metadata, at most 1 MiB; another account’s refresh replaces it. Entries expire after six hours and are read only after the Web API gateway verifies the Spotify account. Cached metadata appears before the normal Spotify refresh; no requests are skipped and no tokens are stored in this cache. Home previews and later pages are not cached. - Account-scoped playlist pages, in
playlists/<account-id>/, at most 32 MiB each. A hit is shown immediately, then Spotify confirms or replaces it. Tokens are not stored. Settings can delete this library cache, together with the library snapshot. - Oxidify has no telemetry, analytics, or hosted service. Besides Spotify
and its album art CDN, the app contacts
lrclib.net while the lyrics panel is open and
Spotify itself has no words for the track, sending the track’s artist,
title, album, and length, and to api.github.com once a day to learn whether
a newer stable Oxidify release exists. Settings can turn off automatic
checks or request a check immediately. Checks time out after 15 seconds;
manual failures are reported, and daily failures do not interrupt playback.
Clicking Update available opens that release in the default browser.
The app never downloads or installs an update itself. With Settings →
Show on Discord turned on (it is off by default), the app hands the Discord
app running on this computer, over its local IPC pipe, the title, artists,
album, cover link, Spotify link, and timing of the song playing here, and
Discord shows it on your profile. Oxidify itself makes no request to
Discord’s servers, and nothing is sent while the setting is off, nothing
plays here, or Discord is not running. Alternate local audio,
selected for Free or unconfirmed accounts and available in Settings, uses a
native YouTube resolver and can also contact the Piped endpoint you chose.
These searches run concurrently with short timeouts. A strong result cancels
slower work. Matches are remembered in a bounded file on this computer,
bin/alternate-matches.jsonunder the state directory: search results for seven days (one with no confident match for 15 minutes, and a match is forgotten when its video fails to play) and resolved stream URLs until the URL’s own expiry less five minutes, so a song played again starts without a lookup. It holds the search keys made from a track’s artists, title and length, video ids and stream URLs, and never a Spotify token. A local yt-dlp process (the official pinned build extracted on this computer, or a strictly newer one you installed) resolves the audio URL, racing Piped when one is set, and searches when the others find no match. Spotify tokens are never sent to an alternate provider. yt-dlp is never downloaded at runtime. YouTube cookies added in Settings stay in the credentials directory; each yt-dlp run gets its own copy in an owner-only scratch folder, deleted when the run ends or, if the run is cut short, when alternate playback next starts. No other request carries them.
Refreshing a page
The circular arrow immediately left of the Settings gear reloads only the current page. Top songs skips its disk-cache read and starts a new live request even if an older load is still running; older responses cannot replace the refreshed view or overwrite a newer cache write. Playlist refresh requests both current metadata and items instead of restoring snapshot-cached items, and ignores cache reads from before the refresh. Opening a playlist still shows a matching disk copy first, then updates it if Spotify’s snapshot changed. Successful loads can update those metadata caches. Audio, artwork, lyrics, and credentials are untouched. Other pages reuse their existing reload behaviour. Search repeats the current query; refresh is disabled in Settings, which has no page data to reload.
Refresh uses the normal API gateway and honours its concurrency limits and
Retry-After cooldowns. It does not bypass Spotify’s rate limits.
When Spotify pushes back
The full Top songs page uses the foreground request lane rather than waiting behind the four-request background queue used by Home previews and sidebar loading. It still shares the API’s six-request concurrency limit and rate-limit cooldown; foreground priority does not bypass Spotify limits.
Each Web API session has its own concurrency and cooldown. Oxidify honours
Retry-After without pausing the other session and treats Development Mode
quota exhaustion separately from an ordinary burst limit. Requests wait out
a cooldown of up to 30 seconds; during a longer one, or for five minutes
after quota exhaustion when Spotify names no wait, they fail at once
instead of reaching Spotify. A logical request
is routed once before dispatch and is never retried through the other app.
Receivers on the local network
Spotify’s device list contains only receivers already logged in to the account. Anything waiting to be given one, which is the normal state for a self-hosted librespot or spotifyd, is invisible to the Web API.
Those receivers announce themselves over mDNS as _spotify-connect._tcp and
answer a small HTTP interface. Oxidify asks a receiver to describe itself,
then hands over the reusable credential librespot already stores, wrapped
twice: once in a key derived from the receiver’s own device id, and again in
a key both sides derive from a Diffie-Hellman exchange with the public key
that receiver just published. The encrypted value is specific to that
receiver and exchange. Oxidify does not write another copy of the
credential.
The receiver then signs in and appears in Spotify’s device list. Oxidify uses the Web API for subsequent control requests. The handover itself is local-network traffic and never goes through a proxy.
Proxies
Outbound traffic follows one proxy setting. system (the default) leaves
routing to environment variables and the platform; off connects directly
no matter what the environment says; http and socks5 route traffic
through one configured proxy. The Web API client signs in to that proxy
when a user and password are given; the password is read once at startup
from its own file in the credentials directory and never logged. With
http, librespot receives the same proxy address but never credentials:
its own clients cannot carry a proxy sign-in, so streaming needs a proxy
that accepts anonymous CONNECT, and access-point discovery through
apresolve.spotify.com also goes through the proxy. librespot only speaks
HTTP CONNECT, so with socks5 it connects directly and only the Web API,
artwork, lyrics and alternate playback use the proxy. A changed proxy takes effect on the next
start; the engine keeps its current routing until then.
The engine
Playback runs on a dedicated runtime: librespot maintains the Spotify Connect session, so this computer appears as a device to every other Spotify client you own, receives transfers, and reports its position back. If the session drops, the engine reconnects with the stored credential; the interface never blocks on any of it. Replacement engines preserve the last volume heard during an automatic reconnect, while an explicit settings change remains authoritative. This applies to the local Spotify engine and does not change account eligibility.
A connection that fails for any reason other than Spotify rejecting the credential, such as a reset after the computer wakes, is tried again with the stored credential after 5 and 15 seconds, 1, 2 and 5 minutes, then every 10 minutes, without flickering through “Connecting”. Enable playback here and Try again reconnect with the stored credential first and open the browser only when there is none or Spotify rejects it.
The engine discovers access points
through apresolve.spotify.com and connects over TCP in the resolver’s
preference order: port 4070 first, falling back to 443 and 80. Only outbound
connections are needed; no inbound ports have to be open.
Each socket setup and handshake has a five-second deadline. The pinned librespot can try up to six access points and retry each connection once, so Oxidify allows 75 seconds for the whole engine connection. Resolving access points and authentication share that deadline, so reaching every fallback endpoint is not guaranteed.
The opt-in alternate engine is a separate session. It does not announce a
Connect device, does not use Spirc, and does not run at the same time as
librespot. It resolves Spotify track metadata to a third-party match. Native
YouTube and optional Piped searches share one async HTTP client; the existing
match score ranks their candidates together. Playable stream URLs come from
Piped and/or yt-dlp (AAC M4A, itag 140 first, then MP3). rusty_ytdl is search-only:
it does not decrypt adaptive media URLs. The engine fetches by HTTP range and
decodes AAC/M4A or MP3 locally. Playback starts when container headers and
the first packets are in (a few frames for MP3; fast-start M4A once moov
is present). Fast-start M4A (moov before mdat) can start while the rest
downloads; moov at the end waits until the file is complete. Alternate
stream selection rejects Opus, WebM, and Ogg/Vorbis. YouTube’s useful
native alternative is WebM/Opus, Opus is not decoded, and enabling Ogg
adds no useful YouTube startup path. If the UI already has title and artist,
a track click does not wait on Spotify /tracks/{id}. A miss can skip to the
next track if you enable that setting. Network stalls and transient HTTP
errors retry with bounded backoff, keep received ranges, and refresh expired
media URLs; several retries in a row without new bytes count as a terminal
failure. A terminal transport or decode failure stops the current item.