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.

fix: a bunch of atproto things

+88 -62
+2 -1
.env
··· 1 - #DISABLE_AUTOMATIC_TRACKS_PROCESSING=t 1 + ATPROTO_CLIENT_ID=http://127.0.0.1:3000/oauth-client-metadata.json 2 + DISABLE_AUTOMATIC_TRACKS_PROCESSING=t
+4 -2
src/components/input/opensubsonic/worker.js
··· 159 159 160 160 stats: removeUndefinedValuesFromRecord({ 161 161 albumGain: undefined, 162 - bitrate: song.bitRate ? song.bitRate * 1000 : undefined, 162 + bitrate: song.bitRate ? Math.round(song.bitRate * 1000) : undefined, 163 163 bitsPerSample: undefined, 164 164 codec: undefined, 165 165 container: undefined, 166 - duration: song.duration, 166 + duration: song.duration != null 167 + ? Math.round(song.duration * 1000) 168 + : undefined, 167 169 lossless: undefined, 168 170 numberOfChannels: undefined, 169 171 sampleRate: undefined,
+24 -31
src/components/output/raw/atproto/element.js
··· 20 20 class ATProtoOutput extends BroadcastableDiffuseElement { 21 21 static NAME = "diffuse/output/raw/atproto"; 22 22 23 + #manager; 24 + 25 + /** @type {PromiseWithResolvers<void>} */ 26 + #authenticated = Promise.withResolvers(); 27 + 23 28 /** @type {Client | null} */ 24 29 #rpc = null; 25 30 26 31 /** @type {OAuthUserAgent | null} */ 27 32 #agent = null; 28 33 29 - /** @type {string | null} */ 30 - #did = null; 31 - 32 - /** Public signal exposing the authenticated DID (null when not logged in). */ 33 - $did = signal(/** @type {string | null} */ (null)); 34 - 35 - #manager; 36 - 37 - /** @type {PromiseWithResolvers<void>} */ 38 - #authenticated = Promise.withResolvers(); 39 - 40 34 constructor() { 41 35 super(); 42 36 43 37 /** @type {OutputManager} */ 44 38 this.#manager = outputManager({ 45 - init: async () => { 46 - await this.#ensureAuthenticated(); 47 - return true; 48 - }, 49 39 facets: { 50 40 empty: () => [], 51 41 get: () => this.#listRecords("sh.diffuse.output.facet"), ··· 73 63 this.themes = this.#manager.themes; 74 64 this.tracks = this.#manager.tracks; 75 65 } 66 + 67 + // SIGNALS 68 + 69 + #did = signal(/** @type {string | null} */ (null)); 70 + 71 + // STATE 72 + 73 + did = this.#did.get; 76 74 77 75 // LIFECYCLE 78 76 ··· 111 109 #setSession(session) { 112 110 this.#agent = new OAuthUserAgent(session); 113 111 this.#rpc = new Client({ handler: this.#agent }); 114 - this.#did = session.info.sub; 115 - this.$did.value = session.info.sub; 112 + this.#did.value = session.info.sub; 116 113 this.#authenticated.resolve(); 117 114 } 118 115 119 - async #ensureAuthenticated() { 120 - await this.whenConnected(); 121 - return this.#authenticated.promise; 122 - } 123 - 124 116 /** 125 117 * Initiate the OAuth flow. 126 118 * Navigates the browser to the authorization server. ··· 138 130 if (this.#agent) { 139 131 await logout(this.#agent); 140 132 this.#agent = null; 133 + this.#authenticated = Promise.withResolvers(); 134 + this.#did.value = null; 141 135 this.#rpc = null; 142 - this.#did = null; 143 - this.$did.value = null; 144 - this.#authenticated = Promise.withResolvers(); 145 136 } 146 137 } 147 138 ··· 153 144 * @returns {Promise<T[]>} 154 145 */ 155 146 async #listRecords(collection) { 156 - if (!this.#rpc || !this.#did) return []; 147 + if (!this.#rpc || !this.#did.value) return []; 157 148 158 149 const records = []; 159 150 let cursor; ··· 162 153 /** @type {any} */ 163 154 const page = await ok(this.#rpc.get( 164 155 "com.atproto.repo.listRecords", 165 - { params: { repo: this.#did, collection, limit: 100, cursor } }, 156 + { params: { repo: this.#did.value, collection, limit: 100, cursor } }, 166 157 )); 167 158 168 159 for (const record of page.records) { ··· 180 171 * @param {Array<{ id: string }>} data 181 172 */ 182 173 async #putRecordsSync(collection, data) { 183 - if (!this.#rpc || !this.#did) return; 174 + if (!this.#rpc || !this.#did.value) return; 184 175 185 176 // 1. Fetch current state 186 177 /** @type {Map<string, { rkey: string, value: unknown }>} */ ··· 191 182 /** @type {any} */ 192 183 const page = await ok(this.#rpc.get( 193 184 "com.atproto.repo.listRecords", 194 - { params: { repo: this.#did, collection, limit: 100, cursor } }, 185 + { params: { repo: this.#did.value, collection, limit: 100, cursor } }, 195 186 )); 196 187 197 188 for (const record of page.records) { ··· 203 194 } while (cursor); 204 195 205 196 // 2. Build desired state 206 - const desired = new Map(data.map((record) => [record.id, record])); 197 + const desired = new Map( 198 + data.map((record) => [record.id, { $type: collection, ...record }]), 199 + ); 207 200 208 201 // 3. Compute diff 209 202 /** @type {unknown[]} */ ··· 242 235 // 4. Apply 243 236 if (writes.length > 0) { 244 237 await this.#rpc.post("com.atproto.repo.applyWrites", { 245 - input: { repo: this.#did, writes }, 238 + input: { repo: this.#did.value, writes }, 246 239 }); 247 240 } 248 241 }
+22 -10
src/components/output/raw/atproto/oauth.js
··· 22 22 * @import {Session} from "@atcute/oauth-browser-client" 23 23 */ 24 24 25 - const STORAGE_KEY = "dor-atproto:did"; 25 + const STORAGE_KEY = "diffuse/output/raw/atproto/did"; 26 26 27 27 // CONFIGURE 28 28 // ========= 29 29 30 + const redirect_uri = 31 + (globalThis.location.origin + globalThis.location.pathname + 32 + globalThis.location.search).replace( 33 + "://localhost", 34 + "://127.0.0.1", 35 + ); 36 + 30 37 configureOAuth({ 31 38 metadata: { 32 - client_id: import.meta.env?.VITE_ATPROTO_CLIENT_ID ?? 33 - "https://elements.diffuse.sh/oauth-client-metadata.json", 34 - redirect_uri: 35 - window.location.origin.replace("://localhost", "://127.0.0.1") + 36 - window.location.pathname, 39 + client_id: redirect_uri.startsWith("http://127.0.0.1") 40 + ? `http://localhost/?redirect_uri=${ 41 + encodeURIComponent(redirect_uri) 42 + }&scope=${encodeURIComponent("atproto transition:generic")}` 43 + : import.meta.env?.ATPROTO_CLIENT_ID ?? 44 + "https://elements.diffuse.sh/oauth-client-metadata.json", 45 + redirect_uri, 37 46 }, 38 47 identityResolver: new LocalActorResolver({ 39 48 handleResolver: new XrpcHandleResolver({ ··· 63 72 scope: "atproto transition:generic", 64 73 }); 65 74 66 - window.location.assign(authUrl.toString()); 75 + globalThis.location.assign(authUrl.toString()); 67 76 } 68 77 69 78 // SESSION RESTORE / CALLBACK ··· 76 85 * @returns {Promise<Session | null>} 77 86 */ 78 87 export async function restoreOrFinalize() { 88 + const location = globalThis.location; 89 + 79 90 // Check for OAuth callback parameters (the library uses response_mode=fragment, 80 91 // so params arrive in the URL hash, not the query string) 81 - const params = new URLSearchParams(window.location.hash.slice(1)); 92 + const params = new URLSearchParams(location.hash.slice(1)); 82 93 83 94 if (params.has("code")) { 84 95 const result = await finalizeAuthorization(params); ··· 87 98 history.replaceState( 88 99 null, 89 100 "", 90 - window.location.pathname + window.location.search, 101 + location.pathname + location.search, 91 102 ); 92 103 93 104 // Persist the DID for future session restoration ··· 105 116 /** @type {`did:${string}:${string}`} */ (did), 106 117 { allowStale: true }, 107 118 ); 108 - } catch { 119 + } catch (err) { 120 + console.warn(err) 109 121 localStorage.removeItem(STORAGE_KEY); 110 122 return null; 111 123 }
+2 -2
src/components/output/raw/atproto/types.d.ts
··· 1 - import type { Signal } from "@common/signal.d.ts"; 1 + import type { SignalReader } from "@common/signal.d.ts"; 2 2 import type { OutputElement } from "../../types.d.ts"; 3 3 4 4 export type ATProtoOutputElement = 5 5 & OutputElement 6 6 & { 7 - $did: Signal<string | null>; 7 + did: SignalReader<string | null>; 8 8 login(handle: string): Promise<void>; 9 9 logout(): Promise<void>; 10 10 };
+17 -7
src/components/processor/metadata/common.js
··· 57 57 58 58 /** @type {TrackStats} */ 59 59 const statsFull = { 60 - albumGain: meta.format.albumGain, 61 - bitrate: meta.format.bitrate, 62 - bitsPerSample: meta.format.bitsPerSample, 60 + albumGain: maybeRound(meta.format.albumGain), 61 + bitrate: maybeRound(meta.format.bitrate), 62 + bitsPerSample: maybeRound(meta.format.bitsPerSample), 63 63 codec: meta.format.codec, 64 64 container: meta.format.container, 65 - duration: meta.format.duration, 65 + duration: meta.format.duration != null 66 + ? Math.round(meta.format.duration * 1000) 67 + : undefined, 66 68 lossless: meta.format.lossless, 67 - numberOfChannels: meta.format.numberOfChannels, 68 - sampleRate: meta.format.sampleRate, 69 - trackGain: meta.format.trackGain, 69 + numberOfChannels: maybeRound(meta.format.numberOfChannels), 70 + sampleRate: maybeRound(meta.format.sampleRate), 71 + trackGain: maybeRound(meta.format.trackGain), 70 72 }; 71 73 72 74 /** @type {TrackTags} */ ··· 146 148 tags, 147 149 }; 148 150 } 151 + 152 + /** 153 + * @param {number | undefined} value 154 + * @returns {number | undefined} 155 + */ 156 + function maybeRound(value) { 157 + return typeof value === "number" ? Math.round(value) : value; 158 + }
+1 -1
src/definitions/output/track.json
··· 47 47 "bitsPerSample": { "type": "integer", "description": "Bit depth" }, 48 48 "codec": { "type": "string", "description": "Compression algorithm" }, 49 49 "container": { "type": "string", "description": "Encoding format" }, 50 - "duration": { "type": "integer", "description": "Duration in seconds" }, 50 + "duration": { "type": "integer", "description": "Duration in milliseconds" }, 51 51 "lossless": { "type": "boolean", "description": "Is track lossless" }, 52 52 "numberOfChannels": { "type": "integer", "description": "Number of audio channels" }, 53 53 "sampleRate": { "type": "integer", "description": "Samples per second" },
+4 -1
src/facets/l/index.js
··· 57 57 } 58 58 59 59 // TODO: Message that facet was not found 60 - if (!facet) return; 60 + if (!facet) { 61 + console.error("Facet not found"); 62 + return; 63 + } 61 64 62 65 // Make sure HTML is loaded 63 66 // TODO: Handle URL loading error
+5 -4
src/themes/blur/artwork-controller/element.js
··· 244 244 const curr = this.$queue.value?.now?.() ?? undefined; 245 245 const audio = this.#audio(); 246 246 const prog = audio?.progress() ?? 0; 247 - const dur = curr?.stats?.duration ?? audio?.duration(); 247 + const durMs = curr?.stats?.duration ?? 248 + (audio?.duration() != null ? audio.duration() * 1000 : undefined); 248 249 249 - if (audio && dur != undefined && !isNaN(dur)) { 250 + if (audio && durMs != undefined && !isNaN(durMs)) { 250 251 const p = Temporal.Duration.from({ 251 - milliseconds: Math.round(dur * prog * 1000), 252 + milliseconds: Math.round(durMs * prog), 252 253 }).round({ 253 254 largestUnit: "hours", 254 255 }); 255 256 256 - const d = Temporal.Duration.from({ milliseconds: Math.round(dur * 1000) }) 257 + const d = Temporal.Duration.from({ milliseconds: Math.round(durMs) }) 257 258 .round({ 258 259 largestUnit: "hours", 259 260 });
+1 -1
src/themes/webamp/configurators/output/element.js
··· 68 68 * @param {RenderArg} _ 69 69 */ 70 70 render({ html }) { 71 - const did = this.#atproto.value?.$did.value ?? null; 71 + const did = this.#atproto.value?.did() ?? null; 72 72 73 73 return html` 74 74 <link rel="stylesheet" href="styles/vendor/98.css" />
+6 -2
src/themes/webamp/webamp/element.js
··· 58 58 dispatch({ 59 59 type: "ADD_TRACK_FROM_URL", 60 60 url: track.uri, 61 - duration: track.stats?.duration, 61 + duration: track.stats?.duration != null 62 + ? track.stats.duration / 1000 63 + : undefined, 62 64 defaultName: undefined, 63 65 id: idx, 64 66 atIndex: idx, ··· 66 68 67 69 dispatch({ 68 70 type: "SET_MEDIA_DURATION", 69 - duration: track.stats?.duration, 71 + duration: track.stats?.duration != null 72 + ? track.stats.duration / 1000 73 + : undefined, 70 74 id: idx, 71 75 }); 72 76