feat(frontend): show YouTube tracks in recently played with their thumbnails

This commit is contained in:
2026-07-16 01:15:50 +02:00
parent c5b0ee3e48
commit 4069b36202
3 changed files with 33 additions and 8 deletions
+5 -1
View File
@@ -45,6 +45,10 @@ export function queueIdForTrack(track: ApiTrackLike): string | number {
return isPendingYouTubeTrack(track) && track.externalId ? track.externalId : track.id; return isPendingYouTubeTrack(track) && track.externalId ? track.externalId : track.id;
} }
export function youTubeThumbnailUrl(videoId: string): string {
return `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
}
export function toQueueItems(tracks: ApiTrackLike[]): QueueItem[] { export function toQueueItems(tracks: ApiTrackLike[]): QueueItem[] {
return tracks.map((track) => { return tracks.map((track) => {
const pendingYouTube = isPendingYouTubeTrack(track); const pendingYouTube = isPendingYouTubeTrack(track);
@@ -53,7 +57,7 @@ export function toQueueItems(tracks: ApiTrackLike[]): QueueItem[] {
title: track.title, title: track.title,
artist: track.artist, artist: track.artist,
source: pendingYouTube ? 'youtube' : 'local', source: pendingYouTube ? 'youtube' : 'local',
coverUrl: pendingYouTube ? `/api/tracks/${track.id}/cover?size=small` : undefined coverUrl: pendingYouTube && track.externalId ? youTubeThumbnailUrl(track.externalId) : undefined
}; };
}); });
} }
@@ -16,7 +16,8 @@
player, player,
isPendingYouTubeTrack, isPendingYouTubeTrack,
queueIdForTrack, queueIdForTrack,
toQueueItems toQueueItems,
youTubeThumbnailUrl
} from '$lib/player/player.svelte'; } from '$lib/player/player.svelte';
import { HUES, fmtTime } from '$lib/theme/color'; import { HUES, fmtTime } from '$lib/theme/color';
import Cover from '$lib/components/ui/Cover.svelte'; import Cover from '$lib/components/ui/Cover.svelte';
@@ -179,6 +180,7 @@
<button type="button" onclick={() => playFrom(i)} class="flex min-w-0 items-center gap-3.5 text-left"> <button type="button" onclick={() => playFrom(i)} class="flex min-w-0 items-center gap-3.5 text-left">
<Cover <Cover
trackId={track.id} trackId={track.id}
src={isPendingYouTubeTrack(track) ? youTubeThumbnailUrl(track.externalId) : undefined}
hue={hueFor(track.id)} hue={hueFor(track.id)}
size="small" size="small"
alt={track.title} alt={track.title}
+25 -6
View File
@@ -11,19 +11,34 @@
const latest = $derived(data.latest); const latest = $derived(data.latest);
const playlists = $derived(data.playlists); const playlists = $derived(data.playlists);
const trackIds = $derived(data.trackIds); const trackIds = $derived(data.trackIds);
interface RecentItem {
id: string;
title: string;
artist?: string;
source: 'local' | 'youtube';
coverUrl?: string;
}
const recentlyPlayed = $derived.by(() => { const recentlyPlayed = $derived.by(() => {
const seen = new Set<string>(); const seen = new Set<string>();
const merged: { id: string; title: string }[] = []; const merged: RecentItem[] = [];
for (const t of player.recentlyPlayed) { for (const t of player.recentlyPlayed) {
const id = String(t.id); const id = String(t.id);
if (seen.has(id)) continue; if (seen.has(id)) continue;
seen.add(id); seen.add(id);
merged.push({ id, title: t.title }); merged.push({ id, title: t.title, artist: t.artist, source: t.source, coverUrl: t.coverUrl });
} }
for (const t of data.recentlyPlayed) { for (const item of toQueueItems(data.recentlyPlayed)) {
if (seen.has(t.id)) continue; const id = String(item.id);
seen.add(t.id); if (seen.has(id)) continue;
merged.push({ id: t.id, title: t.title }); seen.add(id);
merged.push({
id,
title: item.title,
artist: item.artist,
source: item.source ?? 'local',
coverUrl: item.coverUrl
});
} }
return merged.slice(0, 15); return merged.slice(0, 15);
}); });
@@ -98,6 +113,7 @@
> >
<Cover <Cover
trackId={track.id} trackId={track.id}
src={track.coverUrl}
hue={hueFor(track.id)} hue={hueFor(track.id)}
size="small" size="small"
alt={track.title} alt={track.title}
@@ -109,6 +125,9 @@
</Cover> </Cover>
<div class="min-w-0"> <div class="min-w-0">
<div class="truncate text-sm font-semibold text-[var(--mf-text)]">{track.title}</div> <div class="truncate text-sm font-semibold text-[var(--mf-text)]">{track.title}</div>
{#if track.artist}
<div class="truncate text-xs text-neutral-500">{track.artist}</div>
{/if}
</div> </div>
</button> </button>
{/each} {/each}