jefflix-website/lib/offline-db.ts

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) }
})
}