feat(web): Library Management — delete an album (files + record)
First Library Management feature. On the Library album modal, "Delete album" (two-step confirm) removes the album: - deletes the folder on disk (guarded to only ever remove a path strictly inside the library root, LYRA_LIBRARY_ROOT, default /music), - drops the LibraryItem, - sets the matching MonitoredRelease monitored=false + currentQualityClass=null so the monitor won't immediately re-download it (re-Want later to restore). Requires the web container to see the library, so docker-compose now bind-mounts MUSIC_DIR at /music (rw) for web too. New DELETE /api/library/[id] (auth-gated, path-guarded). web 157 tests (delete happy-path + 404 + outside-root guard). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { PageHead } from "../_ui/page-head";
|
||||
import { SectionHeader } from "../_ui/section-header";
|
||||
import { CoverArt } from "../_ui/cover-art";
|
||||
import { AlbumModal } from "../_ui/album-modal";
|
||||
import { toast } from "../_ui/toast";
|
||||
|
||||
type Album = {
|
||||
id: string;
|
||||
@@ -23,6 +24,25 @@ export function LibraryClient() {
|
||||
const [albums, setAlbums] = useState<Album[]>([]);
|
||||
const [q, setQ] = useState("");
|
||||
const [open, setOpen] = useState<Album | null>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
function show(a: Album | null) {
|
||||
setOpen(a);
|
||||
setConfirmDelete(false);
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
if (!open) return;
|
||||
const target = open;
|
||||
const res = await fetch(`/api/library/${target.id}`, { method: "DELETE" }).catch(() => null);
|
||||
if (res?.ok) {
|
||||
setAlbums((list) => list.filter((a) => a.id !== target.id));
|
||||
toast(`Deleted ${target.album}`);
|
||||
show(null);
|
||||
} else {
|
||||
toast(`Couldn't delete ${target.album}`);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/library")
|
||||
@@ -58,7 +78,7 @@ export function LibraryClient() {
|
||||
{albums.length > 0 ? (
|
||||
<div className="album-grid">
|
||||
{shown.map((a) => (
|
||||
<button key={a.id} className="album-card" onClick={() => setOpen(a)} aria-label={`Open ${a.album}`}>
|
||||
<button key={a.id} className="album-card" onClick={() => show(a)} aria-label={`Open ${a.album}`}>
|
||||
<CoverArt rgMbid={a.rgMbid} alt={a.album} size={250} />
|
||||
<div className="ac-title">{a.album}</div>
|
||||
<div className="ac-artist">{a.artist}</div>
|
||||
@@ -75,9 +95,26 @@ export function LibraryClient() {
|
||||
{open ? (
|
||||
<AlbumModal
|
||||
open
|
||||
onClose={() => setOpen(null)}
|
||||
onClose={() => show(null)}
|
||||
album={{ rgMbid: open.rgMbid, album: open.album, artist: open.artist, year: open.year, type: open.primaryType, artistMbid: open.artistMbid, trackNames: open.trackNames }}
|
||||
status={<span className="chip done">In library · {open.format}</span>}
|
||||
actions={
|
||||
confirmDelete ? (
|
||||
<>
|
||||
<span className="rmeta">Delete this album and its files from disk?</span>
|
||||
<button className="btn sm accent" onClick={remove}>
|
||||
Delete
|
||||
</button>
|
||||
<button className="btn sm ghost" onClick={() => setConfirmDelete(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn sm ghost" onClick={() => setConfirmDelete(true)}>
|
||||
Delete album
|
||||
</button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user