Gotchas
A curated selection of the traps that cost real time, each checked against a running system rather than taken from documentation. Most of them fail silently - a 200 that means nothing, a file of the right length that cannot be opened - which is exactly why they are written down.
Jellyfin
- 12.1.0 rejects
X-Emby-Tokenand?api_key=with 401. OnlyAuthorization: MediaBrowser ... Token="..."works, though most docs and older clients show the other two. This is whysource.Targetcarries headers, not just a URL. - Unknown query parameters are ignored with a 200.
IsVirtualItem=truereturned a real film, just as an invented parameter name did. Test a filter by asking for the opposite and checking the count changes. - Its direct-play answer is not trusted alone. It reports
SupportsDirectPlay: truefor an MKV even when the device profile offers only MP4, so the container is checked again. Believing it gives a video element that shows nothing, with no error anywhere. - The device profile errs conservative. Claiming a codec the browser cannot decode is the silent failure; claiming too little only costs a transcode.
- Transcodes are HLS because of seeking. A progressive transcode has no length until it finishes: a 20 s file showed 9.9 s and seeking snapped back. The HLS VOD playlist lists every segment up front.
- Its playlists are relative, so
/api/hls/{source}/{path...}mirrors Jellyfin's own/videos/namespace and the browser resolves every segment onto SoundStorm by itself. - Subtitle URLs are built, not read.
DeliveryUrlcomes back empty;/Videos/{item}/{source}/Subtitles/{index}/Stream.vttworks for every text track. Picture subtitles (PGS, VOBSUB) are not offered - attached, they render nothing. - Transcodes must be stopped: ffmpeg outlives the request, so
OnDonesendsDELETE /Videos/ActiveEncodings. - It will not remove items from an empty library folder - it cannot tell
"everything deleted" from "the drive did not mount". A generated
README.txtin each shelf keeps folders non-empty, so deleted films do leave search. - Each play needs its own
playSessionId, or every play of a film shares one conversion's files - old quality, possibly the old audio track. - Several query options make it write its admin token into the master playlist (HLS subtitles, subtitles in the manifest, trickplay), which would be piped to the device. Those keys are stripped and trickplay forced off.
Audiobookshelf
/loginreturns two tokens.accessTokenexpires in an hour; the legacytokennever does. SoundStorm stores the legacy one, or the backend would be stranded an hour after provisioning.isActivemust be sent when creating a user. Without it the user is created inactive, the response is an ordinary 200 with a token, and that token answers Unauthorized to everything.- Audio is addressed by inode, not item id, so the item must be fetched first.
progressis not derived fromcurrentTime. Send one without the other and the player resumes correctly while its own shelf shows the old percentage.- Sending
isFinished: falseis destructive: for a finished book it resets the position to zero. It is only ever sent as true. - It stores progress unvalidated - the string "x" as a time is accepted. So nothing leaves SoundStorm that is not a finite time, and progress is decoded leniently.
- Missing items keep being served from search and listing, with
isMissingset. The adapter skips them; there is no "not missing" filter to ask for.
Navidrome, Immich and the rest
ND_SCANINTERVAL, notND_SCANSCHEDULE. Unknown keys are ignored, so the wrong name looks fine while no periodic scan ever runs.- Navidrome's file paths were made up from tags unless
ND_SUBSONIC_DEFAULTREPORTREALPATHis on - and that setting is read once per client name, so SoundStorm introduced itself under a new name to get real paths. - Navidrome will not turn lossy into lossless: a lossy song asked for as FLAC comes back as Opus. The server's beat analysis uses a transcoding of its own name instead.
- Immich is pinned to its major version. v3 broke endpoints "that affect only third-party tools" - which is what SoundStorm is.
- Storyteller's app token reads only the
st_tokencookie; a bearer token is sent to the login page. - Every backend handles a deleted file differently: Navidrome hides it, Jellyfin removes it (unless the folder is empty), Audiobookshelf keeps serving it.
- Reconnecting is not provisioning. After a restart a backend may answer 503 while loading; only 401/403 means bad credentials. Treating any failure as "re-provision" once bricked a backend whose working token was still on disk.
PowerShell traps
- A line break before an operator inside
if (...)is a syntax error in 5.1. The file did not parse for two commits because the syntax check ignored the errorsPSParser::Tokenizereturns through a[ref]. - Inner double quotes are stripped on the way to native programs, so
--format '{{index .Config.Labels "..."}}'broke docker. Labels are read as JSON instead. Start-Process -PassThrureturns a process with no cached handle, andWaitForExit(timeout)then never sees the exit. Reading.Handleonce fixes it; otherwise every launch takes exactly the timeout and reports failure.- Native stderr becomes an ErrorRecord, so with
$ErrorActionPreference = 'Stop'docker's progress output kills the script. Every external call goes throughInvoke-NativeorInvoke-Docker- afterdocker infocrashed in exactly the case it existed to detect. - A scriptblock certificate callback breaks
Invoke-WebRequestfor every https address, with an error that never mentions the callback.HttpWebRequeston the pipeline thread works. --no-httpsbound to nothing: a parameter name cannot contain a hyphen, so it was accepted and ignored until an alias was added.- Here-strings carry CRLF into
docker exec bash -c. Set-Aclwrites the audit list too, which needs a privilege ordinary accounts lack.FileInfo.SetAccessControlwrites only what changed.install.ps1stays pure ASCII: without a byte order mark, box characters and em dashes become mojibake in Windows PowerShell 5.1.docker compose pullwrites no colon after a layer id, unlikedocker pull. A filter written for the latter filtered nothing.
Docker Desktop and Windows
- Every connection arrives from Docker's gateway address - localhost, the LAN, and anything a router forwards. "Only from the home network" cannot be checked, which is why the first sign-up needs a setup code, and why sign-in throttling cannot simply lock out by address.
- The TLS connection's local address is the container's, so an IP certificate
needs the LAN address configured (
SOUNDSTORM_TLS_HOSTS), written by the installer. os.MkdirAllcan return EEXIST on a bind mount for a directory that exists - only for folders mounted into other containers.- A rename cannot cross filesystems, so a shelf on another drive needs a copy to a hidden temporary name beside the destination, then a rename.
- New Wi-Fi networks are Public, and the firewall prompt for Docker's backend defaults to Private only, leaving a Block rule that beats any Allow.
- Docker Desktop opens its dashboard on every start unless
OpenUIOnStartupDisabledis written before its installer finishes - which launches it immediately. - Install WSL first (
wsl --install --no-distribution), and checkHypervisorPresentbefore the firmware virtualization flag, which reads false once a hypervisor is running. - Smart App Control blocks downloaded scripts by extension, and Defender blocks
piping a downloaded script into PowerShell on sight. The setup file downloads to a file and runs
it with
-File. - A raw GitHub URL ignores the query string when caching, so the installer downloads from the newest commit's URL instead.
- The compose project name is fixed, so a second install in another folder would adopt the first one's containers. Both installers refuse, naming the other folder.
go:embedrejects file names with an apostrophe.- Do not fix a
.cmdwithsed -i: it strips the CRLF that cmd.exe'sgotoneeds.
The Content-Security-Policy
- It is load-bearing, not hardening. EPUBs can contain scripts, and the reader
renders chapters in a same-origin blob iframe. Without
script-src 'self', opening a book would run a stranger's JavaScript with the session cookie.blob:stays allowed for styles and fonts, or books render unstyled. - It forbids inline script, and says so only in the console. The service-worker registration shipped inline; it was refused silently, and "Add to Home Screen" made a bookmark instead of an app. The fix is a file, never a looser policy.
'self'includes the server's own URLs, so a book naming an absolute/api/...or/static/app.jsaddress could run it. The reader strips scripts from every chapter, stream paths refuse script destinations and script content types, and app.js refuses to run in a frame.- No
crossoriginon the video element - everything is same-origin, and the attribute forces CORS checks that then fail.
Service worker rules
- Served from
/, not/static/: a worker's scope is its directory, and one under/static/would register, report success and intercept nothing. - The manifest must be served as JSON; Go's mime table has no
.webmanifest. - It never answers a range request, anything under
/api/, or a page load. A cached search is stale state; an unhonoured Range breaks seeking like a corrupt file; a cached page hides the browser's certificate warning behind a page that cannot work - the spinner that never stopped after a reinstall. - Network-first, so an update cannot pin anybody to an old build.
- A PWA needs a trusted certificate, not merely https. Chrome treats a self-signed origin as a certificate error and will not register a worker there - hidden by every test run on localhost.
- Nothing may assume
fetchresolves. A failed handshake left the boot spinner turning;api()now returns an offline result.
Mobile layout lessons
minmax(0, 1fr), not1fr. A grid item's minimum is its min-content, so onenowrappath pushed a 390 px phone's page to 531 px.auto-fill, minmax(168px, 1fr)is one column on a phone - every result a screen tall. Phones get two explicit columns.- iOS zooms on inputs under 16 px and does not zoom back.
- A safe-area rule must live in the rule that sets the padding; a later rule setting the whole padding silently undid it and put a close button under the camera.
- The Android web view reports 0 for the cutout it draws into, so the app sets
--safe-topand friends, and the CSS never usesenv()directly. - Translate, rotate, then transform: a spinning record slid by
transformwent off at its own angle. The swipe uses thetranslateproperty. - Chrome's tap highlight is a rectangle over a button's whole box, and cannot be screenshotted. It is switched off by name on cards.
:hoversticks after a tap, so hover styles apply only with(hover: hover).- Anything hanging off the side zooms the phone out and drops the tab bar;
overflow-x: clipon the body and app, clip so sticky headers still work. - Safari decides scroll-or-not within a few pixels, so sideways swipes need
touch-action: pan-y pinch-zoom. - A MutationObserver that writes to what it watches must change a class only when it would change, or removing an absent class rewrites the attribute and it loops for ever - which froze the TV.
- Reduce Motion is common on iPhones. Looks somebody chose to animate ignore it; otherwise they stood still on iPhone and moved on Android.
- Routing music through Web Audio stops it when an iPhone locks, so leveling
uses
audio.volumeand analysis decodes a copy offline.
Files and state
- Something read committed binaries as text and dropped every carriage return. Ebooks would not open and MP3s clicked - all quietly. Committed binaries now have checks that read them as their consumer does.
state.Opensaves every time, taking ownership of the file for whoever opened it. Tools stat first and hand it back.- An empty list must never read back as nil where nil means "everything" - the account libraries list and the hidden-pills list both.
- An unknown command-line argument is an error; it once started a second server against the same state volume.
- The rename invalidated every provisioned volume, since backend account names and the state path changed. Remember it if the project is ever renamed again.