summaryrefslogtreecommitdiffhomepage
path: root/yt-music-dl-web/src/features
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-07-01 22:16:09 +0900
committerAdam Malczewski <[email protected]>2026-07-01 22:16:52 +0900
commitb476f7703c594e9c8ff1b1ecc77f8e2493ef04b4 (patch)
tree22971336f0a7a1ec68f7b9b4f24c80d159ea1118 /yt-music-dl-web/src/features
parent6402a83fe83a0b1a9f5959ff7f45d07d3e309c6e (diff)
downloadyt-walkerman-main.tar.gz
yt-walkerman-main.zip
Phase 8: album art in metadata search results (frontend)HEADmain
Diffstat (limited to 'yt-music-dl-web/src/features')
-rw-r--r--yt-music-dl-web/src/features/library/LibraryMetadataSidebar.svelte12
-rw-r--r--yt-music-dl-web/src/features/library/logic.test.ts31
-rw-r--r--yt-music-dl-web/src/features/library/logic.ts9
-rw-r--r--yt-music-dl-web/src/features/segment-editor/MetadataSidebar.svelte12
-rw-r--r--yt-music-dl-web/src/features/segment-editor/metadata.test.ts57
-rw-r--r--yt-music-dl-web/src/features/segment-editor/metadata.ts14
6 files changed, 129 insertions, 6 deletions
diff --git a/yt-music-dl-web/src/features/library/LibraryMetadataSidebar.svelte b/yt-music-dl-web/src/features/library/LibraryMetadataSidebar.svelte
index 99d7121..aef4165 100644
--- a/yt-music-dl-web/src/features/library/LibraryMetadataSidebar.svelte
+++ b/yt-music-dl-web/src/features/library/LibraryMetadataSidebar.svelte
@@ -222,7 +222,17 @@
<span class="text-[10px] text-base-content/40">current</span>
{/if}
</div>
- <p class="mt-1 truncate text-sm font-medium">{item.title || "(untitled)"}</p>
+ <div class="mt-1 flex items-center gap-1.5">
+ {#if item.artUrl}
+ <img
+ src={item.artUrl}
+ alt=""
+ class="h-5 w-5 shrink-0 rounded object-cover"
+ loading="lazy"
+ />
+ {/if}
+ <p class="truncate text-sm font-medium">{item.title || "(untitled)"}</p>
+ </div>
<p class="truncate text-xs text-base-content/70">{item.artist}</p>
<p class="truncate text-xs text-base-content/50">
{#if item.album}{item.album}{:else}<span class="italic">no album</span>{/if}
diff --git a/yt-music-dl-web/src/features/library/logic.test.ts b/yt-music-dl-web/src/features/library/logic.test.ts
index 4f84bef..388d76e 100644
--- a/yt-music-dl-web/src/features/library/logic.test.ts
+++ b/yt-music-dl-web/src/features/library/logic.test.ts
@@ -418,6 +418,37 @@ describe("library logic (pure)", () => {
expect(req).toEqual({ title: "T", artist: "A", album: "Al" });
expect("track" in req).toBe(false);
});
+
+ it("includes artUrl when the result has one", () => {
+ const r: MetadataResult = {
+ id: "m1",
+ type: "recording",
+ title: "T",
+ artist: "A",
+ score: 70,
+ artUrl: "https://coverartarchive.org/release/abc/front",
+ };
+ const items = trackSidebarItems(base, [r]);
+ const mbItem = items[1];
+ if (!mbItem) throw new Error("expected an MB item");
+ const req = toUpdateRequestFromItem(mbItem);
+ expect(req.artUrl).toBe("https://coverartarchive.org/release/abc/front");
+ });
+
+ it("omits artUrl when the result has none", () => {
+ const r: MetadataResult = {
+ id: "m1",
+ type: "recording",
+ title: "T",
+ artist: "A",
+ score: 70,
+ };
+ const items = trackSidebarItems(base, [r]);
+ const mbItem = items[1];
+ if (!mbItem) throw new Error("expected an MB item");
+ const req = toUpdateRequestFromItem(mbItem);
+ expect("artUrl" in req).toBe(false);
+ });
});
// ─── Match Album helpers (pure) ────────────────────────────────────────────
diff --git a/yt-music-dl-web/src/features/library/logic.ts b/yt-music-dl-web/src/features/library/logic.ts
index fc29b37..4d3676c 100644
--- a/yt-music-dl-web/src/features/library/logic.ts
+++ b/yt-music-dl-web/src/features/library/logic.ts
@@ -234,9 +234,11 @@ export function trackSidebarItems(track: Track, results: MetadataResult[]): Side
* Build a PATCH /api/library/:id body (UpdateTrackRequest) from a clicked
* sidebar item. The "Current tags" entry (source: "youtube") produces an empty
* body (no-op); an MB result sets title + artist always, album + track only
- * when the result actually carries them. Mirrors fillActions from metadata.ts
- * but produces an UpdateTrackRequest (a PATCH body) instead of EditAction[]
- * (in-memory draft edits). Pure: (item) → request.
+ * when the result actually carries them. When the result has a Cover Art
+ * Archive `artUrl`, it is included so the backend downloads + embeds the
+ * front cover. Mirrors fillActions from metadata.ts but produces an
+ * UpdateTrackRequest (a PATCH body) instead of EditAction[] (in-memory draft
+ * edits). Pure: (item) → request.
*/
export function toUpdateRequestFromItem(item: SidebarItem): UpdateTrackRequest {
if (item.source === "youtube") return {}; // no-op — current tags
@@ -246,6 +248,7 @@ export function toUpdateRequestFromItem(item: SidebarItem): UpdateTrackRequest {
};
if (item.album !== undefined) req.album = item.album;
if (item.trackNumber !== undefined) req.track = item.trackNumber;
+ if (item.artUrl !== undefined) req.artUrl = item.artUrl;
return req;
}
diff --git a/yt-music-dl-web/src/features/segment-editor/MetadataSidebar.svelte b/yt-music-dl-web/src/features/segment-editor/MetadataSidebar.svelte
index 8b3b956..45944e7 100644
--- a/yt-music-dl-web/src/features/segment-editor/MetadataSidebar.svelte
+++ b/yt-music-dl-web/src/features/segment-editor/MetadataSidebar.svelte
@@ -182,7 +182,17 @@
<span class="text-[10px] text-base-content/40">current</span>
{/if}
</div>
- <p class="mt-1 truncate text-sm font-medium">{item.title || "(untitled)"}</p>
+ <div class="mt-1 flex items-center gap-1.5">
+ {#if item.artUrl}
+ <img
+ src={item.artUrl}
+ alt=""
+ class="h-5 w-5 shrink-0 rounded object-cover"
+ loading="lazy"
+ />
+ {/if}
+ <p class="truncate text-sm font-medium">{item.title || "(untitled)"}</p>
+ </div>
<p class="truncate text-xs text-base-content/70">{item.artist}</p>
<p class="truncate text-xs text-base-content/50">
{#if item.album}{item.album}{:else}<span class="italic">no album</span>{/if}
diff --git a/yt-music-dl-web/src/features/segment-editor/metadata.test.ts b/yt-music-dl-web/src/features/segment-editor/metadata.test.ts
index b917195..c4ff8dc 100644
--- a/yt-music-dl-web/src/features/segment-editor/metadata.test.ts
+++ b/yt-music-dl-web/src/features/segment-editor/metadata.test.ts
@@ -265,6 +265,23 @@ describe("sidebarItems (pure)", () => {
expect(item.score).toBe(50);
});
+ it("toSidebarItem maps artUrl when the result has one", () => {
+ const r: MetadataResult = {
+ id: "m1",
+ type: "recording",
+ title: "T",
+ artist: "A",
+ score: 70,
+ artUrl: "https://coverartarchive.org/release/abc/front",
+ };
+ expect(toSidebarItem(r).artUrl).toBe("https://coverartarchive.org/release/abc/front");
+ });
+
+ it("toSidebarItem omits artUrl when the result has none", () => {
+ const r: MetadataResult = { id: "m1", type: "recording", title: "T", artist: "A", score: 70 };
+ expect(toSidebarItem(r).artUrl).toBeUndefined();
+ });
+
it("youtubeItem reflects the segment's current parsed fields", () => {
const item = youtubeItem(seg("s1", "Title", "Artist", "Album", 5));
expect(item).toEqual({
@@ -329,10 +346,50 @@ describe("fillActions (pure)", () => {
expect(actions.some((a) => a.type === "editSegmentTrackNumber")).toBe(false);
});
+ it("emits editSegmentAlbumArt when the result has artUrl", () => {
+ const r: MetadataResult = {
+ id: "m1",
+ type: "recording",
+ title: "T",
+ artist: "A",
+ score: 70,
+ artUrl: "https://coverartarchive.org/release/abc/front",
+ };
+ const actions = fillActions("s1", toSidebarItem(r));
+ expect(actions).toContainEqual({
+ type: "editSegmentAlbumArt",
+ segmentId: "s1",
+ albumArt: { kind: "url", url: "https://coverartarchive.org/release/abc/front" },
+ });
+ });
+
+ it("omits editSegmentAlbumArt when the result has no artUrl", () => {
+ const r: MetadataResult = { id: "m1", type: "recording", title: "T", artist: "A", score: 70 };
+ const actions = fillActions("s1", toSidebarItem(r));
+ expect(actions.some((a) => a.type === "editSegmentAlbumArt")).toBe(false);
+ });
+
it("fillFromResult matches fillActions(toSidebarItem)", () => {
const r = mbResult("m1", "T", "A", "Al", 2, 90);
expect(fillFromResult("s1", r)).toEqual(fillActions("s1", toSidebarItem(r)));
});
+
+ it("fillFromResult carries artUrl through to an editSegmentAlbumArt action", () => {
+ const r: MetadataResult = {
+ id: "m1",
+ type: "recording",
+ title: "T",
+ artist: "A",
+ score: 80,
+ artUrl: "https://coverartarchive.org/release/xyz/front",
+ };
+ expect(fillFromResult("s1", r)).toEqual(fillActions("s1", toSidebarItem(r)));
+ expect(fillFromResult("s1", r)).toContainEqual({
+ type: "editSegmentAlbumArt",
+ segmentId: "s1",
+ albumArt: { kind: "url", url: "https://coverartarchive.org/release/xyz/front" },
+ });
+ });
});
// ─── match-album → actions ───────────────────────────────────────────────────
diff --git a/yt-music-dl-web/src/features/segment-editor/metadata.ts b/yt-music-dl-web/src/features/segment-editor/metadata.ts
index 530893a..7838686 100644
--- a/yt-music-dl-web/src/features/segment-editor/metadata.ts
+++ b/yt-music-dl-web/src/features/segment-editor/metadata.ts
@@ -137,6 +137,8 @@ export interface SidebarItem {
score?: number;
/** MusicBrainz MBID; absent for the YouTube entry. */
id?: string;
+ /** Cover Art Archive front-cover URL; absent when the result has none. */
+ artUrl?: string;
}
/** The "Generated from YouTube" entry: the segment's current parsed fields. Pure. */
@@ -163,6 +165,7 @@ export function toSidebarItem(result: MetadataResult): SidebarItem {
};
if (result.album !== undefined) item.album = result.album;
if (result.trackNumber !== undefined) item.trackNumber = result.trackNumber;
+ if (result.artUrl !== undefined) item.artUrl = result.artUrl;
return item;
}
@@ -183,7 +186,9 @@ export function sidebarItems(segment: SegmentDraft, results: MetadataResult[]):
* already mirrors the segment's current parsed values) → no edits.
* - MusicBrainz result: sets title + artist always; album + trackNumber only
* when the result actually carries them (MB omits these from lightweight
- * recording search entries).
+ * recording search entries). When the result has a Cover Art Archive
+ * `artUrl`, emits an `editSegmentAlbumArt` action with a url-kind
+ * `AlbumArtRef` so the backend downloads + embeds the front cover.
*
* Pure: (segmentId, item) → actions. Dispatched by the component through
* `onaction`; the composition root folds each over the pure `reduce`.
@@ -200,6 +205,13 @@ export function fillActions(segmentId: string, item: SidebarItem): EditAction[]
if (item.trackNumber !== undefined) {
actions.push({ type: "editSegmentTrackNumber", segmentId, trackNumber: item.trackNumber });
}
+ if (item.artUrl !== undefined) {
+ actions.push({
+ type: "editSegmentAlbumArt",
+ segmentId,
+ albumArt: { kind: "url", url: item.artUrl },
+ });
+ }
return actions;
}