a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move song queue to IndexedDB

intergrav 3c02afe2 70b850bd

+100 -44
+1
src/index.html
··· 279 279 <script src="js/api.js"></script> 280 280 <script src="js/state.js"></script> 281 281 <script src="js/image-cache.js"></script> 282 + <script src="js/queue-storage.js"></script> 282 283 <script src="js/virtual-scroll.js"></script> 283 284 <script src="js/queue.js"></script> 284 285 <script src="js/library.js"></script>
+1 -1
src/js/events.js
··· 199 199 credentials.username, 200 200 credentials.password, 201 201 ); 202 - loadQueue(); 202 + await loadQueue().catch(() => {}); 203 203 updateQueueDisplay(); 204 204 await initializeApp(); 205 205 // restore song display and pause playback on startup
+1
src/js/image-cache.js
··· 1 1 // image cache that persists to IndexedDB across sessions 2 + 2 3 class ImageCache { 3 4 constructor() { 4 5 this.cache = new Map();
+83
src/js/queue-storage.js
··· 1 + // queue storage that persists to IndexedDB across sessions 2 + 3 + class QueueStorage { 4 + constructor() { 5 + this.db = null; 6 + } 7 + 8 + async ensureDB() { 9 + if (this.db) return this.db; 10 + 11 + return new Promise((resolve) => { 12 + const request = indexedDB.open("tinysub", 1); 13 + 14 + request.onsuccess = () => { 15 + this.db = request.result; 16 + resolve(this.db); 17 + }; 18 + 19 + request.onupgradeneeded = (e) => { 20 + const db = e.target.result; 21 + if (!db.objectStoreNames.contains("queue")) { 22 + db.createObjectStore("queue"); 23 + } 24 + }; 25 + 26 + request.onerror = () => resolve(null); 27 + }); 28 + } 29 + 30 + async save(songs, queueIndex) { 31 + const db = await this.ensureDB(); 32 + if (!db) throw new Error("IndexedDB unavailable"); 33 + 34 + return new Promise((resolve, reject) => { 35 + const tx = db.transaction(["queue"], "readwrite"); 36 + const store = tx.objectStore("queue"); 37 + 38 + store.put(songs, "songs"); 39 + store.put(queueIndex, "queueIndex"); 40 + 41 + tx.onerror = () => reject(new Error("Failed to save queue")); 42 + tx.oncomplete = () => resolve(); 43 + }); 44 + } 45 + 46 + async load() { 47 + const db = await this.ensureDB(); 48 + if (!db) throw new Error("IndexedDB unavailable"); 49 + 50 + return new Promise((resolve, reject) => { 51 + const tx = db.transaction(["queue"], "readonly"); 52 + const store = tx.objectStore("queue"); 53 + 54 + const songsReq = store.get("songs"); 55 + const indexReq = store.get("queueIndex"); 56 + 57 + tx.onerror = () => reject(new Error("Failed to load queue")); 58 + tx.oncomplete = () => { 59 + resolve({ 60 + songs: songsReq.result || [], 61 + queueIndex: indexReq.result ?? -1, 62 + }); 63 + }; 64 + }); 65 + } 66 + 67 + async clear() { 68 + const db = await this.ensureDB(); 69 + if (!db) throw new Error("IndexedDB unavailable"); 70 + 71 + return new Promise((resolve, reject) => { 72 + const tx = db.transaction(["queue"], "readwrite"); 73 + const store = tx.objectStore("queue"); 74 + 75 + store.clear(); 76 + 77 + tx.onerror = () => reject(new Error("Failed to clear queue")); 78 + tx.oncomplete = () => resolve(); 79 + }); 80 + } 81 + } 82 + 83 + const queueStorage = new QueueStorage();
+14 -43
src/js/queue.js
··· 125 125 updateQueue(); 126 126 } 127 127 128 - // persist queue and current index to localStorage 129 - function saveQueue() { 128 + // persist queue and current index to IndexedDB 129 + async function saveQueue() { 130 130 try { 131 - const serialized = state.queue.map((song) => ({ 132 - id: song.id, 133 - title: song.title, 134 - artist: song.artist, 135 - album: song.album, 136 - coverArt: song.coverArt, 137 - duration: song.duration, 138 - track: song.track, 139 - discNumber: song.discNumber, 140 - })); 141 - localStorage.setItem("tinysub_queue", JSON.stringify(serialized)); 142 - localStorage.setItem( 143 - "tinysub_queue_index", 144 - JSON.stringify(state.queueIndex), 145 - ); 131 + await queueStorage.save(state.queue, state.queueIndex); 146 132 } catch (error) { 147 - console.warn("[Queue] localStorage error:", error.message); 148 - if (error.name === "QuotaExceededError") { 149 - try { 150 - localStorage.setItem( 151 - "tinysub_queue_index", 152 - JSON.stringify(state.queueIndex), 153 - ); 154 - } catch { 155 - console.error( 156 - "[Queue] Failed to save even queue index, quota exceeded", 157 - ); 158 - } 159 - } 133 + console.warn("[Queue] storage error:", error.message); 160 134 } 161 135 } 162 136 ··· 205 179 if (onQueueChange) onQueueChange(); 206 180 } 207 181 208 - // restore queue from localStorage 209 - function loadQueue() { 210 - const saved = localStorage.getItem("tinysub_queue"); 211 - if (!saved) return false; 182 + // restore queue from IndexedDB 183 + async function loadQueue() { 184 + try { 185 + const { songs, queueIndex } = await queueStorage.load(); 186 + if (!Array.isArray(songs) || songs.length === 0) return false; 212 187 213 - try { 214 - state.queue = JSON.parse(saved); 215 - const savedIndex = localStorage.getItem("tinysub_queue_index"); 216 - if (savedIndex !== null) { 217 - const index = JSON.parse(savedIndex); 218 - if (isValidQueueIndex(index, state.queue.length)) { 219 - state.queueIndex = index; 220 - } 188 + state.queue = songs; 189 + if (isValidQueueIndex(queueIndex, state.queue.length)) { 190 + state.queueIndex = queueIndex; 221 191 } 222 192 return true; 223 - } catch { 193 + } catch (error) { 194 + console.warn("[Queue] Failed to load queue:", error.message); 224 195 return false; 225 196 } 226 197 }