export class MusicDatabase { constructor() { this.dbName = 'MonochromeDB'; this.version = 2; this.db = null; } async open() { if (this.db) return this.db; return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.version); request.onerror = (event) => { console.error("Database error:", event.target.error); reject(event.target.error); }; request.onsuccess = (event) => { this.db = event.target.result; resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Favorites stores if (!db.objectStoreNames.contains('favorites_tracks')) { const store = db.createObjectStore('favorites_tracks', { keyPath: 'id' }); store.createIndex('addedAt', 'addedAt', { unique: false }); } if (!db.objectStoreNames.contains('favorites_albums')) { const store = db.createObjectStore('favorites_albums', { keyPath: 'id' }); store.createIndex('addedAt', 'addedAt', { unique: false }); } if (!db.objectStoreNames.contains('favorites_artists')) { const store = db.createObjectStore('favorites_artists', { keyPath: 'id' }); store.createIndex('addedAt', 'addedAt', { unique: false }); } if (!db.objectStoreNames.contains('favorites_playlists')) { const store = db.createObjectStore('favorites_playlists', { keyPath: 'uuid' }); store.createIndex('addedAt', 'addedAt', { unique: false }); } }; }); } // Generic Helper async performTransaction(storeName, mode, callback) { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, mode); const store = transaction.objectStore(storeName); const request = callback(store); transaction.oncomplete = () => { resolve(request?.result); }; transaction.onerror = (event) => { reject(event.target.error); }; }); } // Favorites API async toggleFavorite(type, item) { const storeName = `favorites_${type}s`; // tracks, albums, artists const key = type === 'playlist' ? item.uuid : item.id; const exists = await this.isFavorite(type, key); if (exists) { await this.performTransaction(storeName, 'readwrite', (store) => store.delete(key)); return false; // Removed } else { const entry = { ...item, addedAt: Date.now() }; await this.performTransaction(storeName, 'readwrite', (store) => store.put(entry)); return true; // Added } } async isFavorite(type, id) { const storeName = `favorites_${type}s`; try { const result = await this.performTransaction(storeName, 'readonly', (store) => store.get(id)); return !!result; } catch (e) { return false; } } async getFavorites(type) { const storeName = `favorites_${type}s`; const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const index = store.index('addedAt'); const request = index.getAll(); // Returns sorted by addedAt ascending request.onsuccess = () => { // Reverse to show newest first resolve(request.result.reverse()); }; request.onerror = () => reject(request.error); }); } _minifyItem(type, item) { if (!item) return item; // Base properties to keep const base = { id: item.id, addedAt: item.addedAt }; if (type === 'track') { return { ...base, title: item.title, duration: item.duration, explicit: item.explicit, // Keep minimal artist info artists: item.artists?.map(a => ({ id: a.id, name: a.name })) || [], // Keep minimal album info album: item.album ? { id: item.album.id, cover: item.album.cover, releaseDate: item.album.releaseDate } : null, // Fallback date streamStartDate: item.streamStartDate, // Keep version if exists version: item.version }; } if (type === 'album') { return { ...base, title: item.title, cover: item.cover, releaseDate: item.releaseDate, explicit: item.explicit, // UI uses singular 'artist' artist: item.artist ? { name: item.artist.name, id: item.artist.id } : (item.artists?.[0] ? { name: item.artists[0].name, id: item.artists[0].id } : null), // Keep type and track count for UI labels type: item.type, numberOfTracks: item.numberOfTracks }; } if (type === 'artist') { return { ...base, name: item.name, picture: item.picture || item.image // Handle both just in case }; } if (type === 'playlist') { return { uuid: item.uuid, addedAt: item.addedAt, title: item.title, // UI checks squareImage || image || uuid image: item.image || item.squareImage, numberOfTracks: item.numberOfTracks, user: item.user ? { name: item.user.name } : null }; } return item; } async exportData() { const tracks = await this.getFavorites('track'); const albums = await this.getFavorites('album'); const artists = await this.getFavorites('artist'); const playlists = await this.getFavorites('playlist'); const data = { favorites_tracks: tracks.map(t => this._minifyItem('track', t)), favorites_albums: albums.map(a => this._minifyItem('album', a)), favorites_artists: artists.map(a => this._minifyItem('artist', a)), favorites_playlists: playlists.map(p => this._minifyItem('playlist', p)) }; return data; } async importData(data) { // Clear existing? Or merge? Prompt says "Sync" or "Export/Import". // Let's merge by put (replaces if ID exists). const db = await this.open(); const importStore = async (storeName, items) => { if (!items || !Array.isArray(items)) return; const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); for (const item of items) { store.put(item); } }; await importStore('favorites_tracks', data.favorites_tracks); await importStore('favorites_albums', data.favorites_albums); await importStore('favorites_artists', data.favorites_artists); await importStore('favorites_playlists', data.favorites_playlists); } } export const db = new MusicDatabase();