Correct an owned album's artist/album/year from the Library modal ("Edit
metadata"). PATCH /api/library/[id] renames the folder to the canonical
Artist/Album (Year) scheme on the /music mount, updates the LibraryItem,
and re-resolves the MusicBrainz release-group (cover art) when the
artist/album changed (best-effort — keeps the old rgMbid on an MB outage).
- New shared lib/library-path.ts (safeName, albumDir, yearFromPath); the
manual-import route now uses it so edits and imports name folders
identically.
- Library GET falls back to the folder's "(YYYY)" for the year when there's
no matching release, so edited/manual years show in the grid.
- Does NOT rewrite embedded file tags (a worker/mutagen job) — Lyra reads
from the folder + DB. Guards paths inside the library root; 409 on folder
or (artist,album) collision.
web 172 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A manually-imported album had no release-group MBID, so it showed the ◉
placeholder until a Scan matched it. Now the import resolves the release group
(searchReleaseGroup, via the shared MB cache) and stores it on the new
LibraryItem.rgMbid column (migration add_library_rg_mbid). The library route
prefers LibraryItem.rgMbid, falling back to the MonitoredRelease join for
scanned items — so a manual album gets cover art immediately. Best-effort: a
no-match / MB outage just leaves rgMbid null.
web 164 tests, tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The real cause of the failed 584MB album upload: Next.js caps the request body
at 10MB for any route that passes through middleware, and the import route went
through the auth gate. Exclude /api/library/import from the middleware matcher so
the body streams uncapped to busboy, and authenticate it inline instead (new
lib/auth.isAuthed reads + verifies the session cookie from the request).
web 161 tests, tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manual import used request.formData(), which buffers the whole body and caps out
around ~15MB — a real FLAC album is hundreds of MB (Hollywood's Bleeding is
584MB), so it failed with "expected multipart form data". Now the multipart body
is STREAMED with busboy: each file pipes straight to a hidden .import-<uuid>
staging dir on the library filesystem, then the dir is atomically renamed into
place. No full-body buffering, no size cap. Cleaned up on any failure; guards
(400 missing fields/audio, 409 exists) unchanged. web 161 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second Library Management feature. An "Add album" button on the Library page
opens a modal: drag-drop (or pick) the audio files (+ optional cover image),
enter artist/album/year, and Lyra writes the album into the library.
- POST /api/library/import (auth-gated, multipart): sanitizes the folder name
like the worker's safe_name, assembles into a sibling ".importing" temp dir
then renames into place (never a half-written album), writes a cover.jpg from
any image, and records a source="manual" LibraryItem with the on-disk
trackNames + a rough quality class from the extension (a later Scan re-probes /
MB-resolves for cover art + hi-res detection). Guards: 400 (missing
artist/album or no audio), 409 (folder exists / already in library).
- Library page: ImportAlbum drop-zone modal wired to refresh the grid.
web 161 tests (write-to-disk + trackNames + guards + filename sanitization).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Also: DELETE now prunes an emptied artist folder (best-effort).