diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index de76242..6b21b8a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,7 +26,7 @@ fn tag_audio_file( #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let migrations = vec![ + let library_migrations = vec![ Migration { version: 1, description: "create_library_tables", @@ -65,10 +65,64 @@ pub fn run() { } ]; + let deezer_migrations = vec![ + Migration { + version: 1, + description: "create_deezer_cache_tables", + sql: " + CREATE TABLE IF NOT EXISTS deezer_playlists ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + nb_tracks INTEGER DEFAULT 0, + creator_name TEXT, + picture_small TEXT, + picture_medium TEXT, + cached_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS deezer_albums ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist_name TEXT NOT NULL, + nb_tracks INTEGER DEFAULT 0, + release_date TEXT, + picture_small TEXT, + picture_medium TEXT, + cached_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS deezer_artists ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + nb_album INTEGER DEFAULT 0, + picture_small TEXT, + picture_medium TEXT, + cached_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS deezer_tracks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist_name TEXT NOT NULL, + album_title TEXT, + duration INTEGER DEFAULT 0, + cached_at INTEGER NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_deezer_playlists_title ON deezer_playlists(title); + CREATE INDEX IF NOT EXISTS idx_deezer_albums_artist ON deezer_albums(artist_name); + CREATE INDEX IF NOT EXISTS idx_deezer_artists_name ON deezer_artists(name); + CREATE INDEX IF NOT EXISTS idx_deezer_tracks_title ON deezer_tracks(title); + ", + kind: MigrationKind::Up, + } + ]; + tauri::Builder::default() .plugin( tauri_plugin_sql::Builder::new() - .add_migrations("sqlite:library.db", migrations) + .add_migrations("sqlite:library.db", library_migrations) + .add_migrations("sqlite:deezer.db", deezer_migrations) .build() ) .plugin(tauri_plugin_http::init()) diff --git a/src/lib/library/deezer-database.ts b/src/lib/library/deezer-database.ts new file mode 100644 index 0000000..b478d69 --- /dev/null +++ b/src/lib/library/deezer-database.ts @@ -0,0 +1,242 @@ +import Database from '@tauri-apps/plugin-sql'; + +export interface DeezerPlaylist { + id: string; + title: string; + nb_tracks: number; + creator_name: string; + picture_small?: string; + picture_medium?: string; + cached_at: number; +} + +export interface DeezerAlbum { + id: string; + title: string; + artist_name: string; + nb_tracks: number; + release_date?: string; + picture_small?: string; + picture_medium?: string; + cached_at: number; +} + +export interface DeezerArtist { + id: string; + name: string; + nb_album: number; + picture_small?: string; + picture_medium?: string; + cached_at: number; +} + +export interface DeezerTrack { + id: string; + title: string; + artist_name: string; + album_title: string; + duration: number; + cached_at: number; +} + +let db: Database | null = null; + +/** + * Initialize database connection + */ +export async function initDeezerDatabase(): Promise { + if (!db) { + db = await Database.load('sqlite:deezer.db'); + } + return db; +} + +/** + * Get cached playlists + */ +export async function getCachedPlaylists(): Promise { + const database = await initDeezerDatabase(); + const playlists = await database.select( + 'SELECT * FROM deezer_playlists ORDER BY title COLLATE NOCASE' + ); + return playlists || []; +} + +/** + * Get cached albums + */ +export async function getCachedAlbums(): Promise { + const database = await initDeezerDatabase(); + const albums = await database.select( + 'SELECT * FROM deezer_albums ORDER BY artist_name COLLATE NOCASE, title COLLATE NOCASE' + ); + return albums || []; +} + +/** + * Get cached artists + */ +export async function getCachedArtists(): Promise { + const database = await initDeezerDatabase(); + const artists = await database.select( + 'SELECT * FROM deezer_artists ORDER BY name COLLATE NOCASE' + ); + return artists || []; +} + +/** + * Get cached tracks + */ +export async function getCachedTracks(): Promise { + const database = await initDeezerDatabase(); + const tracks = await database.select( + 'SELECT * FROM deezer_tracks ORDER BY title COLLATE NOCASE' + ); + return tracks || []; +} + +/** + * Upsert playlists + */ +export async function upsertPlaylists(playlists: any[]): Promise { + try { + console.log('[deezer-database] Upserting playlists, count:', playlists.length); + if (playlists.length > 0) { + console.log('[deezer-database] First playlist sample:', playlists[0]); + } + + const database = await initDeezerDatabase(); + const now = Math.floor(Date.now() / 1000); + + // Clear existing playlists + await database.execute('DELETE FROM deezer_playlists'); + console.log('[deezer-database] Cleared existing playlists'); + + // Insert new playlists + for (const playlist of playlists) { + await database.execute( + `INSERT INTO deezer_playlists (id, title, nb_tracks, creator_name, picture_small, picture_medium, cached_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + String(playlist.PLAYLIST_ID), + playlist.TITLE || '', + playlist.NB_SONG || 0, + playlist.PARENT_USERNAME || 'Unknown', + playlist.PLAYLIST_PICTURE || null, + playlist.PICTURE_TYPE || null, + now + ] + ); + } + console.log('[deezer-database] Inserted', playlists.length, 'playlists'); + } catch (err) { + console.error('[deezer-database] Error in upsertPlaylists:', err); + throw err; + } +} + +/** + * Upsert albums + */ +export async function upsertAlbums(albums: any[]): Promise { + const database = await initDeezerDatabase(); + const now = Math.floor(Date.now() / 1000); + + // Clear existing albums + await database.execute('DELETE FROM deezer_albums'); + + // Insert new albums + for (const album of albums) { + await database.execute( + `INSERT INTO deezer_albums (id, title, artist_name, nb_tracks, release_date, picture_small, picture_medium, cached_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, + [ + String(album.ALB_ID), + album.ALB_TITLE || '', + album.ART_NAME || 'Unknown', + album.NB_SONG || 0, + album.PHYSICAL_RELEASE_DATE || null, + album.ALB_PICTURE || null, + album.PICTURE_TYPE || null, + now + ] + ); + } +} + +/** + * Upsert artists + */ +export async function upsertArtists(artists: any[]): Promise { + const database = await initDeezerDatabase(); + const now = Math.floor(Date.now() / 1000); + + // Clear existing artists + await database.execute('DELETE FROM deezer_artists'); + + // Insert new artists + for (const artist of artists) { + await database.execute( + `INSERT INTO deezer_artists (id, name, nb_album, picture_small, picture_medium, cached_at) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + String(artist.ART_ID), + artist.ART_NAME || '', + artist.NB_ALBUM || 0, + artist.ART_PICTURE || null, + artist.PICTURE_TYPE || null, + now + ] + ); + } +} + +/** + * Upsert tracks + */ +export async function upsertTracks(tracks: any[]): Promise { + const database = await initDeezerDatabase(); + const now = Math.floor(Date.now() / 1000); + + // Clear existing tracks + await database.execute('DELETE FROM deezer_tracks'); + + // Insert new tracks + for (const track of tracks) { + await database.execute( + `INSERT INTO deezer_tracks (id, title, artist_name, album_title, duration, cached_at) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + String(track.SNG_ID), + track.SNG_TITLE || '', + track.ART_NAME || 'Unknown', + track.ALB_TITLE || '', + track.DURATION || 0, + now + ] + ); + } +} + +/** + * Get cache timestamp + */ +export async function getCacheTimestamp(): Promise { + const database = await initDeezerDatabase(); + const result = await database.select<{ cached_at: number }[]>( + 'SELECT cached_at FROM deezer_playlists LIMIT 1' + ); + return result[0]?.cached_at || null; +} + +/** + * Clear all Deezer cache + */ +export async function clearDeezerCache(): Promise { + const database = await initDeezerDatabase(); + await database.execute('DELETE FROM deezer_playlists'); + await database.execute('DELETE FROM deezer_albums'); + await database.execute('DELETE FROM deezer_artists'); + await database.execute('DELETE FROM deezer_tracks'); + await database.execute('VACUUM'); +} diff --git a/src/lib/services/deezer.ts b/src/lib/services/deezer.ts index f0b7e32..d02be4a 100644 --- a/src/lib/services/deezer.ts +++ b/src/lib/services/deezer.ts @@ -307,7 +307,7 @@ export class DeezerAPI { const response = await this.apiCall('deezer.pageProfile', { USER_ID: userId, tab: 'playlists', - nb: 100 + nb: -1 }); return response.TAB?.playlists?.data || []; @@ -317,6 +317,86 @@ export class DeezerAPI { } } + // Get user albums + async getUserAlbums(): Promise { + try { + const userData = await this.getUserData(); + const userId = userData.USER.USER_ID; + + const response = await this.apiCall('deezer.pageProfile', { + USER_ID: userId, + tab: 'albums', + nb: -1 + }); + + return response.TAB?.albums?.data || []; + } catch (error) { + console.error('Error fetching albums:', error); + return []; + } + } + + // Get user artists + async getUserArtists(): Promise { + try { + const userData = await this.getUserData(); + const userId = userData.USER.USER_ID; + + const response = await this.apiCall('deezer.pageProfile', { + USER_ID: userId, + tab: 'artists', + nb: -1 + }); + + return response.TAB?.artists?.data || []; + } catch (error) { + console.error('Error fetching artists:', error); + return []; + } + } + + // Get user favorite tracks (uses the more reliable song.getFavoriteIds method) + async getUserTracks(): Promise { + try { + // Get favorite track IDs + const idsResponse = await this.apiCall('song.getFavoriteIds', { + nb: -1, + start: 0, + checksum: null + }); + + const trackIds = idsResponse.data?.map((x: any) => x.SNG_ID) || []; + + if (trackIds.length === 0) { + console.log('[Deezer] No favorite tracks found'); + return []; + } + + console.log(`[Deezer] Found ${trackIds.length} favorite track IDs, fetching details...`); + + // Fetch track details in batches (Deezer API might have limits) + const batchSize = 100; + const tracks: any[] = []; + + for (let i = 0; i < trackIds.length; i += batchSize) { + const batchIds = trackIds.slice(i, i + batchSize); + const batchResponse = await this.apiCall('song.getListData', { + SNG_IDS: batchIds + }); + + if (batchResponse.data) { + tracks.push(...batchResponse.data); + } + } + + console.log(`[Deezer] Fetched ${tracks.length} track details`); + return tracks; + } catch (error) { + console.error('Error fetching favorite tracks:', error); + return []; + } + } + // Get album data async getAlbumData(albumId: string): Promise { return this.apiCall('album.getData', { alb_id: albumId }); diff --git a/src/lib/stores/deezer.ts b/src/lib/stores/deezer.ts index 358895d..c95ab79 100644 --- a/src/lib/stores/deezer.ts +++ b/src/lib/stores/deezer.ts @@ -18,6 +18,7 @@ export interface DeezerAuthState { arl: string | null; user: DeezerUser | null; loggedIn: boolean; + cacheTimestamp: number | null; } // Initialize the store with deezer.json @@ -27,7 +28,8 @@ const store = new LazyStore('deezer.json'); const defaultState: DeezerAuthState = { arl: null, user: null, - loggedIn: false + loggedIn: false, + cacheTimestamp: null }; // Create a writable store for reactive UI updates @@ -37,11 +39,13 @@ export const deezerAuth: Writable = writable(defaultState); export async function loadDeezerAuth(): Promise { const arl = await store.get('arl'); const user = await store.get('user'); + const cacheTimestamp = await store.get('cacheTimestamp'); deezerAuth.set({ arl: arl ?? null, user: user ?? null, - loggedIn: !!(arl && user) + loggedIn: !!(arl && user), + cacheTimestamp: cacheTimestamp ?? null }); } @@ -82,5 +86,16 @@ export async function getArl(): Promise { return (await store.get('arl')) ?? null; } +// Save cache timestamp +export async function saveCacheTimestamp(timestamp: number): Promise { + await store.set('cacheTimestamp', timestamp); + await store.save(); + + deezerAuth.update(s => ({ + ...s, + cacheTimestamp: timestamp + })); +} + // Initialize on module load loadDeezerAuth(); diff --git a/src/routes/services/deezer/+page.svelte b/src/routes/services/deezer/+page.svelte index b6c5f39..f128506 100644 --- a/src/routes/services/deezer/+page.svelte +++ b/src/routes/services/deezer/+page.svelte @@ -1,44 +1,167 @@ -
-

Deezer Authentication

+
+

Deezer

{#if !$deezerAuth.loggedIn} -
- {#if errorMessage} + {#if loginError}
- ⚠ {errorMessage} + ⚠ {loginError}
{/if} - {#if successMessage} + {#if loginSuccess}
- ✓ {successMessage} + ✓ {loginSuccess}
{/if}
-
-
How to get your ARL token
@@ -208,121 +292,233 @@
+ {:else if loading} +

Loading favorites...

{:else} - -
-
-
User Info
-
-
-