internal/library/scanner.go: filepath.WalkDir over library.scan_paths; per-file os.Stat → mtime vs tracks.updated_at skip; tag.ReadFrom (pure-Go, no cgo via github.com/dhowden/tag); resolve-or-create artist (GetArtistByName → UpsertArtist) and album (GetAlbumByArtistAndTitle → UpsertAlbum); UpsertTrack. Defaults: Unknown Artist / Unknown Album / filename fallback. DurationMs=0 pending ffprobe. Walk + per-file errors logged and counted; scan keeps going. Context-cancel via fs.SkipAll.
internal/server/server.go: ScanTrigger interface for testability; mounts /api/admin with RequireAdmin and POST /api/admin/scan (synchronous — small-library v1).
cmd/minstrel/main.go: startup scan in goroutine when scan_on_startup + non-empty paths; scanner passed into server.New.
internal/db/queries/albums.sql: GetAlbumByArtistAndTitle for the no-mbid dedupe path (partial-unique mbid index doesn't dedupe NULL rows).
internal/library/scanner_test.go: integration test gated on MINSTREL_TEST_DATABASE_URL; generates ID3v2.3 fixtures at test time (no binaries in git); asserts walk counts, article-strip sort (The Artist Y → sort Artist Y), and incremental skip on re-scan.
Test plan
go build ./... clean, go vet ./... clean, gofmt -l clean
go test -short ./... passes
MINSTREL_TEST_DATABASE_URL integration run: scanner test green (first scan Added=4, second Skipped=4; artist sort [Artist X, Artist Y])
CI-runner image parity: golangci-lint + short tests green
E2E smoke on local pg: /healthz 200; /api/admin/scan — 401 (no token), 401 (bad token), 200 (admin token) with JSON stats
CI test.yml run on this PR green
Release :main image publishes after merge
## Summary
Implements Fable task #293 — library scanner (spec §4 #1, §12).
- **`internal/library/scanner.go`**: `filepath.WalkDir` over `library.scan_paths`; per-file `os.Stat` → mtime vs `tracks.updated_at` skip; `tag.ReadFrom` (pure-Go, no cgo via `github.com/dhowden/tag`); resolve-or-create artist (`GetArtistByName` → `UpsertArtist`) and album (`GetAlbumByArtistAndTitle` → `UpsertAlbum`); `UpsertTrack`. Defaults: `Unknown Artist` / `Unknown Album` / filename fallback. `DurationMs=0` pending ffprobe. Walk + per-file errors logged and counted; scan keeps going. Context-cancel via `fs.SkipAll`.
- **`internal/auth/middleware.go`**: `RequireAdmin(pool)` checks `X-API-Token` → `GetUserByAPIToken` → `IsAdmin`; 401 missing/invalid, 403 not admin, 500 lookup. Exposes `UserFromContext`.
- **`internal/server/server.go`**: `ScanTrigger` interface for testability; mounts `/api/admin` with `RequireAdmin` and `POST /api/admin/scan` (synchronous — small-library v1).
- **`internal/config/config.go`**: `LibraryConfig{ScanPaths, ScanOnStartup}`; `SMARTMUSIC_LIBRARY_SCAN_PATHS` (colon-separated, empty entries dropped) + `SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP` env.
- **`cmd/minstrel/main.go`**: startup scan in goroutine when `scan_on_startup` + non-empty paths; scanner passed into `server.New`.
- **`internal/db/queries/albums.sql`**: `GetAlbumByArtistAndTitle` for the no-mbid dedupe path (partial-unique mbid index doesn't dedupe NULL rows).
- **`internal/library/scanner_test.go`**: integration test gated on `MINSTREL_TEST_DATABASE_URL`; generates ID3v2.3 fixtures at test time (no binaries in git); asserts walk counts, article-strip sort (`The Artist Y` → sort `Artist Y`), and incremental skip on re-scan.
## Test plan
- [x] `go build ./...` clean, `go vet ./...` clean, `gofmt -l` clean
- [x] `go test -short ./...` passes
- [x] `MINSTREL_TEST_DATABASE_URL` integration run: scanner test green (first scan Added=4, second Skipped=4; artist sort `[Artist X, Artist Y]`)
- [x] CI-runner image parity: golangci-lint + short tests green
- [x] E2E smoke on local pg: `/healthz` 200; `/api/admin/scan` — 401 (no token), 401 (bad token), 200 (admin token) with JSON stats
- [ ] CI `test.yml` run on this PR green
- [ ] Release `:main` image publishes after merge
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements Fable task #293 — library scanner (spec §4 #1, §12).
internal/library/scanner.go:filepath.WalkDiroverlibrary.scan_paths; per-fileos.Stat→ mtime vstracks.updated_atskip;tag.ReadFrom(pure-Go, no cgo viagithub.com/dhowden/tag); resolve-or-create artist (GetArtistByName→UpsertArtist) and album (GetAlbumByArtistAndTitle→UpsertAlbum);UpsertTrack. Defaults:Unknown Artist/Unknown Album/ filename fallback.DurationMs=0pending ffprobe. Walk + per-file errors logged and counted; scan keeps going. Context-cancel viafs.SkipAll.internal/auth/middleware.go:RequireAdmin(pool)checksX-API-Token→GetUserByAPIToken→IsAdmin; 401 missing/invalid, 403 not admin, 500 lookup. ExposesUserFromContext.internal/server/server.go:ScanTriggerinterface for testability; mounts/api/adminwithRequireAdminandPOST /api/admin/scan(synchronous — small-library v1).internal/config/config.go:LibraryConfig{ScanPaths, ScanOnStartup};SMARTMUSIC_LIBRARY_SCAN_PATHS(colon-separated, empty entries dropped) +SMARTMUSIC_LIBRARY_SCAN_ON_STARTUPenv.cmd/minstrel/main.go: startup scan in goroutine whenscan_on_startup+ non-empty paths; scanner passed intoserver.New.internal/db/queries/albums.sql:GetAlbumByArtistAndTitlefor the no-mbid dedupe path (partial-unique mbid index doesn't dedupe NULL rows).internal/library/scanner_test.go: integration test gated onMINSTREL_TEST_DATABASE_URL; generates ID3v2.3 fixtures at test time (no binaries in git); asserts walk counts, article-strip sort (The Artist Y→ sortArtist Y), and incremental skip on re-scan.Test plan
go build ./...clean,go vet ./...clean,gofmt -lcleango test -short ./...passesMINSTREL_TEST_DATABASE_URLintegration run: scanner test green (first scan Added=4, second Skipped=4; artist sort[Artist X, Artist Y])/healthz200;/api/admin/scan— 401 (no token), 401 (bad token), 200 (admin token) with JSON statstest.ymlrun on this PR green:mainimage publishes after merge