Offline music¶
sw.js is a service worker shared by both UIs. It only runs over HTTPS or on localhost, so over plain http://192.168… it's absent and everything still works online. It caches:
- Bootstrap and its icons (
vendor/), from its own cache first: kept when the worker installs, old versions dropped whenVENDOR_FILESchanges. - Album covers. Their URLs contain the image's hash, so a cached copy never goes stale.
- Every song played in this browser, up to a per-device limit (1 GB by default). The least recently played songs are dropped first.
It also keeps the app's own files: both apps' pages, scripts, styles, images and the manifest, matched by APP_PATH in nsy-app-v1. These are fetched network first, so every load online gets the latest and a deploy still shows on the next reload. The kept copy is used only when the network fails, or takes longer than APP_TIMEOUT (4 s). Only plain same-origin answers are kept, never a sign-in page that a proxy redirected to, and never API answers.
"Network first" means the server, not the browser's HTTP cache. The server sends every app file with Cache-Control: no-cache (see How a request is handled), and app_file() fetches scripts, styles and images with cache: 'no-cache', so copies a browser cached before that header can't outlive it. A navigation can't be rebuilt with options, so pages rely on the server's header alone.
Downloads¶
An album, playlist or mix kept on purpose. downloads.js builds a snapshot from the server and sends it to the worker (download-save):
{ id: 'album-5', kind, name, subtitle, href, art, thumb,
source: { kind: 'album', id } | { kind: 'playlist', id } | { kind: 'mix', key, libraryId, param, name },
tracks: [TrackOut…] }
- Storage: the worker stores snapshots in
/__nsy/downloads.json. They share one update queue with the audio index (with_meta), so eviction always knows what's downloaded. - Songs are pinned: they're stored once in the same audio cache as played songs. Pinned songs are never evicted, aren't counted against the played-songs limit, and aren't cleared with played songs.
- Downloading: songs come one at a time (
queue_downloads), while the worker is kept alive. Covers go innsy-download-art-v1, which isn't trimmed. - Removing: removing a download, or a sync dropping songs from it, deletes the songs no other download keeps.
- Re-syncing:
syncDownloads()runs at start and on reconnect. It rebuilds an album or playlist older than 6 hours, and a mix once a day. A download the browser stopped part way through (workers get stopped) is handed back as it was. - Offline: the app opens on
/downloads, and a download's page and its Play button play only the songs on the device.
In the iOS app none of this runs at all: its web view has no navigator.serviceWorker (see The iOS app). In the Android app none of this holds for downloads: the app keeps them itself (see The Android app). downloads.js asks NessieDownloads there and the worker elsewhere, with the same answers.
connection.js decides when the app is offline. "Offline" means the device has no network or the last request to the server got no answer at all, because away from home a phone can be online while the server isn't reachable. api.js reports each request, and a request with no answer throws an ApiError with status 0. While the server is out of reach, /health is asked every 15 s, so coming back is noticed without the user doing anything. When the connection changes:
- the header's Offline badge shows or hides;
showError()says "You're offline" instead of "check the server";- a view that couldn't load is drawn again once the server answers;
- if the app opened offline,
catchUp()inmain.jsasks again for whatstart()asks first: who's listening, the libraries, Lidarr and AI. Until then, the sidebar says your libraries will show once Nessie can reach its server, rather than "No libraries yet"; - the player switches from the server to This device with
setOutput(…, { remember: false }), so the saved choice stays the server. Coming back online tells you how to switch back, rather than interrupting what's playing on the phone.
offline.js talks to the worker with postMessage over a MessageChannel, using messages like audio-cache-status and audio-cache-clear. That's how Settings → Offline shows and clears the cache.
The player also sends audio-cache-prefetch with the next few track ids. The worker answers at once, then downloads any not yet cached one at a time in the background. A newer request replaces the list, because the queue has moved on. Downloads go into the same cache under the same size limit, so a prefetched song plays from the cache with no network needed.