Commit Graph
100 Commits
Author SHA1 Message Date
bvandeusen cd535b9de6 Merge pull request 'feat(library): extract track duration via ffprobe' (#13) from dev into main 2026-04-20 12:11:09 +00: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
bvandeusen be92087ba1 Merge pull request 'fix(library): drop out-of-range release years instead of failing the album' (#12) from dev into main 2026-04-20 04:22:18 +00: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
bvandeusen 22bf7a70f2 Merge pull request 'feat(subsonic): getUser + envelope-shaped 404 for /rest/*' (#11) from dev into main 2026-04-20 03:13:39 +00: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
bvandeusen 5318f9b8dc Merge pull request 'fix(auth): subsonic_password on bootstrap + dev compose defaults' (#10) from dev into main 2026-04-19 23:07:13 +00: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 f49d672b5f Merge pull request 'feat(subsonic): stream/download/getCoverArt/scrobble (#296)' (#9) from dev into main 2026-04-19 19:33:10 +00: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 6637bbfdaf Merge pull request 'feat(subsonic): browse endpoints — getArtists/getAlbum/search3/… (#295)' (#8) from dev into main 2026-04-19 19:13:12 +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 b6f7c894fb Merge pull request 'feat(subsonic): auth + envelope + ping.view + getLicense.view (Fable #294)' (#7) from dev into main 2026-04-19 18:10:12 +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 0805320ec3 Merge pull request 'M1/#293: library scanner (walk + tag-parse + upsert incremental) + admin scan endpoint' (#6) from dev into main 2026-04-19 15:21:10 +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 8e1f8c4ae7 Merge pull request 'M1/#292: core library schema + sqlc + admin bootstrap' (#5) from dev into main 2026-04-19 02:36:25 +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 2062ec53bb Merge pull request 'M1/#291: wire Postgres migrations + pgxpool' (#4) from dev into main 2026-04-19 01:38:09 +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 d70ac2c81c ci(release): switch registry login to REGISTRY_TOKEN (#3)
Uses the repo-scoped REGISTRY_TOKEN PAT (write:package) for docker
login instead of the default GITHUB_TOKEN, which Forgejo does not
issue with container-registry write scope.
2026-04-18 23:33:08 +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 04e63f220b ci(release): add workflow_dispatch + docker diagnostics (#2)
Enables manual retrigger of release.yml and dumps docker/buildx state
before the build so the next failure is diagnosable without guessing
at socket mounts or buildx driver setup.
2026-04-18 23:21:17 +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 72b8e00d80 M0: repo scaffolding, CI pipelines, and Go skeleton (#1)
Closes milestone M0: CI (test + release), Forgejo runner on go-ci label,
Go skeleton (cmd/minstrel, config, logging, server with /healthz,
migrations stub), Dockerfile, docker-compose, golangci-lint config.

Preserves `dev` branch for ongoing feature work per the documented
dev → main → tag release workflow.
2026-04-18 22:00:45 +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