* refactor(artwork): let callers pin the blurhash component counts * feat(artwork): synthesize a unique placeholder blurhash from a seed * fix(artwork): base the synthetic-hash no-collision guarantee on the prefix, not length The prior comment and test claimed a synthetic value could never collide with a real one because it's always shorter. That's false: components() targets ~16 tiles by scaling one axis down as the other hits the 9 cap, so an extreme aspect ratio (e.g. 10x200) collapses to 1x9 = 9 components, which encodes to the same 22 characters as a 3x3 synthetic hash. The old test only exercised a square 64x64 gradient, so it never caught this. The real guarantee is structural, not length-based: the first character encodes shape as (xComp-1)+(yComp-1)*9, and components() derives xf*yf = 16 exactly before flooring/capping (xf = sqrt(16w/h), yf = xf*h/w = sqrt(16h/w), so xf*yf = sqrt(256) = 16). Two factors both in [2,3) can't multiply to 16, so Encode can never derive 3x3 - the synthetic prefix 'K' is structurally exclusive to Synthetic. Replaced the length-based test with one that sweeps extreme aspect ratios and asserts no real encode ever produces prefix 'K', alongside the assertion that Synthetic always does. * feat(artwork): tint a synthetic blurhash from a base colour Parses baseColor into HSL and clamps saturation/lightness so the tint stays muted; per-cell hashing (unchanged) is what keeps a shared tint across an album's tracks from colliding, as the new 1M-seed spec proves under a single fixed colour. * fix(artwork): trim over-long comment in synthetic blurhash test Review flagged the collision spec's comment for exceeding the 2-line budget; the Finamp rationale it restated already lives in the design doc and commit history. * feat(jellyfin): send a synthetic blurhash for unresolved artwork Clients render nothing where a placeholder belongs when ImageBlurHashes is omitted for pending artwork. primaryImage now synthesizes a value (seeded on the tag, so a cover swap re-keys it) whenever a tag is present but no blurhash has been computed yet. Known-absent artwork (ImageAbsent) is unaffected: it still emits neither tag nor blurhash, since GetOrPlaceholder would otherwise pin a shared placeholder under a distinct cache key for a year. * fix(jellyfin): avoid computing a synthetic blurhash when a real one exists cmp.Or evaluates both arguments before choosing between them, so blurhash.Synthetic ran (and was discarded) on every call even when img.BlurHash was already set. That's the common case in production: artwork_repository.go populates BlurHash from persisted values once a scan resolves it, so a healthy library paid the synthesis cost (xxh3 hashing, HSL conversion, image alloc, DCT encode) on every mapped item for a value it never used. Branch on emptiness first instead. * feat(jellyfin): tint a pending track's placeholder with its album's colour primaryImage never reaches the embeddedArtPending branch of SongToBaseItem, since there is no resolved image yet to feed it. Seed the synthetic blurhash on mf.ID (so the value stays unique and the client still issues the read-through request) but tint it with the album's DominantColor, which is already hydrated on MediaFile at no extra cost. * style(artwork): trim synthetic blurhash comments to the budget * docs(jellyfin): fix stale blurhash README bullet + two review nits The "Blurhashes are synthetic" bullet under Known limitations described dto/blurhash.go, which was deleted when the real core/artwork-computed blurhash + synthetic-fallback pipeline landed; every claim in it was false. Replaced it with an accurate paragraph in the Images section, since the described behaviour is now the finished design, not a gap. Also: fix a doc/body comment mismatch in blurhash.go (component counts are 2..9, not 1..9), and deduplicate the inline DC-extraction logic in synthetic_test.go by reusing the existing dcOf helper. * refactor(artwork): slice the synthetic cell jitter on byte boundaries The three perturbations came off one hash with mismatched masks and shifts (0xFF at 0, 0x3F at 8, 0x3F at 14), so a reader had to do the arithmetic to confirm the fields did not overlap. Only 20 of the 64 bits were in use either way, so the narrower fields bought nothing. Uniform byte slices at 0/8/16 are non-overlapping by inspection and give each field the full 8 bits. Both 1,000,000-seed collision specs still measure 1,000,000 distinct values. Also drops the local `n` alias, which was a second name for synthComponents inside a 15-line function. * docs(jellyfin): fix the blurhash paragraph's opening sentence It opened with "follow the same principle", pointing back at the preceding paragraph on admin-context artwork resolution — an unrelated subject, so the reader looks for a connection that is not there. * fix(artwork): render the synthetic grid larger than its component count The 3x3 cell grid was handed straight to the encoder as a 3x3 image, so the source had exactly as many samples as basis functions. Blurhash normalises its coefficients by 1/(w*h) and 2/(w*h), which assumes many samples per component, so the AC terms came out far too large. At the bottom-right corner the x and y bases are both [1, -0.5, -0.5], everything lines up negative, and the result clamped to black — a dark blob on every synthetic placeholder. Rendering the same nine colours bilinearly at 8x8 first removes it: measured over three seeds, the darkest corner goes from 13 to 74 and the darkest pixel from 1 to 63. 8px is the smallest size that clears the artefact; 12 and 16 are visually indistinguishable and cost 1.7x and 2.7x more. The interpolation is hand-rolled rather than x/image's scaler, which allocated 528 times per call against 14 for this. Both 1,000,000-seed collision specs still measure 1,000,000 distinct values. * refactor(artwork): tidy the synthetic upscale helpers cellWeight nudged its upper bound with a 1e-9 epsilon so int() could never land on the last cell. Clamping the coordinate and then the index says the same thing without a magic constant, and makes the clamp-don't-extrapolate intent explicit — edge pixels map outside the cell centres, so the fraction would otherwise run past 1. The grid type is spelled once as colorGrid rather than repeated in the local and the upscale signature, and encodeAt's doc now states the source-size contract that Synthetic depends on, so the next caller sees it at the function rather than only in synthetic.go. Output is unchanged: all seven sample hashes match byte for byte. * perf(artwork): make the synthetic upscale separable Both axes are square and constant-sized, so the per-pixel cell index and weight were the same 8 values recomputed 64 times per call. They are now built once by sync.OnceValue, matching the srgbToLinearTable pattern. The interpolation is also separable: stretching each of the 3 grid rows horizontally once and then blending rows vertically does 264 lerps where the per-pixel form did 576. upscale drops from 347ns to 260ns, Synthetic from 1780ns to 1669ns. Output is byte-identical across all seven sample hashes — same operations in the same order, only hoisted. * refactor(artwork): let a caller supply the cosine basis encodeAt built its cosine tables inline, so Synthetic rebuilt bit-identical ones on every call — its shape is always 3 components over 8 pixels. Splitting the table construction into cosBasis and the encoder proper into encodePixels lets Synthetic build the basis once via sync.OnceValue, and leaves encodeAt's signature and behaviour untouched. Synthetic drops from 14 allocations to 6 (1669ns to 1508ns). The time saving is small and this path only runs while artwork is still unresolved; the allocation cut is the point, alongside a shorter encodeAt. Every hash is unchanged: nine real Encode outputs spanning square, 10x200, 200x10, 1x50 and a real JPEG, plus all seven synthetic samples, all byte for byte identical before and after.
Navidrome Music Server 
Navidrome is an open source web-based music collection server and streamer. It gives you freedom to listen to your music collection from any browser or mobile device. It's like your personal Spotify!
Note: The master branch may be in an unstable or even broken state during development.
Please use releases instead of
the master branch in order to get a stable set of binaries.
Check out our Live Demo!
Any feedback is welcome! If you need/want a new feature, find a bug or think of any way to improve Navidrome, please file a GitHub issue or join the discussion in our Subreddit. If you want to contribute to the project in any other way (ui/backend dev, translations, themes), please join the chat in our Discord server.
Installation
See instructions on the project's website
Cloud Hosting
PikaPods has partnered with us to offer you an officially supported, cloud-hosted solution. A share of the revenue helps fund the development of Navidrome at no additional cost for you.
Features
- Handles very large music collections
- Streams virtually any audio format available
- Reads and uses all your beautifully curated metadata
- Great support for compilations (Various Artists albums) and box sets (multi-disc albums)
- Multi-user, each user has their own play counts, playlists, favourites, etc...
- Very low resource usage
- Multi-platform, runs on macOS, Linux and Windows. Docker images are also provided
- Ready to use binaries for all major platforms, including Raspberry Pi
- Automatically monitors your library for changes, importing new files and reloading new metadata
- Supports lyrics from sidecar .ttml, .yaml/.yml Lyricsfile, .elrc, .lrc, .srt, .txt files and embedded TTML, Enhanced LRC, LRC, SRT, and plain-text tags (via
lyricspriority) - Themeable, modern and responsive Web interface based on Material UI
- Compatible with all Subsonic/Madsonic/Airsonic clients
- Transcoding on the fly. Can be set per user/player. Opus encoding is supported
- Translated to various languages
Translations
Navidrome uses POEditor for translations, and we are always looking for more contributors
Documentation
All documentation can be found in the project's website: https://www.navidrome.org/docs. Here are some useful direct links:
Screenshots
