No description
  • Go 71.8%
  • Vue 20.2%
  • JavaScript 4.1%
  • CSS 1.8%
  • Makefile 1.3%
  • Other 0.8%
Find a file
Julien COSMAO 4cd4574128
feat: show which genres a rule actually touches
"does not contain electro" also drops Electronica, and nothing on the
page said so. The text typed into a contains rule is a pattern, not a
value, and the library is the only thing that can tell you what it
selects.

Add GET /api/match: it reads the suggestion cache already kept for
autocompletion and applies the same comparison Navidrome will, exact for
is/isNot and substring for contains/notContains, plus their list forms.
The list it returns is capped at 100, the count is not. The editor shows
the answer under the rule, and an empty one in red — that is a typo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3AZBjgLbEZTWKhSSifv2i
2026-09-12 12:22:47 +02:00
cmd/nspkit Revert "describe a playlist, get a rule tree" 2026-09-08 21:22:07 +02:00
internal feat: show which genres a rule actually touches 2026-09-12 12:22:47 +02:00
testdata feat: pick several genres in one rule 2026-09-12 12:03:14 +02:00
web feat: show which genres a rule actually touches 2026-09-12 12:22:47 +02:00
.dockerignore build: Makefile, image, and the upstream drift check 2026-09-08 08:37:59 +02:00
.gitignore nsp: the .nsp model and operator table 2026-09-08 08:25:20 +02:00
Dockerfile build: Makefile, image, and the upstream drift check 2026-09-08 08:37:59 +02:00
go.mod nsp: the .nsp model and operator table 2026-09-08 08:25:20 +02:00
Makefile Revert "describe a playlist, get a rule tree" 2026-09-08 21:22:07 +02:00
README.md feat: pick several genres in one rule 2026-09-12 12:03:14 +02:00

nspkit

Interactive builder for Navidrome smart playlists (.nsp), served at https://music.cosmao.info/smartplaylist.

One Go binary: the HTTP API plus the Vue SPA embedded with go:embed. It keeps no state — the .nsp files in the playlists directory are the state.

What the format actually supports

The documentation is wrong on two points, and silent on a third. All three were checked against model/criteria/json.go:

  • No not conjunction. Only all and any. Negation exists only as an operator's negative twin (isNot, notContains, notInTheLast, notInPlaylist, isMissing), which is why the UI puts negation in the operator dropdown instead of offering a NOT toggle. startsWith, endsWith, inTheRange, before and after have no twin: "does not start with" is not expressible.
  • No gte/lte. nspkit offers ≥ and ≤ and writes them as {"any":[{"gt":{f:v}},{"is":{f:v}}]}, folding that back on read. Exact for every type, unlike gt: v-1.
  • No list operators. A rule holds one value. nspkit offers is any of, is none of, contains any of and contains none of on the three fields with a completion source (genre, artist, album artist), and writes them as one simple operator per value under a conjunction: {"all":[{"notContains":{"genre":"Electro"}},{"notContains":{"genre":"Techno"}}]}. A group of that shape folds back into a single rule on read, so a hand-written OR of three genres opens as one chip list.

make check-upstream diffs the operator and field tables against Navidrome's source. Run it before bumping the Navidrome image.

Running it locally

make dev                      # Go server on :8080, playlists in /tmp/nspkit-playlists
cd web && npm run dev         # Vite, proxying /smartplaylist/api to :8080

--dev-user stands in for the Remote-User header Authelia sets in production.

Configuration

Flag Env Default
--addr ADDR :8080
--playlists PLAYLISTS_DIR /playlists
--navidrome NAVIDROME_URL http://navidrome.navidrome.svc.cluster.local:4533
--base-path BASE_PATH /smartplaylist
--user-header USER_HEADER Remote-User
--dev-user DEV_USER —

--base-path and Vite's base in web/vite.config.js must agree: there is no strip-prefix middleware in front, the server owns the prefix.

Authentication

nspkit never authenticates a user itself. Authelia gates the route and sets Remote-User; nspkit forwards that header to Navidrome, which trusts it because the pod sits in ND_EXTAUTH_TRUSTEDSOURCES. No credential is stored anywhere.

Triggering a scan requires a Navidrome admin. A non-admin gets a 403 naming the account.

Concurrent edits

GET /api/playlists/{file} returns a version — a hash of the file's contents. PUT sends it back and is refused with a 409 when it no longer matches, so a file edited by hand in between is reported rather than clobbered. Not the mtime: the kernel caches the wall clock at roughly jiffy granularity, so two writes a few milliseconds apart share a timestamp and the guard would miss the case it exists for.

Deployment

Chart sd/nspkit in the kube_manifests repo.