mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
1
static/98.css.map
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user