/** * IndexedDB wrapper for offline audio storage. * Two object stores: * - 'audio-blobs': trackId → Blob (the audio file) * - 'track-meta': trackId → Track metadata (for listing without loading blobs) */ import type { Track } from '@/components/music/music-provider' const DB_NAME = 'soulsync-offline' const DB_VERSION = 1 const AUDIO_STORE = 'audio-blobs' const META_STORE = 'track-meta' function openDB(): Promise { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION) req.onupgradeneeded = () => { const db = req.result if (!db.objectStoreNames.contains(AUDIO_STORE)) { db.createObjectStore(AUDIO_STORE) } if (!db.objectStoreNames.contains(META_STORE)) { db.createObjectStore(META_STORE) } } req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) } function tx( db: IDBDatabase, stores: string | string[], mode: IDBTransactionMode = 'readonly' ): IDBTransaction { return db.transaction(stores, mode) } /** Save an audio blob and track metadata */ export async function saveTrack(trackId: string, blob: Blob, meta: Track): Promise { const db = await openDB() const t = tx(db, [AUDIO_STORE, META_STORE], 'readwrite') t.objectStore(AUDIO_STORE).put(blob, trackId) t.objectStore(META_STORE).put(meta, trackId) return new Promise((resolve, reject) => { t.oncomplete = () => { db.close(); resolve() } t.onerror = () => { db.close(); reject(t.error) } }) } /** Get the audio blob for a track */ export async function getTrackBlob(trackId: string): Promise { const db = await openDB() const t = tx(db, AUDIO_STORE) const req = t.objectStore(AUDIO_STORE).get(trackId) return new Promise((resolve, reject) => { req.onsuccess = () => { db.close(); resolve(req.result) } req.onerror = () => { db.close(); reject(req.error) } }) } /** Check if a track is stored offline */ export async function hasTrack(trackId: string): Promise { const db = await openDB() const t = tx(db, META_STORE) const req = t.objectStore(META_STORE).count(trackId) return new Promise((resolve, reject) => { req.onsuccess = () => { db.close(); resolve(req.result > 0) } req.onerror = () => { db.close(); reject(req.error) } }) } /** Save only the audio blob (no metadata — used for pre-caching) */ export async function saveBlob(trackId: string, blob: Blob): Promise { const db = await openDB() const t = tx(db, AUDIO_STORE, 'readwrite') t.objectStore(AUDIO_STORE).put(blob, trackId) return new Promise((resolve, reject) => { t.oncomplete = () => { db.close(); resolve() } t.onerror = () => { db.close(); reject(t.error) } }) } /** Check if an audio blob exists (regardless of metadata) */ export async function hasBlob(trackId: string): Promise { const db = await openDB() const t = tx(db, AUDIO_STORE) const req = t.objectStore(AUDIO_STORE).count(trackId) return new Promise((resolve, reject) => { req.onsuccess = () => { db.close(); resolve(req.result > 0) } req.onerror = () => { db.close(); reject(req.error) } }) } /** Remove a track from offline storage */ export async function removeTrack(trackId: string): Promise { const db = await openDB() const t = tx(db, [AUDIO_STORE, META_STORE], 'readwrite') t.objectStore(AUDIO_STORE).delete(trackId) t.objectStore(META_STORE).delete(trackId) return new Promise((resolve, reject) => { t.oncomplete = () => { db.close(); resolve() } t.onerror = () => { db.close(); reject(t.error) } }) } /** List all offline tracks (metadata only) */ export async function listOfflineTracks(): Promise { const db = await openDB() const t = tx(db, META_STORE) const req = t.objectStore(META_STORE).getAll() return new Promise((resolve, reject) => { req.onsuccess = () => { db.close(); resolve(req.result) } req.onerror = () => { db.close(); reject(req.error) } }) } /** Get all offline track IDs (for fast Set building) */ export async function listOfflineIds(): Promise { const db = await openDB() const t = tx(db, META_STORE) const req = t.objectStore(META_STORE).getAllKeys() return new Promise((resolve, reject) => { req.onsuccess = () => { db.close(); resolve(req.result as string[]) } req.onerror = () => { db.close(); reject(req.error) } }) } /** Get total storage used (sum of all blob sizes in bytes) */ export async function getTotalSize(): Promise { const db = await openDB() const t = tx(db, AUDIO_STORE) const store = t.objectStore(AUDIO_STORE) const req = store.openCursor() let total = 0 return new Promise((resolve, reject) => { req.onsuccess = () => { const cursor = req.result if (cursor) { const blob = cursor.value as Blob total += blob.size cursor.continue() } else { db.close() resolve(total) } } req.onerror = () => { db.close(); reject(req.error) } }) } /** Remove all offline data */ export async function clearAll(): Promise { const db = await openDB() const t = tx(db, [AUDIO_STORE, META_STORE], 'readwrite') t.objectStore(AUDIO_STORE).clear() t.objectStore(META_STORE).clear() return new Promise((resolve, reject) => { t.oncomplete = () => { db.close(); resolve() } t.onerror = () => { db.close(); reject(t.error) } }) }