feat(playlist): use first album cover art for playlists

Add logic to find and use album cover art as a fallback when playlist
cover art is missing, both for local and online playlists. Update
database schema and upsert logic to store album picture URLs for
online tracks. Improve UI to display fallback cover art when needed.
This commit is contained in:
2025-10-02 22:20:06 -04:00
parent 8e8afb0f66
commit 0bc0e70274
6 changed files with 96 additions and 3 deletions

View File

@@ -121,6 +121,7 @@ pub fn run() {
title TEXT NOT NULL,
artist_name TEXT NOT NULL,
album_title TEXT,
album_picture TEXT,
duration INTEGER DEFAULT 0,
track_number INTEGER,
cached_at INTEGER NOT NULL,

View File

@@ -36,6 +36,7 @@ export interface DeezerTrack {
title: string;
artist_name: string;
album_title: string;
album_picture?: string | null;
duration: number;
cached_at: number;
}
@@ -47,6 +48,7 @@ export interface DeezerPlaylistTrack {
title: string;
artist_name: string;
album_title: string;
album_picture: string | null;
duration: number;
track_number: number | null;
cached_at: number;
@@ -292,15 +294,21 @@ export async function upsertPlaylistTracks(playlistId: string, tracks: any[]): P
// Insert new tracks
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
// Convert ALB_PICTURE hash to full URL
const albumPictureUrl = track.ALB_PICTURE
? `https://e-cdns-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/500x500-000000-80-0-0.jpg`
: null;
await database.execute(
`INSERT INTO deezer_playlist_tracks (playlist_id, track_id, title, artist_name, album_title, duration, track_number, cached_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
`INSERT INTO deezer_playlist_tracks (playlist_id, track_id, title, artist_name, album_title, album_picture, duration, track_number, cached_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
playlistId,
String(track.SNG_ID),
track.SNG_TITLE || '',
track.ART_NAME || 'Unknown',
track.ALB_TITLE || '',
albumPictureUrl,
track.DURATION || 0,
track.TRACK_NUMBER || i + 1,
now

View File

@@ -1,6 +1,7 @@
import { readTextFile, exists, readDir } from '@tauri-apps/plugin-fs';
import { invoke } from '@tauri-apps/api/core';
import type { Track, AudioFormat, PlaylistWithTracks, TrackMetadata } from '$lib/types/track';
import { findAlbumArt } from './album';
/**
* Get audio format from file extension
@@ -210,6 +211,40 @@ export async function findPlaylistArt(playlistPath: string): Promise<string | un
}
}
/**
* Find cover art fallback by looking at album folders of tracks in the playlist
* Iterates through tracks to find the first one with album metadata and a cover image
*/
export async function findPlaylistCoverFallback(
tracks: Track[],
musicFolder: string
): Promise<string | undefined> {
for (const track of tracks) {
const album = track.metadata.album;
const albumArtist = track.metadata.albumArtist || track.metadata.artist;
if (!album || !albumArtist) {
continue; // Skip tracks without album metadata
}
// Construct album folder path following the same structure as downloader
const albumPath = `${musicFolder}/${albumArtist}/${album}`;
try {
// Check if album folder exists and has cover art
const coverArtPath = await findAlbumArt(albumPath);
if (coverArtPath) {
return coverArtPath;
}
} catch (error) {
// Album folder doesn't exist or can't be read, continue to next track
continue;
}
}
return undefined;
}
/**
* Load playlist with track information
*/

View File

@@ -3,7 +3,7 @@
import { page } from '$app/stores';
import { settings, loadSettings } from '$lib/stores/settings';
import { scanPlaylists } from '$lib/library/scanner';
import { loadPlaylistTracks, findPlaylistArt } from '$lib/library/playlist';
import { loadPlaylistTracks, findPlaylistArt, findPlaylistCoverFallback } from '$lib/library/playlist';
import CollectionView from '$lib/components/CollectionView.svelte';
import type { Track, PlaylistWithTracks } from '$lib/types/track';
@@ -58,6 +58,11 @@
playlistData = tracksData;
coverArtPath = coverPath;
// If no cover art found, try fallback from first track's album
if (!coverArtPath && playlistData.tracks.length > 0) {
coverArtPath = await findPlaylistCoverFallback(playlistData.tracks, $settings.musicFolder);
}
loading = false;
} catch (e) {
error = 'Error loading playlist: ' + (e as Error).message;

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { convertFileSrc } from '@tauri-apps/api/core';
import { deezerAuth } from '$lib/stores/deezer';
import { deezerAPI } from '$lib/services/deezer';
import { settings } from '$lib/stores/settings';
@@ -14,6 +15,7 @@
import { TrackExistenceCache } from '$lib/library/trackMatcher';
import { addDeezerTrackToQueue } from '$lib/services/deezer/addToQueue';
import { downloadDeezerPlaylist } from '$lib/services/deezer/playlistDownloader';
import { findPlaylistCoverFallback } from '$lib/library/playlist';
import DeezerCollectionView from '$lib/components/DeezerCollectionView.svelte';
import type { Track } from '$lib/types/track';
import type { DeezerTrack } from '$lib/types/deezer';
@@ -85,6 +87,47 @@
// Check track existence after loading tracks
await checkTrackExistence();
// If no valid cover art from Deezer playlist, fetch and use first track's album cover
if ((!playlistPicture || !playlistPicture.startsWith('http')) && tracks.length > 0) {
const cachedTracks = isFavoriteTracks
? await getCachedTracks()
: await getCachedPlaylistTracks(playlistId);
if (cachedTracks.length > 0) {
// If we have cached album picture, use it
if (cachedTracks[0].album_picture) {
playlistPicture = cachedTracks[0].album_picture;
} else if ($deezerAuth.arl && cachedTracks[0].track_id) {
// Fetch album data from API to get cover
try {
deezerAPI.setArl($deezerAuth.arl);
const trackData = await deezerAPI.getTrackData(cachedTracks[0].track_id);
if (trackData && trackData.ALB_PICTURE) {
const albumCoverUrl = `https://e-cdns-images.dzcdn.net/images/cover/${trackData.ALB_PICTURE}/500x500-000000-80-0-0.jpg`;
playlistPicture = albumCoverUrl;
// Update cache with the album picture
const database = await import('$lib/library/deezer-database').then(m => m.initDeezerDatabase());
await database.execute(
'UPDATE deezer_playlist_tracks SET album_picture = $1 WHERE track_id = $2',
[albumCoverUrl, cachedTracks[0].track_id]
);
}
} catch (err) {
console.error('Failed to fetch album cover:', err);
}
}
}
// Final fallback to local files
if ((!playlistPicture || !playlistPicture.startsWith('http')) && $settings.musicFolder) {
const fallbackPath = await findPlaylistCoverFallback(tracks, $settings.musicFolder);
if (fallbackPath) {
playlistPicture = convertFileSrc(fallbackPath);
}
}
}
} catch (e) {
error = 'Error loading playlist: ' + (e instanceof Error ? e.message : String(e));
} finally {

1
static/98.css.map Normal file

File diff suppressed because one or more lines are too long