Files
musicseerr/frontend/src/lib/components/TopAlbumsList.svelte
T
Harvey df779c9e6d Mus 19 library sync completing with issues (#29)
* fix: Sync issues - AudioDB warmig +  automatic sync skips

* progress ui/ux + discovery and album fixes

* artist fixes

* several request level fixes and improvements

* handle request fails + artist refresh + resilience fixes

* fix format

* fix stop sync fail + last.fn mbid issues + failures/validation reworks
2026-04-08 00:29:36 +01:00

198 lines
6.1 KiB
Svelte

<script lang="ts">
import { albumHref } from '$lib/utils/entityRoutes';
import { onMount } from 'svelte';
import { Music2, Download } from 'lucide-svelte';
import type { TopAlbum } from '$lib/types';
import { colors } from '$lib/colors';
import { libraryStore } from '$lib/stores/library';
import { requestAlbum } from '$lib/utils/albumRequest';
import AlbumImage from './AlbumImage.svelte';
import LibraryBadge from './LibraryBadge.svelte';
import LastFmPlaceholder from './LastFmPlaceholder.svelte';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
albums: TopAlbum[];
loading?: boolean;
configured?: boolean;
source?: string;
}
let { albums, loading = false, configured = true, source = '' }: Props = $props();
let requestingIds = new SvelteSet<string>();
let libraryMbids = new SvelteSet<string>();
let requestedMbids = new SvelteSet<string>();
let storeInitialized = $state(false);
onMount(() => {
const unsubscribe = libraryStore.subscribe((state) => {
libraryMbids = new SvelteSet(state.mbidSet);
requestedMbids = new SvelteSet(state.requestedSet);
storeInitialized = state.initialized;
});
return unsubscribe;
});
function isInLibrary(album: TopAlbum): boolean {
const mbid = album.release_group_mbid?.toLowerCase();
if (storeInitialized) return mbid ? libraryMbids.has(mbid) : false;
return album.in_library || (mbid ? libraryMbids.has(mbid) : false);
}
function isRequested(album: TopAlbum): boolean {
if (album.requested) return true;
const mbid = album.release_group_mbid?.toLowerCase();
if (!mbid) return false;
return requestedMbids.has(mbid) && !libraryMbids.has(mbid);
}
function isRequesting(album: TopAlbum): boolean {
return album.release_group_mbid ? requestingIds.has(album.release_group_mbid) : false;
}
async function handleRequest(album: TopAlbum) {
if (!album.release_group_mbid) return;
const id = album.release_group_mbid;
requestingIds.add(id);
try {
await requestAlbum(id, {
artist: album.artist_name ?? undefined,
album: album.title ?? undefined,
year: album.year ?? undefined
});
} finally {
requestingIds.delete(id);
}
}
</script>
<div class="flex flex-col min-w-0">
<h3 class="text-lg font-semibold mb-3">Popular Albums</h3>
{#if loading}
<div class="space-y-2">
{#each Array(10) as _, i (`skeleton-${i}`)}
<div class="flex items-center gap-3 p-2">
<div class="skeleton w-12 h-12 rounded"></div>
<div class="flex-1">
<div class="skeleton h-4 w-3/4 mb-1"></div>
<div class="skeleton h-3 w-1/2"></div>
</div>
</div>
{/each}
</div>
{:else if !configured}
<div class="bg-base-200 rounded-lg p-4 text-center flex-1 flex items-center justify-center">
<div>
<p class="text-base-content/70 text-sm">Connect a music service to see popular albums</p>
<a href="/settings" class="btn btn-primary btn-xs mt-2">Configure</a>
</div>
</div>
{:else if albums.length === 0}
<div class="bg-base-200 rounded-lg p-4 text-center flex-1 flex items-center justify-center">
<p class="text-base-content/70 text-sm">No album data available</p>
</div>
{:else}
<div class="space-y-1">
{#each albums as album, i (album.release_group_mbid || `album-${i}`)}
{#if album.release_group_mbid}
<a
href={albumHref(album.release_group_mbid)}
class="flex items-center gap-3 p-2 rounded-lg hover:bg-base-200 cursor-pointer transition-colors group"
>
<div class="w-12 h-12 shrink-0 relative">
<AlbumImage
mbid={album.release_group_mbid}
alt={album.title}
size="full"
className="w-12 h-12 rounded"
/>
{#if isInLibrary(album)}
<LibraryBadge
status="library"
musicbrainzId={album.release_group_mbid}
albumTitle={album.title}
artistName={album.artist_name || 'Unknown'}
size="sm"
positioning="absolute -bottom-1 -right-1"
/>
{:else if isRequested(album)}
<LibraryBadge
status="requested"
musicbrainzId={album.release_group_mbid}
albumTitle={album.title}
artistName={album.artist_name || 'Unknown'}
size="sm"
positioning="absolute -bottom-1 -right-1"
/>
{/if}
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">{album.title}</p>
<p class="text-xs text-base-content/50 truncate">
{#if album.year}{album.year}{/if}
{#if album.year && album.listen_count}<span class="mx-1"></span>{/if}
{#if album.listen_count}
{album.listen_count.toLocaleString()} plays
{/if}
</p>
</div>
{#if !isInLibrary(album) && !isRequested(album)}
<button
type="button"
class="btn btn-circle btn-sm opacity-0 group-hover:opacity-100 transition-all shrink-0 hover:scale-110 hover:brightness-110"
style="background-color: {colors.accent}; border: none;"
onclick={(e) => {
e.stopPropagation();
e.preventDefault();
handleRequest(album);
}}
disabled={isRequesting(album)}
aria-label="Request album"
>
{#if isRequesting(album)}
<span
class="loading loading-spinner loading-xs"
style="color: {colors.secondary};"
></span>
{:else}
<Download class="h-4 w-4" color={colors.secondary} strokeWidth={2.5} />
{/if}
</button>
{/if}
</a>
{:else}
<div
class="flex items-center gap-3 p-2 rounded-lg transition-colors {source === 'lastfm'
? 'opacity-75'
: ''}"
>
{#if source === 'lastfm'}
<LastFmPlaceholder />
{:else}
<div class="w-12 h-12 shrink-0 bg-base-300 rounded flex items-center justify-center">
<Music2 class="w-6 h-6 opacity-50" />
</div>
{/if}
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">{album.title}</p>
<p class="text-xs text-base-content/50 truncate">
{#if album.listen_count}
{album.listen_count.toLocaleString()} plays
{/if}
</p>
</div>
</div>
{/if}
{/each}
</div>
{/if}
</div>