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:
@@ -6,6 +6,7 @@ import { SectionHeader } from "../_ui/section-header";
|
||||
import { CoverArt } from "../_ui/cover-art";
|
||||
import { AlbumModal } from "../_ui/album-modal";
|
||||
import { toast } from "../_ui/toast";
|
||||
import { ImportAlbum } from "./import-album";
|
||||
|
||||
type Album = {
|
||||
id: string;
|
||||
@@ -44,10 +45,13 @@ export function LibraryClient() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function refresh() {
|
||||
fetch("/api/library")
|
||||
.then((r) => r.json())
|
||||
.then((d) => setAlbums(d.albums ?? []));
|
||||
}
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
const shown = useMemo(() => {
|
||||
@@ -61,6 +65,7 @@ export function LibraryClient() {
|
||||
<PageHead title="Library" eyebrow="The pressings · everything on disk" />
|
||||
|
||||
<form className="request-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<ImportAlbum onImported={refresh} />
|
||||
<label className="field">
|
||||
<span>Filter</span>
|
||||
<input aria-label="filter library" placeholder="Artist or album…" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
|
||||
Reference in New Issue
Block a user