142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
/**
|
|
* 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<IDBDatabase> {
|
|
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<void> {
|
|
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<Blob | undefined> {
|
|
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<boolean> {
|
|
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) }
|
|
})
|
|
}
|
|
|
|
/** Remove a track from offline storage */
|
|
export async function removeTrack(trackId: string): Promise<void> {
|
|
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<Track[]> {
|
|
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<string[]> {
|
|
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<number> {
|
|
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<void> {
|
|
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) }
|
|
})
|
|
}
|