diff --git a/src/lib/components/DeezerCollectionView.svelte b/src/lib/components/DeezerCollectionView.svelte index 9b2ec15..b953e2f 100644 --- a/src/lib/components/DeezerCollectionView.svelte +++ b/src/lib/components/DeezerCollectionView.svelte @@ -13,6 +13,9 @@ onTrackClick?: (index: number) => void; onDownloadTrack?: (index: number) => void; onDownloadPlaylist?: () => void; + onRefresh?: () => void; + refreshing?: boolean; + lastCached?: number | null; downloadingTrackIds?: Set; } @@ -27,9 +30,18 @@ onTrackClick, onDownloadTrack, onDownloadPlaylist, + onRefresh, + refreshing = false, + lastCached = null, downloadingTrackIds = new Set() }: Props = $props(); + function formatTimestamp(timestamp: number | null): string { + if (!timestamp) return 'Never'; + const date = new Date(timestamp * 1000); + return date.toLocaleString(); + } + type ViewMode = 'tracks' | 'info'; let viewMode = $state('tracks'); @@ -177,14 +189,27 @@ Tracks: {tracks.length} + {#if lastCached} +
+ Last updated: + {formatTimestamp(lastCached)} +
+ {/if}
Actions - -

Download all tracks and save as m3u8 playlist

+
+
+ +

Download all tracks and save as m3u8 playlist

+
+ {#if onRefresh} + + {/if} +
{/if} @@ -300,6 +325,13 @@ text-align: center; } + .actions-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + .download-btn { padding: 2px 8px; font-size: 11px; diff --git a/src/lib/components/SpotifyCollectionView.svelte b/src/lib/components/SpotifyCollectionView.svelte index f9b59f9..de67f65 100644 --- a/src/lib/components/SpotifyCollectionView.svelte +++ b/src/lib/components/SpotifyCollectionView.svelte @@ -13,6 +13,9 @@ onTrackClick?: (index: number) => void; onDownloadTrack?: (index: number) => void; onDownloadPlaylist?: () => void; + onRefresh?: () => void; + refreshing?: boolean; + lastCached?: number | null; downloadingTrackIds?: Set; } @@ -26,9 +29,18 @@ onTrackClick, onDownloadTrack, onDownloadPlaylist, + onRefresh, + refreshing = false, + lastCached = null, downloadingTrackIds = new Set() }: Props = $props(); + function formatTimestamp(timestamp: number | null): string { + if (!timestamp) return 'Never'; + const date = new Date(timestamp * 1000); + return date.toLocaleString(); + } + type ViewMode = 'tracks' | 'info'; let viewMode = $state('tracks'); @@ -164,20 +176,40 @@ Tracks: {tracks.length} + {#if lastCached} +
+ Last updated: + {formatTimestamp(lastCached)} +
+ {/if} {#if $deezerAuth.loggedIn}
Actions - -

Download all tracks via Deezer and save as m3u8 playlist

+
+
+ +

Download all tracks via Deezer and save as m3u8 playlist

+
+ {#if onRefresh} + + {/if} +
{:else}
Downloads -

Deezer login required to download Spotify tracks

+
+

Deezer login required to download Spotify tracks

+ {#if onRefresh} + + {/if} +

Sign in to Deezer in Services → Deezer to enable downloads

{/if} @@ -306,6 +338,13 @@ color: #808080; } + .actions-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + .warning-text { margin: 0 0 8px 0; font-size: 12px; diff --git a/src/routes/services/deezer/playlists/[id]/+page.svelte b/src/routes/services/deezer/playlists/[id]/+page.svelte index 7e851ef..d60172d 100644 --- a/src/routes/services/deezer/playlists/[id]/+page.svelte +++ b/src/routes/services/deezer/playlists/[id]/+page.svelte @@ -352,6 +352,9 @@ onTrackClick={handleTrackClick} onDownloadTrack={handleDownloadTrack} onDownloadPlaylist={handleDownloadPlaylist} + onRefresh={refreshPlaylistTracks} + {refreshing} + {lastCached} {downloadingTrackIds} /> {/if} diff --git a/src/routes/services/spotify/playlists/[id]/+page.svelte b/src/routes/services/spotify/playlists/[id]/+page.svelte index 7d74b1d..ff3e139 100644 --- a/src/routes/services/spotify/playlists/[id]/+page.svelte +++ b/src/routes/services/spotify/playlists/[id]/+page.svelte @@ -8,6 +8,7 @@ getCachedPlaylistTracks, getCachedTracks, upsertPlaylistTracks, + upsertTracks, type SpotifyPlaylist, type SpotifyPlaylistTrack, type SpotifyTrack @@ -22,12 +23,14 @@ let playlistId = $derived($page.params.id!); let loading = $state(true); + let refreshing = $state(false); let error = $state(null); let playlist = $state(null); let playlistTracks = $state([]); let selectedTrackIndex = $state(null); let coverImageUrl = $state(undefined); let downloadingTrackIds = $state(new Set()); + let lastCached = $state(null); // Convert Spotify tracks to Track type for CollectionView let tracks = $derived( @@ -96,6 +99,8 @@ cached_at: track.cached_at })); + lastCached = allTracks[0]?.cached_at || null; + // Set cover art from first track's album if (allTracks.length > 0) { if (allTracks[0].album_image_url) { @@ -171,6 +176,7 @@ } else { playlist = cachedPlaylist; coverImageUrl = playlist.image_url; + lastCached = playlist.cached_at; } // Load tracks @@ -204,6 +210,56 @@ } } + function ensureSpotifyAuth(): boolean { + if (!$spotifyAuth.accessToken || !$spotifyAuth.clientId || !$spotifyAuth.clientSecret || !$spotifyAuth.refreshToken) { + return false; + } + spotifyAPI.setClientCredentials($spotifyAuth.clientId, $spotifyAuth.clientSecret); + spotifyAPI.setTokens($spotifyAuth.accessToken, $spotifyAuth.refreshToken, $spotifyAuth.expiresAt!); + return true; + } + + async function refreshPlaylist() { + if (refreshing) return; + if (!ensureSpotifyAuth()) { + setError('Not logged in to Spotify'); + return; + } + + refreshing = true; + + try { + if (playlistId === 'spotify-likes') { + // Refresh liked tracks + const apiTracks = await spotifyAPI.getAllUserTracks(); + await upsertTracks(apiTracks); + + // Reload from cache + await loadSpotifyLikes(); + } else { + // Refresh regular playlist tracks + const apiTracks = await spotifyAPI.getPlaylistTracks(playlistId); + await upsertPlaylistTracks(playlistId, apiTracks); + + // Reload from cache + const cachedTracks = await getCachedPlaylistTracks(playlistId); + playlistTracks = cachedTracks; + + if (playlist) { + playlist.track_count = cachedTracks.length; + } + } + + lastCached = Math.floor(Date.now() / 1000); + console.log('[Spotify Playlist] Refresh complete!'); + } catch (e) { + console.error('Error refreshing playlist:', e); + setError('Error refreshing playlist: ' + (e instanceof Error ? e.message : String(e))); + } finally { + refreshing = false; + } + } + function handleTrackClick(index: number) { selectedTrackIndex = index; } @@ -294,6 +350,9 @@ onTrackClick={handleTrackClick} onDownloadTrack={handleDownloadTrack} onDownloadPlaylist={handleDownloadPlaylist} + onRefresh={refreshPlaylist} + {refreshing} + {lastCached} /> {/if}