@smcboyardee/cratelens
v1.2.0
Published
Self-hosted, read-only music library explorer with a harmonic mix builder, BPM/key analysis, and library health tools.
Maintainers
Readme
CrateLens
A zero-build, local-first DJ curation hub for the music library you already have.
Point CrateLens at a folder of music, and it gives you back a fast, browsable library with harmonic mix-building, BPM/key analysis, and library health tools, plus a path to fill in what's missing. One Node.js process. No database, no build step, and no changes to your files.
Status: v1.2.0. Released under the MIT License. The source repository is currently private; see Project status and roadmap.
Features & Highlights
- Set Builder — one click reorders a crate into continuous harmonic chains along the Camelot wheel, bounded BPM jumps and all, reseeding a new run when a chain dead-ends so the whole crate becomes a BPM-ascending arc.
- Smart Crates with rule logic — a crate can carry a rule tree (AND/OR/NOT over BPM, year, genre, Camelot keys, lossless, play history, and recency) instead of a fixed track list; membership recomputes live, and the editor shows a live "Matches N tracks" count as you build rules.
- Harmonic "Suggested Next" — the Queue shows the best harmonic matches for the playing track, using the Camelot wheel and a ±5 BPM window.
- Per-crate Health Badge — every crate shows how mix-ready it is, expanding to the exact tracks missing BPM/key or stuck in analysis.
- Update checker — Settings shows the running version and flags when a newer GitHub release is available.
- Curation Hub — Discover unifies artist recommendations, discography gaps, Deezer trending charts, and MusicBrainz upcoming releases in one place, alongside a Wishlist/radar and a Hidden Artists list.
- Actionable Health dashboard — cooldown, missing-tag, missing-art, and duplicate-album-folder issues as expandable, browsable lists with file paths and per-copy format detail, plus a one-click Force Retry per track.
- Duplicate album detection — surfaces albums that exist more than once under different folders (a different copy or format of the same release) so you can compare and clean up by hand.
- Harmonic mixing via Camelot keys — every track's key normalises onto the Camelot wheel; the Crates Composer builds a mixable set along compatible keys and an energy curve.
- Soulseek integration, end to end — search and download through slskd, with an optional CrateSmith hand-off when a download finishes.
- M3U8 crate export — save a crate, then export it as a
.m3u8playlist for Serato, Rekordbox, or Traktor, with host-path mapping for Docker setups.
What it's for
A large local music collection is hard to use. It is tough to see what you own at a glance, tedious to find tracks that mix well together, and easy to lose track of which albums you are missing. CrateLens reads your existing files (and never changes them), then gives you a fast way to explore the collection, analyse it, and build DJ-ready sets.
It is aimed at:
- DJs and selectors who want harmonically compatible sets (Camelot or Open Key notation).
- Collectors who want to browse a library, check its health, and fill in gaps.
- Anyone running a home server or NAS who wants a nicer window into their music.
How it relates to CrateSmith
CrateLens is the companion to CrateSmith. CrateSmith organises your music into a clean, tagged library. CrateLens lets you look inside that library, build harmonic sets, find what is missing, and pull in new music, then hands finished downloads back to CrateSmith to file away. The two are independent, so you can run CrateLens on its own against any library you already have.
At a glance
- One Node.js process, no build step. The runtime dependencies are
music-metadata(tags),sharp(thumbnails), andessentia.js(BPM/key analysis). - Read-only with respect to your library. It scans, it never writes. All derived data (thumbnails, fetched art, analysis, cache) lives in
data/. - Every track's key is normalised to the Camelot wheel. It accepts Camelot (
8A), Open Key (1d/12m), and standard names (F#m,Db major), so the mix builder always has consistent data to work with.
Screenshots
All shots are dark mode, the default theme.
Wall

The library at a glance: an album-art grid with search and genre, decade, format, and key filters. Click an album for its track list with colour-coded Camelot keys.
Discover

New artists recommended from the ones you already own, plus the albums you are missing for artists you collect.
Crates, Composer

Set a vibe and a flow, or anchor on a track, then build.

The result is one harmonic journey you can play, save as a crate, or export.
Saved crates

Crates you have saved, ready to reopen or play.
Downloads

Soulseek transfers through slskd, with the CrateSmith hand-off controls and a wishlist.
Stats

The collection in numbers: the Camelot key wheel, BPM spread, top genres and artists, and headline totals.
Settings

Themes, optional services, the library root, and automatic maintenance.
Feature overview
| Area | What it does |
| --- | --- |
| Wall | Album-art grid with search, genre/decade/format/key filters, sorting, a Surprise me shortcut, and on-hover track and BPM info. Click an album for its track list with colour-coded Camelot key chips. |
| Discover | Recommends new artists ranked from the ones you already own, each with a "because you have X, Y" reason and some albums to try. Also surfaces Deezer trending charts and MusicBrainz upcoming releases, finds discography gaps (the studio albums you are missing for artists you collect), and lets you maintain a Wishlist/radar and a Hidden Artists list. |
| Downloads | The Soulseek/slskd transfer view and the CrateSmith hand-off controls. Watch active, queued, completed, and failed transfers, manage a wishlist, and choose what happens when a download finishes. |
| Crates, Composer | The harmonic set builder. Anchor on a track or start from a vibe, and CrateLens walks the library through compatible Camelot keys along an energy curve into one mixable set. Save it as a crate, export it as .m3u8, or find more like it to download. |
| Crates, Saved | Your saved crates and working mix drafts. |
| Stats | Camelot key wheel (click to filter the Wall), BPM distribution, albums by decade, top genres and artists, file formats, and headline totals. |
| Health | A collection-health score with practical, expandable issue lists: tracks in the analysis-failure cooldown (with one-click Force Retry), missing tags, missing art, low-bitrate files, and duplicate album folders with per-copy format detail. |
| Settings | Themes, accent colour, content width and cover size, library root, automatic maintenance, and the slskd and CrateSmith connection settings. |
| Player | A streaming player bar (queue, seek, volume, shuffle). Play an album, play a mix, or start a radio from a genre and BPM range. FLAC, MP3, and other formats stream with seeking. |
How the views fit together
┌───────────────────────┐
│ Discover │ find new artists and the albums you're missing
└───────────┬───────────┘
│ pick one, then click the download button
▼
┌───────────────────────┐
│ Downloads │ search and download it over Soulseek (via slskd)
└───────────┬───────────┘
│ finished files land in your downloads folder
▼
┌───────────────────────┐
│ CrateSmith │ organises the files into your library (separate app)
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Your music library │ clean, tagged, on disk
└───────────┬───────────┘
│ CrateLens scans it, read-only
▼
┌───────────────────────┐
│ Explore & mix │ Wall, Crates, Stats, Health
└───────────────────────┘Everything except CrateSmith is part of CrateLens. The integration point between CrateLens, slskd, and CrateSmith is the filesystem: slskd downloads into the folder CrateSmith watches, CrateSmith files it into your library, and CrateLens reads that library read-only. There is no tight coupling beyond that and one optional "nudge" API call.
Requirements
- Docker and Docker Compose (recommended), or Node.js 20 or newer to run it via npm or a manual clone.
- A music library on disk (any folder tree of audio files).
ffmpegfor BPM/key analysis. It is bundled in the Docker image. For an npm or manual run, install it yourself or setFFMPEG_PATHto its location.- Optional: a running slskd instance for Soulseek downloads.
- Optional: a running CrateSmith instance for automatic organising.
Installation
Three ways to run CrateLens, depending on how much you want managed for you.
Option 1: Docker (recommended)
The pre-built image is the fastest way to get running, and bundles ffmpeg so BPM/key analysis works out of the box.
curl -O https://raw.githubusercontent.com/SmcBoyardee/CrateLens/master/docker-compose.yml
curl -O https://raw.githubusercontent.com/SmcBoyardee/CrateLens/master/.env.example
cp .env.example .env # then edit MUSIC_LIBRARY_ROOT
docker compose up -dOpen http://localhost:28419.
Host port 28419 maps to container port 28418 (28418 is left free for a native run or CrateSmith). Your library is mounted read-only at /music, and persistent data lives in ./data.
By default docker-compose.yml builds the image from a cloned copy of this repo. To pull the published image instead of building, swap the build: block for image::
services:
cratelens:
image: ghcr.io/your-github-username/cratelens:latest
# build: { context: ., args: { APP_VERSION: "1.2.0" } } # remove or comment outReplace your-github-username with the actual GitHub owner of the repository you're pulling from, and latest with a specific version tag (for example v1.2.0) if you want to pin it.
Some useful commands:
docker compose logs -f cratelens # follow logs
docker compose restart cratelens # restart after an .env change
docker compose down # stop and remove containers
docker compose up --build -d # rebuild after a code change
docker compose config # validate compose + .env resolutiondocker compose config (without --quiet) prints the fully resolved configuration, including any secrets from your .env (such as CRATESMITH_TOKEN or SLSKD_API_KEY) and your real library path. Only run it in your own terminal, and do not paste its output elsewhere.
Option 2: npm
# run once without installing anything
LIBRARY_ROOT=/path/to/your/music npx cratelens
# or install it globally
npm install -g cratelens
LIBRARY_ROOT=/path/to/your/music cratelensOpen http://localhost:28418 (the default port; set PORT to change it). This path does not bundle ffmpeg: install it yourself and make sure it's on PATH, or set FFMPEG_PATH to its location. All the environment variables in Configuration below apply directly, using LIBRARY_ROOT and PORT in place of the Docker-only MUSIC_LIBRARY_ROOT.
Option 3: Manual (clone and run)
git clone https://github.com/SmcBoyardee/CrateLens.git
cd CrateLens
npm install
npm run fixtures # optional: generate a synthetic test library
npm start # serves http://localhost:28418Point it at a real library through the Settings menu, the LIBRARY_ROOT environment variable, or data/config.json.
Configuration (environment variables)
Copy .env.example to .env and edit it. .env is gitignored, so never commit it. Everything is optional except MUSIC_LIBRARY_ROOT (Docker) or LIBRARY_ROOT (npm/manual).
| Variable | Purpose |
| --- | --- |
| MUSIC_LIBRARY_ROOT | Required for Docker. Host path to your music library, mounted read-only at /music. |
| LIBRARY_ROOT | Required for npm/manual runs in place of MUSIC_LIBRARY_ROOT. Path to your music library directly on the host. |
| PORT | npm/manual runs only. Port to listen on. Defaults to 28418. |
| HOST_LIBRARY_ROOT | Optional. Same root as MUSIC_LIBRARY_ROOT, in host notation (for example D:\Music\Albums). Used to rewrite paths in exported .m3u8 playlists so they open on the host, not inside the container. |
| TZ | Local timezone for the nightly maintenance window (for example Europe/Malta). Defaults to UTC. |
| MAINTENANCE_ENABLED | true or false. Initial default for automatic maintenance. The detailed settings live in Settings. |
| RESCAN_INTERVAL_MIN | Plain incremental rescan every N minutes (0 is off). Separate from nightly maintenance. |
| MUSIC_DOWNLOADS | Host path slskd downloads into. Point it at the same folder CrateSmith watches. |
| SLSKD_URL | slskd base URL, for example http://slskd:5030. Can also be set in Settings. |
| SLSKD_API_KEY | slskd API key. Can also be set in Settings. |
| CRATESMITH_URL | CrateSmith base URL, for example http://host.docker.internal:28417. Must be http or https. |
| CRATESMITH_TOKEN | CrateSmith's own access token, used for the hand-off. |
| CRATESMITH_MODE | off, dry, or safe. What to do when a download finishes. |
| AUTH_TOKEN | Optional. When set, every API request must present this token. Unset means open, which is the trusted-LAN default. See Security and privacy. |
| FFMPEG_PATH | Optional path to an ffmpeg binary (npm/manual runs only; the Docker image bundles ffmpeg). |
Integration URLs (SLSKD_URL, CRATESMITH_URL) are validated to be http or https only. Schemes like file:, ftp:, and gopher: are rejected, so the server cannot be pointed at something dangerous. Local and LAN hosts (localhost, 127.0.0.1, private IPs, Docker service names) are allowed, which is the normal case for self-hosting.
Library path and volume mounting
In Docker, the bind mount in docker-compose.yml is the source of truth:
volumes:
- ./data:/app/data # writable derived data
- type: bind
source: ${MUSIC_LIBRARY_ROOT} # your library, from .env
target: /music
read_only: true # CrateLens never writes hereInside the container the library root is always /music (LIBRARY_ROOT=/music). The host path comes from MUSIC_LIBRARY_ROOT. If you change the host path, expect a fresh scan, because the cache is keyed to the active root.
Importing and scanning your library
- Start the app and open it in a browser.
- If the library root is not set yet, point CrateLens at it (Settings, or
LIBRARY_ROOT/MUSIC_LIBRARY_ROOT). - Click Rescan in the header, or let automatic maintenance pick it up overnight.
Scans are incremental: files whose size and modified time are unchanged are not re-parsed, so repeat scans are fast. The result is cached to data/library.json. To enrich the collection you can use the Health view buttons, or let automatic maintenance do it for you:
- Fetch missing art: MusicBrainz then Cover Art Archive, falling back to the iTunes Search API. Paced to stay within MusicBrainz etiquette.
- Analyse BPM and keys: ffmpeg decodes a window of each track, then essentia.js (WASM) estimates tempo and key and maps it onto the Camelot wheel. Progress is checkpointed to
data/analysis.json, so it can be interrupted and resumed.
Both are non-destructive. Results live in data/, and the library itself is untouched.
Automatic maintenance
CrateLens can keep the library fresh on its own, so you do not have to remember to press Rescan, Fetch art, or Analyse. By default it runs once a day during the night and does three things in order:
- Rescan the library for added, changed, or removed files (incremental, so it is cheap when nothing changed).
- Fetch art for albums that are still missing it.
- Analyse tracks that are still missing BPM or key.
Each task is skipped when there is nothing to do, so a quiet night is light on the disk and the network. A run never starts a second time while one is already going, and it will not start while a manual Rescan, Fetch art, or Analyse job is running. If the library root is missing or offline, the run is skipped and the reason is logged. Every step is logged clearly, for example scan started, art: nothing missing, skipped, analysis complete, or run failed.
Settings
Open Settings and find Automatic maintenance. You can:
- Turn automatic maintenance on or off.
- Set how often it runs (every N hours, default 24).
- Set the nightly window it is allowed to run in (default 01:00 to 03:00).
- Choose which tasks are included: rescan, fetch missing art, analyse missing BPM/key.
- Run it once immediately with Run now.
The card also shows the last run, what it did, and when the next run is due. Settings are saved to data/config.json using the same crash-safe atomic writes as the rest of the app.
Defaults and timezone
Automatic maintenance is enabled by default, runs every 24 hours within the 01:00 to 03:00 window, and includes all three tasks. You can change any of that in Settings, or disable it entirely with MAINTENANCE_ENABLED=false.
The schedule uses the server (container) local time. In Docker that is UTC unless you set TZ, so to make the night window match your actual night, set TZ to your zone (for example TZ=Europe/Malta) in .env.
A maintenance run can do real work the first time it sees a large or un-enriched library: a full art fetch and a full analysis pass both take a while. If your library is very large, consider widening the window, lowering the frequency, or turning off the analysis task.
Some files cannot be analysed (for example corrupt or unusual encodings). When a track fails analysis, CrateLens records the attempt and skips that track on automatic runs for about a week, so a handful of bad files are not retried every night. The track is tried again once the cooldown passes, or sooner if the file changes on disk, or immediately via Force Retry on the Health view. The Settings card shows how many tracks are currently skipped this way, and the manual Analyse button always retries everything.
Soulseek / slskd integration (optional)
CrateLens can hand recommendations to a slskd instance (a Soulseek client with a REST API) to download them, completing the loop: Discover, download, CrateSmith organises, and it shows up here.
# in .env, set MUSIC_DOWNLOADS to the folder CrateSmith watches, then:
docker compose --profile soulseek up -d- Open slskd at
http://localhost:5030, set your Soulseek login, and create an API key. - In CrateLens Settings under slskd, enter
http://slskd:5030and that API key. - On a Discover card (or a missing album), click the download button. CrateLens searches Soulseek, shows album-folder candidates (format, bitrate, availability), and you pick one. It lands in
MUSIC_DOWNLOADS, where CrateSmith takes over.
Only download content you have the right to.
CrateSmith hand-off (optional)
When a Soulseek download finishes, CrateLens can ask a running CrateSmith instance to organise the new album:
- In CrateLens Settings under CrateSmith, set the URL (
http://host.docker.internal:28417from the container, orhttp://localhost:28417for a native run) and paste CrateSmith's own access token. You can also setCRATESMITH_URLandCRATESMITH_TOKENin.env. - Choose the mode:
- Off: do nothing.
- Plan (
dry): trigger a CrateSmith dry-run so it is ready to review. - Organise (
safe): run the dry-run and execute the actions CrateSmith classes as safe. Anything it marks for review or blocks is always left for you.
CrateLens polls slskd's transfers, and when the queue drains after a success it fires the hand-off (debounced, one run at a time). CrateLens does not move anything itself, because slskd already lands files where CrateSmith looks.
Data, cache, and storage
Everything CrateLens generates lives under data/, which is gitignored:
| Path | Contents |
| --- | --- |
| data/library.json | Scan cache ({ root, scannedAt, tracks }), keyed to the active library root. |
| data/analysis.json | BPM/key analysis checkpoints. |
| data/config.json | Saved settings (library root, maintenance, slskd and CrateSmith connection, mode). |
| data/maintenance.json | Last automatic-maintenance run time and result. |
| data/thumbs/ | Generated album thumbnails. |
| data/fetched-art/ | Cover art fetched during enrichment. |
JSON state files are written atomically (write to a temp file, then rename), so a crash or power loss mid-write cannot truncate them. The most important stores (library.json, config.json, and the crates/wishlist/mixes/history files) also keep a one-slot .bak. If a JSON file is found corrupted on startup, CrateLens logs a clear warning and falls back safely rather than crashing or overwriting the file.
Note: the library cache only loads when its stored
rootmatches the active root. Changing the library root (in Settings or viaLIBRARY_ROOT) automatically starts a fresh scan that repopulates the cache. Do not hand-edit therootvalue indata/library.json, or a running instance may treat the cache as stale and look empty until the next scan finishes.
Security and privacy
- Never commit secrets or personal data.
.env,data/,slskd/, andnode_modules/are gitignored. Real credentials (Soulseek login, slskd API key, CrateSmith token) belong only in.env. .env.exampleis a sanitised template that uses placeholder paths, not real ones. Keep it that way.- Your library paths are private. Do not bake real host paths into committed files. Use
.env, the Settings UI, ordata/config.json, all of which are untracked. - The library is mounted read-only, so CrateLens cannot modify or delete your music. File access is contained to the library root: the media and art routes reject path traversal (
..) and symlink escapes, and the scanner will not follow symlinks that point outside the root or loop on symlink cycles. - Integration URLs are validated to http or https only, which reduces SSRF risk from a misconfigured or hostile slskd or CrateSmith URL.
- Responses carry baseline security headers (
X-Content-Type-Options,Referrer-Policy, and a Content-Security-Policy with a strictscript-src 'self'). API errors return generic messages, and full detail is logged on the server only. - Force Retry (the per-track analysis bypass on the Health view) is capped at a small number of concurrent runs server-side, so it cannot be used to spawn unbounded analysis processes.
- The container runs as a non-root user with
no-new-privileges.
Network exposure and authentication
By default CrateLens has no authentication, because it is meant for a trusted LAN or home server. For anything less trusted, pick one or both of these:
- Bind to localhost only and reach it through a reverse proxy or an SSH tunnel. In
docker-compose.yml, change the port mapping to"127.0.0.1:28419:28418". - Require a token (see below).
Either way, do not expose CrateLens directly to the public internet without a proxy you control.
Turning on token auth
Generate a long random token. Any of these work:
openssl rand -hex 32 # or node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Put it in
.envnext todocker-compose.yml(do not commit.env):AUTH_TOKEN=paste-your-generated-token-hereRestart so the container picks it up:
docker compose up -d
Now every API request must send the token as Authorization: Bearer <token> or X-CrateLens-Token: <token>. In the browser, the app notices the 401 on first load, prompts you for the token once, and stores it in that browser's local storage so you are not asked again. You can change or clear it later in Settings under Access token. If you run behind a reverse proxy, you can have the proxy inject the header instead of using the in-app prompt.
To turn it off again, remove the AUTH_TOKEN line from .env (or set it empty) and run docker compose up -d. Clearing the token in Settings on each browser is optional, since the server stops requiring it.
Troubleshooting
| Symptom | Likely cause and fix |
| --- | --- |
| "Point me at your crates" or empty library | Library root not set, or the cache root does not match. Set MUSIC_LIBRARY_ROOT / LIBRARY_ROOT, then Rescan. Do not edit data/library.json by hand. |
| Compose will not start, MUSIC_LIBRARY_ROOT error | You did not copy .env.example to .env or did not set the path. |
| cratelens: command not found after npm install -g | npm's global bin directory isn't on PATH. Run npm config get prefix and add that prefix's bin folder to PATH, or use npx cratelens instead. |
| BPM/key analysis does nothing | ffmpeg is missing. It is bundled in Docker. For npm/manual runs, install it or set FFMPEG_PATH. |
| Maintenance runs at the wrong time | The schedule uses the container local time. Set TZ to your zone in .env. |
| Downloads tab says "Soulseek not connected" | slskd URL or key not set, or slskd is not running. Start the soulseek profile and configure it in Settings. |
| CrateSmith shows "Not connected" | Wrong URL or token, or CrateSmith is not running. From the container use http://host.docker.internal:28417. |
| "Unsupported URL scheme" when saving slskd or CrateSmith | Only http:// and https:// URLs are accepted. |
| API returns 401, or the app keeps asking for a token | AUTH_TOKEN is set on the server. Enter the matching token when prompted, or in Settings under Access token. |
| Cannot reach the app | Use 28419 for Docker (not 28418), and 28418 for npm/manual runs. |
Development notes
- The front end is plain vanilla: a single
public/app.js,public/app.css, andpublic/index.html, with no framework and no bundler. Theming uses CSS custom properties (data-themeon<html>), and theme bootstrap is an externalpublic/theme-init.jsso the Content-Security-Policy can keepscript-src 'self'. - The back end is a single
server.js(static UI and JSON API) plus helpers inlib/:scanner,analyze,camelot,discover,crates,enrich,soulseek,cratesmith, andsafe(atomic writes, URL validation, path containment). - Tests:
npm test(Node's built-in runner, specs intest/). They cover key parsing, path containment, symlink and traversal safety, URL validation, atomic writes, M3U export formatting, and API smoke tests, including a real-server boot. - Audit dependencies:
npm run audit. - Validate the container config:
npm run compose:check(docker compose config). This prints the resolved configuration including any.envsecrets, so use it locally only, not in CI or shared logs. - Generate synthetic fixtures:
npm run fixtures. - CI (GitHub Actions) runs the syntax check, tests, an audit, and the compose validation on push and pull request. A separate workflow builds and publishes the Docker image to GHCR automatically on tagged releases.
Docker and service naming
Docker-facing identifiers are lowercase cratelens (image, service, container, compose project name). The human-facing name is CrateLens.
Project status and roadmap
v1.1.0 is the current release: Smart Crates with live-evaluated rules, harmonic "Suggested Next" track suggestions in the Queue, a per-crate Health Badge, and a built-in GitHub update checker, on top of the v1.0.0 foundation (a unified Discover curation hub, an actionable Health dashboard with duplicate detection, Soulseek integration end to end, M3U8 crate export, and an automated Docker release pipeline).
On the list:
- More detailed per-transfer progress in the Downloads view.
- Refreshed screenshots and a short getting-started walkthrough.
The source repository is currently private. Bug reports and suggestions are welcome once it is made public.
Contributing
Contributions are not being accepted while the repository is private. Once it is public, issues and pull requests will be welcome. For now, feel free to fork and experiment under the terms of the licence below.
License
CrateLens is released under the MIT License. You are free to use, modify, and distribute it, with attribution and no warranty.
Development quickstart
For contributors working on this repo, the standard checks are:
npm test
npm audit
node --check server.js
node --check public/app.js
docker compose config --quietLocal app smoke checks:
curl http://127.0.0.1:28419/api/state
curl http://127.0.0.1:28419/api/libraryDo not commit .env, data/, media files, release zips, cache files, or screenshots showing private paths.
Use feature branches for meaningful changes.
