feat(mixtape): DiscographyBuilderView for Artist Collections

Cross-source artist discography builder. Fetches the artist's Qobuz
releases (via v2_get_artist_page) and LocalLibrary albums by artist
name (via v2_library_get_albums filtered client-side), normalizes
titles to group variants of the same album, ranks each group by
quality_score (bit_depth × 10M + sample_rate_hz + format_bonus), and
renders a primary row + indented alternate rows per group.

Compilations detected heuristically (best of / greatest hits /
anthology / ...) and unchecked by default. Default selection: primary
of each non-compilation group. User can check alternates to include
them too. Order by release date / title / manual (preserves input
order). Submit creates an artist_collection with
source_type='artist_discography' + source_ref=artistId and enqueues
items in display order.

ArtistDetailView gains a "Build Artist Collection" action button
(LibraryBig icon, secondary style, rendered only when callback prop is
present). Entry point from CollectionsView left as-is (artist picker
deferred). Added 'discography-builder' to ViewType union.

Toast keys toast.collectionCreated / toast.collectionCreateFailed
added to all five locale files (en, es, de, fr, pt).

cc
This commit is contained in:
vicrodh committed 2026-04-18 20:57:02 -06:00
1 parent b7458522b3
commit 2460467e48
9 files changed
+880 -4

No files matched your search

+28 -2
View File
@@ -189,6 +189,7 @@
import MixtapesView from '$lib/components/views/MixtapesView.svelte';
import CollectionsView from '$lib/components/views/CollectionsView.svelte';
import MixtapeCollectionDetailView from '$lib/components/views/MixtapeCollectionDetailView.svelte';
import DiscographyBuilderView from '$lib/components/views/DiscographyBuilderView.svelte';
import {
collectionsStore,
createCollection,
@@ -572,6 +573,7 @@
// Mixtape / Collection routing state
let mixtapeDetailId = $state<string | null>(null);
let discographyArtistId = $state<string | null>(null);
let showCreateModal = $state(false);
let createModalKind = $state<CollectionKind>('mixtape');
let createModalName = $state('');
@@ -5454,6 +5456,10 @@
onLabelClick={handleLabelClick}
onMusicianClick={handleMusicianClick}
onLocationClick={handleLocationClick}
onBuildArtistCollection={(artistId) => {
discographyArtistId = artistId;
navTo('discography-builder', artistId);
}}
activeTrackId={currentTrack?.id ?? null}
isPlaybackActive={isPlaying}
/>
@@ -5941,8 +5947,8 @@
onOpen={(id) => openMixtapeDetail(id)}
onCreate={() => openCreateModal('collection')}
onBuildArtistCollection={() => {
// TODO: Phase 8 — open DiscographyBuilderView
console.log('[+page] Build Artist Collection clicked — Phase 8 feature');
// Artist picker not yet available from CollectionsView — entry point lives in ArtistDetailView
console.log('[+page] Build Artist Collection: use ArtistDetailView entry point');
}}
/>
{:else if activeView === 'mixtape-detail'}
@@ -5964,6 +5970,26 @@
<p>No collection selected.</p>
</div>
{/if}
{:else if activeView === 'discography-builder'}
{#if discographyArtistId}
<DiscographyBuilderView
artistId={discographyArtistId}
onBack={() => {
const prevId = discographyArtistId;
discographyArtistId = null;
navigateTo('artist', prevId ?? undefined);
}}
onCreated={(col) => {
discographyArtistId = null;
mixtapeDetailId = col.id;
navTo('mixtape-detail', col.id);
}}
/>
{:else}
<div class="detail-placeholder">
<p>No artist selected.</p>
</div>
{/if}
{:else}
<!-- Catch-all fallback: view has no matching data, show loading/error -->
<div class="view-error">