Commit Graph
100 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 4.7 9b0433ad9b docs: add web UI server auth foundation plan
10-task plan for the first implementation slice of the web UI
scaffold: sessions table, /api/auth/{login,logout}, /api/me, and
the RequireUser middleware (cookie + bearer). Stops short of the
library read endpoints and the SvelteKit project, which become
their own follow-up plans.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 19:30:24 -04:00
bvandeusenandClaude Opus 4.7 cb3df51f08 docs: add web UI scaffold design spec
First pass of the web UI design doc from the M1 close-out brainstorm.
Covers the stack (SvelteKit static + Go embed), auth model
(cookie + bearer from one endpoint), /api/* surface, shell layout
(sidebar + bottom player), first-cut feature scope, and how later
web UI work threads into M2–M5 alongside each backend milestone
instead of landing as a post-hoc M6.

Also ignores the local .superpowers/ brainstorming companion cache.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 19:18:04 -04:00
bvandeusenandClaude Opus 4.7 4a9193bc52 feat(library): extract track duration via ffprobe
Scrubbing/seeking in clients was a no-op because every track shipped
with duration_ms=0. Shell out to ffprobe (already in the image) per
file during scan and record the parsed duration. ffprobe failures are
warned + recorded as 0 so a single bad file doesn't sink the scan.

Tightened the incremental skip to also require duration_ms > 0 so
existing libraries get backfilled on the next rescan instead of
needing a wipe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 08:02:37 -04:00
bvandeusenandClaude Opus 4.7 dcd19c0143 fix(library): drop out-of-range years instead of failing the album
Tag-supplied years were being passed straight to Postgres' date column
without validation. Files with corrupt or 5+ digit years (seen in the
wild on a couple of dozen albums) tripped SQLSTATE 22008 and the entire
album upsert failed, dropping every track on those albums from the
library.

Validate the year is within 1..9999 before constructing the date. If
it's outside that window, log a warning naming the album and year, and
insert the album with no release_date — a soft field shouldn't take
down the whole row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:14:51 -04:00
bvandeusenandClaude Opus 4.7 dfcdf4d3ca feat(subsonic): getUser + envelope-shaped 404 for /rest/*
Feishin's first-login flow hits getUser to discover the authenticated
identity's roles. We never registered the route, so chi returned a
plain-text 404 — Feishin's parser treats anything non-Subsonic as a
generic auth failure ("Failed to log in"), masking the real cause.

- Implement /rest/getUser with the full role bag. Admins get every
  role; non-admins get the play-music subset. Single-user M1 means
  cross-user lookups by admins return the caller's roles for now;
  revisit when user management lands.
- Set sub.NotFound on /rest/* to emit a Subsonic envelope (code 0,
  "Method not implemented") instead of plain text. Any future client
  probing an unimplemented endpoint now sees a parseable failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 21:56:31 -04:00
bvandeusenandClaude Opus 4.7 64582b21e3 fix(auth): set subsonic_password on admin bootstrap
Bootstrap was creating password_hash + api_token but leaving
subsonic_password nil, which meant Subsonic clients (Feishin, Symfonium)
got ErrTokenNotSupported on t+s auth — the server had no plaintext to
hash against. Mirror the bootstrap password into subsonic_password so
the admin can sign in to Subsonic clients with the same credential
printed on first boot. Plaintext at rest is the cost of Subsonic's
legacy auth; matches Navidrome's posture.

Also folds in the local dev compose tweaks: dedicated bridge network
with postgres unpublished from the host, and a bind-mount aimed at the
operator's real library path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 18:30:54 -04:00
bvandeusenandClaude Opus 4.7 e6b84190e7 chore(compose): bake in dev mount + scan defaults
Dev stack can now `docker compose up --build` with no extra env: mounts
./music read-only, enables startup scan, and bootstraps admin with a
generated password printed to stderr on first boot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 18:12:13 -04:00
bvandeusen 3c95740ebe feat(subsonic): emit coverArt id unconditionally (#296)
getCoverArt now falls back to sidecar images next to the first track in
an album, so browse responses can advertise a coverArt id whether or not
albums.cover_art_path is set. Worst case the client gets a Subsonic 70
when no sidecar exists.
2026-04-19 19:28:30 +00:00
bvandeusen 547adb74ac feat(subsonic): register media handlers (#296)
Wire /stream, /download, /getCoverArt, /scrobble onto /rest via mediaHandlers.
2026-04-19 19:27:15 +00:00
bvandeusen eb776bdaa2 test(subsonic): cover stream helpers (#296)
findSidecarCover priority (cover.* wins over folder.*), imageContentType
mapping, and nowPlayingMap write/read round-trip.
2026-04-19 19:26:59 +00:00
bvandeusen 0760b48037 feat(subsonic): add stream/download/getCoverArt/scrobble (#296)
http.ServeContent handles Range/ETag/If-Modified-Since; download forces
attachment disposition. getCoverArt tries albums.cover_art_path, then
falls back to cover.{jpg,jpeg,png}/folder.{jpg,jpeg,png} next to the first
track's file. scrobble submission=false records a track into an in-memory
nowPlaying map keyed by user — M2 will read this and wire real events.
2026-04-19 19:26:39 +00:00
bvandeusen 9997781ab8 test(subsonic): cover browse type helpers (#295)
UUID round-trip, index letter bucketing, content-type mapping, date/ts
conversions, and small number utilities used by browse handlers.
2026-04-19 19:08:38 +00:00
bvandeusen d982bcb2ff test(subsonic): pin getMusicFolders wire shape (#295) 2026-04-19 19:08:18 +00:00
bvandeusen ca92cec159 feat(subsonic): register browse endpoints (#295)
Wire getMusicFolders, getIndexes, getArtists, getArtist, getAlbum,
getAlbumList2, getSong, and search3 onto the /rest router via browseHandlers.
2026-04-19 19:08:08 +00:00
bvandeusen 252428a104 feat(subsonic): add browse handlers (#295)
Implement getMusicFolders, getIndexes, getArtists, getArtist, getAlbum,
getSong, getAlbumList2, and search3. Album list types supported: newest,
alphabeticalByName, alphabeticalByArtist, random, byGenre, recent, frequent
(recent/frequent stub to [] pending M2 play history).
2026-04-19 19:07:50 +00:00
bvandeusen 71e8849dca subsonic: browse response shapes and conversion helpers (#295)
Adds the Subsonic browse wire types (MusicFolder, Index, ArtistRef,
ArtistDetail, AlbumRef, AlbumDetail, SongRef, and their response
envelopes) with dual json+xml tags so a single struct serves both
formats. Includes UUID wire helpers (uuidToID/parseUUID), indexing
helpers (indexLetter), MIME mapping (contentTypeForFormat), and the
artist-name resolver used by album listings.

IDs are bare UUID strings on the wire — endpoint context disambiguates.
2026-04-19 19:05:03 +00:00
bvandeusen f8249a12ac sqlc generate for CountTracksByAlbum and SearchTracks (#295) 2026-04-19 19:03:59 +00:00
bvandeusen f3a80347c1 sqlc generate for SearchArtists (#295) 2026-04-19 19:03:30 +00:00
bvandeusen 319ef06a4f sqlc generate for album browse queries (#295) 2026-04-19 19:03:10 +00:00
bvandeusen acd2c7f034 sqlc: add CountTracksByAlbum and SearchTracks for Subsonic browse (#295)
CountTracksByAlbum drives the songCount attr on AlbumRef; SearchTracks
backs the song facet of search3.
2026-04-19 19:02:27 +00:00
bvandeusen 97eb91f50b sqlc: add SearchArtists for Subsonic search3 (#295) 2026-04-19 19:02:14 +00:00
bvandeusen a498d20021 sqlc: add album listing/search queries for Subsonic browse (#295)
Adds ListAlbumsAlphaByName, ListAlbumsAlphaByArtist (with sqlc.embed
for the joined artist sort_name), ListAlbumsNewest, ListAlbumsRandom,
ListAlbumsByGenre, and SearchAlbums. Powers getAlbumList2 type filters
and the album facet of search3.
2026-04-19 19:02:03 +00:00
bvandeusen 98818fe28c fix(subsonic): check fmt.Fprintf return (errcheck lint) 2026-04-19 18:07:36 +00:00
bvandeusen 02e867ce94 docs: refresh config.example.yaml with auth/library/subsonic sections 2026-04-19 17:40:36 +00:00
bvandeusen 9f325cf309 feat(cmd): plumb SubsonicConfig into server.New 2026-04-19 17:40:24 +00:00
bvandeusen 644af30a86 test(server): update New() call for new Subsonic arg 2026-04-19 17:40:10 +00:00
bvandeusen 5288e9d5df feat(server): wire subsonic.Mount under /rest 2026-04-19 17:40:05 +00:00
bvandeusen f8b0d78544 test(config): subsonic env override 2026-04-19 17:39:48 +00:00
bvandeusen e649ad2f66 feat(config): subsonic.allow_plaintext_password 2026-04-19 17:39:30 +00:00
bvandeusen 841a3d8165 test(subsonic): decodePassword + md5(password+salt) derivation 2026-04-19 17:39:12 +00:00
bvandeusen 1885d197d6 test(subsonic): envelope JSON/XML/JSONP + WriteFail 2026-04-19 17:39:03 +00:00
bvandeusen da2bb672f0 feat(subsonic): mount /rest router with /ping and /getLicense
Exposes each handler at /rest/name and /rest/name.view, GET+POST, for
client convention compatibility.
2026-04-19 17:38:44 +00:00
bvandeusen 2a75fc6687 feat(subsonic): ping.view + getLicense.view handlers 2026-04-19 17:38:34 +00:00
bvandeusen cdf56801cd feat(subsonic): auth middleware (apiKey / t+s / p gated)
apiKey (OpenSubsonic) is preferred. t+s uses md5(subsonic_password+salt)
with a constant-time compare. p is disabled by default and gated by
SubsonicConfig.AllowPlaintextPassword; enc:HEX obfuscation decoded.
Auth failures write a Subsonic "failed" envelope so clients don't see
HTTP 401.
2026-04-19 17:38:28 +00:00
bvandeusen fd408d78af feat(subsonic): response envelope with JSON/XML/JSONP emit
Renders one wire format per ?f= parameter; JSON wraps in
{"subsonic-response":...}, XML emits with xmlns, JSONP wraps in callback
or falls back to JSON when callback is empty.
2026-04-19 17:38:07 +00:00
bvandeusen 4a447e081d feat(subsonic): error code constants 2026-04-19 17:37:49 +00:00
bvandeusen f916cde8c8 chore(sqlc): regenerate models.go for subsonic_password 2026-04-19 17:37:40 +00:00
bvandeusen 2d153e1e9a chore(sqlc): regenerate users.sql.go for subsonic_password 2026-04-19 17:37:32 +00:00
bvandeusen bc22ec9a91 feat(db): add SetSubsonicPassword query
Allows setting or clearing the opt-in Subsonic legacy credential.
2026-04-19 17:37:18 +00:00
bvandeusen b5bf0cc9d6 feat(db): migration 0003 down — drop users.subsonic_password 2026-04-19 17:37:12 +00:00
bvandeusen 9fd3fec149 feat(db): migration 0003 - users.subsonic_password opt-in column
Subsonic token auth (t=md5(password+salt)) needs a reversibly stored
credential; bcrypt password_hash can't serve. NULL means the user has
not opted in, forcing apiKey (OpenSubsonic) instead.
2026-04-19 17:37:11 +00:00
bvandeusen 6e776810b2 M1/#293: RequireAdmin middleware + UserFromContext 2026-04-19 15:17:16 +00:00
bvandeusen c58061778c M1/#293: integration test for scanner walk + incremental 2026-04-19 15:17:09 +00:00
bvandeusen fdf28efef0 M1/#293: library scanner (walk + tag-parse + upsert + mtime incremental) 2026-04-19 15:16:48 +00:00
bvandeusen 968087198d M1/#293: update server_test for new New() signature 2026-04-19 15:16:22 +00:00
bvandeusen 8e91235b88 M1/#293: add /api/admin/scan route gated on X-API-Token 2026-04-19 15:16:15 +00:00
bvandeusen bea0604846 M1/#293: wire scanner into startup + server 2026-04-19 15:16:03 +00:00
bvandeusen e3257a705a M1/#293: regenerate sqlc output for GetAlbumByArtistAndTitle 2026-04-19 15:15:50 +00:00
bvandeusen 918506168a M1/#293: add GetAlbumByArtistAndTitle for no-mbid dedupe 2026-04-19 15:15:35 +00:00
bvandeusen 29d32e3097 M1/#293: cover LibraryConfig env + yaml 2026-04-19 15:15:29 +00:00
bvandeusen 7cc7e17abc M1/#293: add LibraryConfig + env overrides 2026-04-19 15:15:12 +00:00
bvandeusen 0e727b85b0 M1/#293: go.sum for dhowden/tag 2026-04-19 15:14:56 +00:00
bvandeusen c5d83364cd M1/#293: add dhowden/tag dep for library scanner 2026-04-19 15:11:40 +00:00
bvandeusen 442668874d M1/#292: integration test for admin bootstrap (MINSTREL_TEST_DATABASE_URL-gated) 2026-04-19 02:32:58 +00:00
bvandeusen 33f6514696 M1/#292: auth.Bootstrap for empty-users-table first-run admin 2026-04-19 02:32:43 +00:00
bvandeusen 9e59a0cf73 M1/#292: sqlc-generated users queries 2026-04-19 02:32:31 +00:00
bvandeusen 451971a9bd M1/#292: sqlc-generated tracks queries 2026-04-19 02:32:24 +00:00
bvandeusen da23c62262 M1/#292: sqlc-generated albums queries 2026-04-19 02:32:08 +00:00
bvandeusen 0377c58513 M1/#292: sqlc-generated artists queries 2026-04-19 02:31:58 +00:00
bvandeusen 2c10d4e23a M1/#292: sqlc-generated models (Album/Artist/Track/User) 2026-04-19 02:31:49 +00:00
bvandeusen e0baeb2980 M1/#292: sqlc-generated DBTX + Queries 2026-04-19 02:31:43 +00:00
bvandeusen 34ae7d6cde M1/#292: user queries (create + lookup by username/token + count) 2026-04-19 02:31:39 +00:00
bvandeusen 85673f9881 M1/#292: track queries (upsert-by-file_path/get-by-id/get-by-path/list-by-album) 2026-04-19 02:31:36 +00:00
bvandeusen 58b29cc4bd M1/#292: album queries (upsert/get/list-by-artist) 2026-04-19 02:31:31 +00:00
bvandeusen 7c46d8358f M1/#292: artist queries (upsert/get/list) 2026-04-19 02:31:27 +00:00
bvandeusen a243357ce6 M1/#292: down migration for core library 2026-04-19 02:31:23 +00:00
bvandeusen 75733a0f7f M1/#292: core library migration (artists/albums/tracks/users) 2026-04-19 02:31:21 +00:00
bvandeusen 0776dd05ae M1/#292: sqlc v2 config → dbq package via pgx/v5 2026-04-19 02:31:10 +00:00
bvandeusen 8dd064616a M1/#292: add Makefile with sqlc generate + test/lint/build 2026-04-19 02:31:06 +00:00
bvandeusen d358d7e1a0 M1/#292: wire auth.Bootstrap into startup 2026-04-19 02:31:05 +00:00
bvandeusen d7e04a757a M1/#292: test env overrides for admin bootstrap 2026-04-19 02:30:51 +00:00
bvandeusen 70cd49299a M1/#292: add Auth.AdminBootstrap config + env overrides 2026-04-19 02:30:39 +00:00
bvandeusen 69ffc32b7d M1/#292: go.sum tidy for bcrypt 2026-04-19 02:30:27 +00:00
bvandeusen 8f2317c567 M1/#292: add golang.org/x/crypto as direct dep (bcrypt) 2026-04-19 02:27:26 +00:00
bvandeusen 79cd25b24a M1/#291: drop goose-style stub migration (replaced by 0001_init.*) 2026-04-19 01:35:23 +00:00
bvandeusen 08a627b10b M1/#291: placeholder down migration 2026-04-19 01:12:39 +00:00
bvandeusen ceb0161677 M1/#291: placeholder up migration to establish runner 2026-04-19 01:12:37 +00:00
bvandeusen 3e6ce809d0 M1/#291: document SMARTMUSIC_* env vars + test DSN 2026-04-19 01:12:35 +00:00
bvandeusen c252608558 M1/#291: add minstrel service to compose stack 2026-04-19 01:12:31 +00:00
bvandeusen e54482409a M1/#291: run migrations + open pool on startup 2026-04-19 01:12:23 +00:00
bvandeusen 9c66736916 M1/#291: env-gated migration test against real postgres 2026-04-19 01:12:12 +00:00
bvandeusen 380a76c053 M1/#291: wire pgxpool + golang-migrate runner with embedded migrations 2026-04-19 01:12:02 +00:00
bvandeusen 8041d19dbe M1/#291: sync go.sum for pgx + golang-migrate deps 2026-04-19 01:11:46 +00:00
bvandeusen e0b7e27d78 M1/#291: add pgx/v5 + golang-migrate deps, pinned for Go 1.23 2026-04-19 01:05:12 +00:00
bvandeusen aa7599f91d ci(release): use REGISTRY_TOKEN secret for Forgejo registry push
The default GITHUB_TOKEN provided by Forgejo Actions lacks write:package
scope, so docker buildx push failed with 401 reqPackageAccess. Swap in
the repo-level REGISTRY_TOKEN secret (a PAT scoped write:package) for
docker login; github.actor still supplies the username.

Also drops the transient Docker environment diagnostics step — the
registry was the issue, the socket/buildx setup is fine.
2026-04-18 23:29:15 +00:00
bvandeusen 0f9efed50c ci(release): add workflow_dispatch + pre-build docker diagnostics
Adds a manual-trigger hook so we don't need a fresh commit to re-run
this pipeline, and dumps docker/buildx state before the build so CI
failures are diagnosable without docker.sock guesswork.
2026-04-18 23:19:06 +00:00
bvandeusen 8fbf355305 fix(server): silence errcheck on deferred Body.Close 2026-04-18 21:48:07 +00:00
bvandeusen 4c5b4d1790 fix(server): rename unused request param to _ for revive/unused-parameter 2026-04-18 21:47:52 +00:00
bvandeusen 743fbe9d5c ci(lint): drop revive exported rule; require no doc-comment boilerplate
Revive's `exported` check forced doc comments on every exported symbol —
that conflicts with the project's policy of only writing comments when
the "why" is non-obvious. Keep `var-naming`, `unused-parameter`, and
`early-return` for signal that doesn't mandate prose.
2026-04-18 21:47:40 +00:00
bvandeusen 9bce7fee10 ci: add .golangci.yml config for CI lint step 2026-04-18 21:21:14 +00:00
bvandeusen 70c89bb7df feat(skel): example YAML config mirroring Default() 2026-04-18 21:21:08 +00:00
bvandeusen b40e379b84 feat(skel): docker-compose for local Postgres (integration-test stack) 2026-04-18 21:21:01 +00:00
bvandeusen 7077614116 feat(skel): multi-stage Dockerfile for minstrel runtime
Builder uses golang:1.23-bookworm (matches runner-base:go-ci).
Runtime is debian:bookworm-slim with ffmpeg (server spec §3) plus a
non-root minstrel user. Release workflow builds from this Dockerfile.
2026-04-18 21:20:54 +00:00
bvandeusen ff7cd8d38b feat(skel): placeholder migration so embed.FS has a file to index 2026-04-18 21:20:45 +00:00
bvandeusen 9b9e0e4011 test(db): verify migrations stub loads embedded files and returns ErrNotConfigured 2026-04-18 21:20:40 +00:00
bvandeusen 58fa790b2b feat(skel): db package with embedded migrations stub
Establishes the embed.FS and migrations/ layout up front. Migrate()
returns ErrNotConfigured intentionally — the real goose runner lands in
M2 alongside Postgres work, avoiding a toolchain bump to Go 1.24/1.25
through goose's transitive modernc.org/libc dependency chain.
2026-04-18 21:20:34 +00:00
bvandeusen 4dbf02d645 test(server): verify /healthz returns 200 with {"status":"ok"} 2026-04-18 21:20:23 +00:00
bvandeusen 16f2fc2ce4 feat(skel): chi router with RequestID/Recoverer middleware and /healthz 2026-04-18 21:20:17 +00:00
bvandeusen d206f9281f feat(skel): slog logger factory with text|json format and level parsing 2026-04-18 21:20:10 +00:00
bvandeusen a29d876ac4 test(config): cover defaults, YAML load, missing file, env overrides 2026-04-18 21:20:02 +00:00