df779c9e6d
* 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
198 lines
6.1 KiB
Svelte
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>
|