A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

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

feat: save scrobbler auth data in settings

+139 -23
+13 -9
src/components/supplement/last.fm/element.js
··· 1 1 import { md5 } from "@noble/hashes/legacy.js"; 2 2 import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js"; 3 3 4 - import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js"; 4 + import { 5 + BroadcastableDiffuseElement, 6 + defineElement, 7 + } from "~/common/element.js"; 5 8 import { computed, signal } from "~/common/signal.js"; 9 + import { clearSession, readSession, saveSession } from "../session.js"; 6 10 7 11 /** 8 12 * @import {Track} from "~/definitions/types.d.ts" ··· 95 99 ); 96 100 97 101 this.#isAuthenticating.set(true); 102 + 98 103 try { 99 104 const session = await this.#getSession(urlToken); 100 - this.#setSession(session); 105 + await this.#setSession(session); 101 106 } catch (err) { 102 107 console.warn("last.fm: failed to exchange token for session", err); 103 108 } finally { ··· 107 112 return; 108 113 } 109 114 110 - // Restore an existing session from localStorage 111 - const stored = localStorage.getItem(STORAGE_KEY); 115 + const stored = await readSession(this, STORAGE_KEY); 112 116 113 117 if (stored) { 114 118 try { ··· 121 125 this.#handle.value = handle; 122 126 } 123 127 } catch { 124 - localStorage.removeItem(STORAGE_KEY); 128 + await clearSession(this, STORAGE_KEY); 125 129 } 126 130 } 127 131 } ··· 146 150 /** 147 151 * Clear the stored session. 148 152 */ 149 - signOut() { 153 + async signOut() { 150 154 this.#sessionKey.set(null); 151 155 this.#handle.set(null); 152 - localStorage.removeItem(STORAGE_KEY); 156 + await clearSession(this, STORAGE_KEY); 153 157 } 154 158 155 159 /** @param {{ key: string, name: string }} session */ 156 - #setSession({ key, name: handle }) { 160 + async #setSession({ key, name: handle }) { 157 161 this.#sessionKey.set(key); 158 162 this.#handle.set(handle); 159 - localStorage.setItem(STORAGE_KEY, JSON.stringify({ key, name: handle })); 163 + await saveSession(this, STORAGE_KEY, JSON.stringify({ key, name: handle })); 160 164 } 161 165 162 166 // SCROBBLE ACTIONS
+6 -5
src/components/supplement/listenbrainz/element.js
··· 1 1 import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js"; 2 2 import { computed, signal } from "~/common/signal.js"; 3 + import { clearSession, readSession, saveSession } from "../session.js"; 3 4 4 5 /** 5 6 * @import {Track} from "~/definitions/types.d.ts" ··· 65 66 async #tryRestore() { 66 67 await this.whenConnected(); 67 68 68 - const stored = localStorage.getItem(STORAGE_KEY); 69 + const stored = await readSession(this, STORAGE_KEY); 69 70 70 71 if (stored) { 71 72 try { ··· 79 80 this.#handle.value = username; 80 81 } 81 82 } catch { 82 - localStorage.removeItem(STORAGE_KEY); 83 + await clearSession(this, STORAGE_KEY); 83 84 } 84 85 } 85 86 } ··· 97 98 const username = await this.#validateToken(token); 98 99 this.#userToken.set(token); 99 100 this.#handle.set(username); 100 - localStorage.setItem(STORAGE_KEY, JSON.stringify({ token, username })); 101 + await saveSession(this, STORAGE_KEY, JSON.stringify({ token, username })); 101 102 } catch (err) { 102 103 console.warn("listenbrainz: failed to authenticate", err); 103 104 throw err; ··· 109 110 /** 110 111 * Clear the stored session. 111 112 */ 112 - signOut() { 113 + async signOut() { 113 114 this.#userToken.set(null); 114 115 this.#handle.set(null); 115 - localStorage.removeItem(STORAGE_KEY); 116 + await clearSession(this, STORAGE_KEY); 116 117 } 117 118 118 119 // SCROBBLE ACTIONS
+6 -5
src/components/supplement/rocksky/element.js
··· 2 2 3 3 import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js"; 4 4 import { signal } from "~/common/signal.js"; 5 + import { clearSession, readSession, saveSession } from "../session.js"; 5 6 6 7 import { 7 8 clearStoredSession, ··· 91 92 this.#handle.value = did; 92 93 } 93 94 94 - localStorage.setItem(STORAGE_KEY, JSON.stringify({ did })); 95 + await saveSession(this, STORAGE_KEY, JSON.stringify({ did })); 95 96 return; 96 97 } 97 98 } catch (err) { ··· 99 100 } 100 101 101 102 // Restore previously stored connection state. 102 - const stored = localStorage.getItem(STORAGE_KEY); 103 + const stored = await readSession(this, STORAGE_KEY); 103 104 104 105 if (stored) { 105 106 try { ··· 113 114 this.#handle.value = did; 114 115 } 115 116 } catch { 116 - localStorage.removeItem(STORAGE_KEY); 117 + await clearSession(this, STORAGE_KEY); 117 118 } 118 119 } 119 120 } ··· 139 140 /** 140 141 * Disconnect from Rocksky. 141 142 */ 142 - signOut() { 143 + async signOut() { 143 144 const did = localStorage.getItem(DID_STORAGE_KEY); 144 145 145 146 if (did) { ··· 152 153 153 154 this.#connected.set(false); 154 155 this.#handle.set(null); 155 - localStorage.removeItem(STORAGE_KEY); 156 + await clearSession(this, STORAGE_KEY); 156 157 } 157 158 158 159 // SCROBBLE ACTIONS
+86
src/components/supplement/session.js
··· 1 + import { queryOptional } from "~/common/element.js"; 2 + import * as Output from "~/common/output.js"; 3 + 4 + /** 5 + * @import {DiffuseElement} from "~/common/element.js" 6 + * @import {OutputElement} from "~/components/output/types.d.ts" 7 + */ 8 + 9 + /** 10 + * Read a stored session value from output settings when an output is configured, 11 + * otherwise falls back to localStorage. 12 + * 13 + * @param {DiffuseElement} element 14 + * @param {string} key 15 + * @returns {Promise<string | null>} 16 + */ 17 + export async function readSession(element, key) { 18 + /** @type {OutputElement | null} */ 19 + const output = queryOptional(element, "output-selector"); 20 + 21 + if (output) { 22 + const settings = await Output.data(output.settings); 23 + const existing = settings.find((s) => s.key === key); 24 + if (existing) return existing.value; 25 + } 26 + 27 + return localStorage.getItem(key); 28 + } 29 + 30 + /** 31 + * Save a session value to output settings when an output is configured, 32 + * otherwise falls back to localStorage. 33 + * 34 + * @param {DiffuseElement} element 35 + * @param {string} key 36 + * @param {string} value 37 + */ 38 + export async function saveSession(element, key, value) { 39 + /** @type {OutputElement | null} */ 40 + const output = queryOptional(element, "output-selector"); 41 + 42 + if (output) { 43 + const settings = await Output.data(output.settings); 44 + const existing = settings.find((s) => s.key === key); 45 + 46 + const updated = existing 47 + ? settings.map((s) => s.key === key ? { ...s, value } : s) 48 + : [ 49 + ...settings, 50 + { 51 + $type: /** @type {"sh.diffuse.output.setting"} */ ( 52 + "sh.diffuse.output.setting" 53 + ), 54 + id: crypto.randomUUID(), 55 + key, 56 + value, 57 + }, 58 + ]; 59 + 60 + await output.settings.save(updated); 61 + } else { 62 + localStorage.setItem(key, value); 63 + } 64 + } 65 + 66 + /** 67 + * Remove a stored session value from output settings when an output is configured, 68 + * otherwise falls back to localStorage. 69 + * 70 + * @param {DiffuseElement} element 71 + * @param {string} key 72 + */ 73 + export async function clearSession(element, key) { 74 + /** @type {OutputElement | null} */ 75 + const output = queryOptional(element, "output-selector"); 76 + 77 + if (output) { 78 + const settings = await Output.data(output.settings); 79 + const updated = settings.filter((s) => s.key !== key); 80 + if (updated.length !== settings.length) { 81 + await output.settings.save(updated); 82 + } 83 + } else { 84 + localStorage.removeItem(key); 85 + } 86 + }
+7 -1
src/facets/misc/scrobble/index.inline.js
··· 10 10 11 11 async function setup() { 12 12 await foundation.orchestrator.scrobbleAudio(); 13 - const configurator = await foundation.configurator.scrobbles(); 13 + const [configurator, output] = await Promise.all([ 14 + foundation.configurator.scrobbles(), 15 + foundation.orchestrator.output(), 16 + ]); 14 17 15 18 // Bundled scrobblers 16 19 const { default: LastFmScrobbler } = await import( ··· 19 22 20 23 const lastFm = new LastFmScrobbler(); 21 24 lastFm.setAttribute("group", foundation.GROUP); 25 + lastFm.setAttribute("output-selector", output.selector); 22 26 configurator.append(lastFm); 23 27 24 28 const { default: RockskyScrobbler } = await import( ··· 27 31 28 32 const rocksky = new RockskyScrobbler(); 29 33 rocksky.setAttribute("group", foundation.GROUP); 34 + rocksky.setAttribute("output-selector", output.selector); 30 35 configurator.append(rocksky); 31 36 32 37 const { default: ListenBrainzScrobbler } = await import( ··· 35 40 36 41 const listenBrainz = new ListenBrainzScrobbler(); 37 42 listenBrainz.setAttribute("group", foundation.GROUP); 43 + listenBrainz.setAttribute("output-selector", output.selector); 38 44 configurator.append(listenBrainz); 39 45 }
+7 -1
src/facets/misc/scrobble/last.fm/index.inline.js
··· 19 19 } 20 20 } 21 21 22 - const configurator = await foundation.configurator.scrobbles(); 22 + const [configurator, output] = await Promise.all([ 23 + foundation.configurator.scrobbles(), 24 + foundation.orchestrator.output(), 25 + ]); 23 26 24 27 /** @type {import("~/components/supplement/last.fm/element.js").CLASS | null} */ 25 28 let lastFm = configurator.querySelector("ds-lastfm-scrobbler"); ··· 30 33 31 34 lastFm = new LastFmScrobbler(); 32 35 lastFm.setAttribute("group", foundation.GROUP); 36 + lastFm.setAttribute("output-selector", output.selector); 33 37 configurator.append(lastFm); 38 + } else { 39 + lastFm.setAttribute("output-selector", output.selector); 34 40 } 35 41 36 42 await customElements.whenDefined(lastFm.localName);
+7 -1
src/facets/misc/scrobble/listenbrainz/index.inline.js
··· 7 7 8 8 foundation.setup({ title: "ListenBrainz | Scrobble | Diffuse" }); 9 9 10 - const configurator = await foundation.configurator.scrobbles(); 10 + const [configurator, output] = await Promise.all([ 11 + foundation.configurator.scrobbles(), 12 + foundation.orchestrator.output(), 13 + ]); 11 14 12 15 /** @type {import("~/components/supplement/listenbrainz/element.js").CLASS | null} */ 13 16 let listenBrainz = configurator.querySelector("ds-listenbrainz-scrobbler"); ··· 18 21 19 22 listenBrainz = new ListenBrainzScrobbler(); 20 23 listenBrainz.setAttribute("group", foundation.GROUP); 24 + listenBrainz.setAttribute("output-selector", output.selector); 21 25 configurator.append(listenBrainz); 26 + } else { 27 + listenBrainz.setAttribute("output-selector", output.selector); 22 28 } 23 29 24 30 await customElements.whenDefined(listenBrainz.localName);
+7 -1
src/facets/misc/scrobble/rocksky/index.inline.js
··· 15 15 // Set doc title 16 16 foundation.setup({ title: "Rocksky | Scrobble | Diffuse" }); 17 17 18 - const configurator = await foundation.configurator.scrobbles(); 18 + const [configurator, output] = await Promise.all([ 19 + foundation.configurator.scrobbles(), 20 + foundation.orchestrator.output(), 21 + ]); 19 22 20 23 /** @type {import("~/components/supplement/rocksky/element.js").CLASS | null} */ 21 24 let rocksky = configurator.querySelector("ds-rocksky-scrobbler"); ··· 26 29 27 30 rocksky = new RockskyScrobbler(); 28 31 rocksky.setAttribute("group", foundation.GROUP); 32 + rocksky.setAttribute("output-selector", output.selector); 29 33 configurator.append(rocksky); 34 + } else { 35 + rocksky.setAttribute("output-selector", output.selector); 30 36 } 31 37 32 38 await customElements.whenDefined(rocksky.localName);