feat(web): Library Management — manual album import (drag-and-drop)

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).
This commit is contained in:
Jonathan
2026-07-14 14:00:35 +02:00
parent 0d3fe8d907
commit 93ba9c7e16
6 changed files with 299 additions and 3 deletions
+6
View File
@@ -204,6 +204,12 @@ nav.contents .sep { flex: 1; }
font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--graphite); margin: 0 0 10px;
}
.import-form { display: flex; flex-direction: column; gap: 6px; }
.dropzone {
border: 1.5px dashed var(--rule-2); padding: 28px 20px; text-align: center; cursor: pointer;
font-family: var(--mono); font-size: 0.8rem; color: var(--graphite); margin-bottom: 8px;
}
.dropzone:hover, .dropzone.over { border-color: var(--accent); color: var(--accent); }
/* ── Phase 2: page headers, list rows, tools ──────────── */
.page-head { margin: 6px 0 26px; }