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

Configure Feed

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

fix: a bunch of things

+267 -132
+1 -1
.env
··· 1 1 ATPROTO_CLIENT_ID=http://127.0.0.1:3000/oauth-client-metadata.json 2 - DISABLE_AUTOMATIC_TRACKS_PROCESSING=t 2 + #DISABLE_AUTOMATIC_TRACKS_PROCESSING=t
+1
deno.jsonc
··· 24 24 "@okikio/transferables": "jsr:@okikio/transferables@^1.0.2", 25 25 "@orama/orama": "npm:@orama/orama@^3.1.18", 26 26 "@phosphor-icons/web": "npm:@phosphor-icons/web@^2.1.2", 27 + "@std/html": "jsr:@std/html@^1.0.5", 27 28 "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", 28 29 "alien-signals": "npm:alien-signals@^3.1.2", 29 30 "bs58check": "npm:bs58check@^4.0.0",
+2 -5
src/common/playlist.js
··· 2 2 * @import {PlaylistItem, Track} from "@definitions/types.d.ts" 3 3 */ 4 4 5 - import { Temporal } from "@js-temporal/polyfill"; 5 + import { compareTimestamps } from "@common/utils.js"; 6 6 7 7 /** 8 8 * Filter tracks by playlist membership using an indexed lookup. ··· 151 151 if (group.length > 1) { 152 152 group.sort((a, b) => { 153 153 if (!a.updatedAt || !b.updatedAt) return a.updatedAt ? 1 : -1; 154 - return Temporal.ZonedDateTime.compare( 155 - Temporal.ZonedDateTime.from(a.updatedAt), 156 - Temporal.ZonedDateTime.from(b.updatedAt), 157 - ); 154 + return compareTimestamps(a.updatedAt, b.updatedAt); 158 155 }); 159 156 } 160 157 }
+19 -5
src/common/utils.js
··· 1 + import { Temporal } from "@js-temporal/polyfill"; 1 2 import { base64url } from "iso-base/rfc4648"; 2 3 import { xxh32r } from "xxh32/dist/raw.js"; 3 4 ··· 32 33 */ 33 34 export function boolAttr(value) { 34 35 return value === ""; 36 + } 37 + 38 + /** 39 + * @param {string} a 40 + * @param {string} b 41 + */ 42 + export function compareTimestamps(a, b) { 43 + return Temporal.Instant.compare( 44 + Temporal.Instant.from(a), 45 + Temporal.Instant.from(b), 46 + ); 35 47 } 36 48 37 49 /** ··· 142 154 * @returns {string} 143 155 */ 144 156 export function safeDecodeURIComponent(str) { 145 - try { 146 - return decodeURIComponent(str); 147 - } catch { 148 - return str; 149 - } 157 + return str.replace( 158 + /%u([0-9A-Fa-f]{4})|%([0-9A-Fa-f]{2})/g, 159 + (_, unicode, byte) => 160 + unicode 161 + ? String.fromCharCode(parseInt(unicode, 16)) 162 + : String.fromCharCode(parseInt(byte, 16)), 163 + ); 150 164 } 151 165 152 166 /**
+10 -1
src/components/input/opensubsonic/worker.js
··· 112 112 const sid = serverId(parsed.server); 113 113 servers[sid] = parsed.server; 114 114 115 + const path = safeDecodeURIComponent(parsed.path); 115 116 cache[sid] ??= {}; 116 - cache[sid][safeDecodeURIComponent(parsed.path)] = t; 117 + cache[sid][path] = t; 117 118 }); 118 119 119 120 /** ··· 154 155 const fromCache = path ? cache[sid]?.[path] : undefined; 155 156 if (fromCache) return fromCache; 156 157 158 + const now = new Date().toISOString(); 159 + 157 160 /** @type {Track} */ 158 161 const track = { 159 162 $type: "sh.diffuse.output.track", 160 163 id: TID.now(), 164 + createdAt: now, 165 + updatedAt: now, 161 166 kind: autoTypeToTrackKind(song.type), 162 167 uri: buildURI(server, { songId: song.id, path }), 163 168 ··· 240 245 // keep a placeholder track so the server gets 241 246 // picked up as a source. 242 247 if (!tracks.length) { 248 + const now = new Date().toISOString(); 249 + 243 250 tracks = [{ 244 251 $type: "sh.diffuse.output.track", 245 252 id: TID.now(), 253 + createdAt: now, 254 + updatedAt: now, 246 255 kind: "placeholder", 247 256 uri: buildURI(server), 248 257 }];
+10
src/components/input/s3/worker.js
··· 134 134 const id = cachedTrack?.id || TID.now(); 135 135 const stats = cachedTrack?.stats; 136 136 const tags = cachedTrack?.tags; 137 + const now = new Date().toISOString(); 137 138 138 139 /** @type {Track} */ 139 140 const track = { 140 141 $type: "sh.diffuse.output.track", 141 142 id, 143 + createdAt: cachedTrack?.createdAt ?? now, 144 + updatedAt: cachedTrack?.updatedAt ?? now, 142 145 stats, 143 146 tags, 144 147 uri: buildURI(bucket, l.key), ··· 151 154 // keep a placeholder track so the bucket gets 152 155 // picked up as a source. 153 156 if (!tracks.length) { 157 + const now = new Date().toISOString(); 158 + 154 159 tracks = [{ 155 160 $type: "sh.diffuse.output.track", 156 161 id: TID.now(), 162 + createdAt: now, 163 + updatedAt: now, 157 164 kind: "placeholder", 158 165 uri: buildURI(bucket), 159 166 }]; ··· 208 215 }; 209 216 210 217 const uri = buildURI(bucket); 218 + const now = new Date().toISOString(); 211 219 212 220 /** @type {Track} */ 213 221 const track = { 214 222 $type: "sh.diffuse.output.track", 215 223 id: TID.now(), 224 + createdAt: now, 225 + updatedAt: now, 216 226 kind: "placeholder", 217 227 uri, 218 228 };
+6 -1
src/components/orchestrator/process-tracks/worker.js
··· 93 93 processed++; 94 94 $progress.value = { processed, total: tracks.length }; 95 95 96 - return [...acc, { ...track, stats, tags }]; 96 + return [...acc, { 97 + ...track, 98 + stats, 99 + tags, 100 + updatedAt: new Date().toISOString(), 101 + }]; 97 102 }, 98 103 Promise.resolve([]), 99 104 );
+1
src/components/transformer/output/bytes/dasl-sync/constants.js
··· 1 + export const IDB_PREFIX = "diffuse/transformer/bytes/dasl-sync";
+213 -119
src/components/transformer/output/bytes/dasl-sync/element.js
··· 1 - import { Temporal } from "@js-temporal/polyfill"; 2 - import { ifDefined } from "lit-html/directives/if-defined.js"; 1 + import * as IDB from "idb-keyval"; 3 2 import { decode, encode } from "@atcute/cbor"; 4 3 import deepDiff from "@fry69/deep-diff"; 5 4 6 5 import "@components/output/polymorphic/indexed-db/element.js"; 7 6 8 7 import * as CID from "@common/cid.js"; 9 - import { computed, signal } from "@common/signal.js"; 8 + import { computed, signal, untracked } from "@common/signal.js"; 9 + import { compareTimestamps } from "@common/utils.js"; 10 10 import { OutputTransformer } from "../../base.js"; 11 + import { IDB_PREFIX } from "./constants.js"; 11 12 12 13 /** 13 - * @import { RenderArg } from "@common/element.d.ts" 14 - * @import { SignalReader } from "@common/signal.d.ts"; 15 - * @import { OutputElement } from "@components/output/types.d.ts"; 16 - * 14 + * @import { Signal, SignalReader } from "@common/signal.d.ts"; 17 15 * @import { Container } from "./types.d.ts" 18 16 */ 19 17 ··· 25 23 super(); 26 24 27 25 const remote = this.base(); 28 - const local = this.#localOutput.get; 29 26 30 27 /** 31 28 * @template {{ id: string; updatedAt: string }} T 32 29 * @param {SignalReader<Uint8Array | undefined>} localCollection 33 30 * @param {SignalReader<Uint8Array | undefined>} remoteCollection 34 - * @returns {SignalReader<{ container: Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }; diverged: boolean; local: boolean; remote: boolean; }>} 35 31 */ 36 32 const state = (localCollection, remoteCollection) => { 37 - return computed(() => { 33 + /** 34 + * @typedef {{ container: Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }; diverged: boolean; local: boolean; remote: boolean; }} State 35 + */ 36 + 37 + const sig = signal( 38 + /** @type {State} */ ({ 39 + container: { 40 + cid: undefined, 41 + data: [], 42 + inventory: { current: {}, removed: [] }, 43 + }, 44 + diverged: false, 45 + local: false, 46 + remote: false, 47 + }), 48 + { eager: true }, 49 + ); 50 + 51 + /** @returns {State} */ 52 + const determine = () => { 38 53 const lb = localCollection(); 39 54 const rb = remote.ready() ? remoteCollection() : undefined; 40 55 ··· 54 69 } 55 70 : { 56 71 container: { 72 + cid: undefined, 57 73 data: [], 58 74 inventory: { current: {}, removed: [] }, 59 75 }, ··· 71 87 ); 72 88 73 89 /** 74 - * @type {Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }} 90 + * @type {State["container"]} 75 91 */ 76 92 let container = r; 77 93 78 94 if (diverged.local || diverged.remote) { 79 95 const promise = this.merge(l, r).then((c) => { 96 + console.log("Merged:", c); 80 97 mergedSignal.set(c); 81 98 return c; 82 99 }); ··· 93 110 local: diverged.local, 94 111 remote: diverged.remote, 95 112 }; 113 + }; 114 + 115 + this.effect(() => { 116 + const result = determine(); 117 + const current = untracked(sig.get); 118 + 119 + const newCID = "merged" in result.container 120 + ? undefined // handle async case separately 121 + : result.container.cid; 122 + 123 + const currentCID = "merged" in current.container 124 + ? undefined 125 + : current.container.cid; 126 + 127 + // Skip if both are non-merged and CIDs match 128 + if ( 129 + newCID !== undefined && currentCID !== undefined && 130 + newCID === currentCID 131 + ) { 132 + return; 133 + } 134 + 135 + // For the non-merged common case, set synchronously 136 + if (!("merged" in result.container)) { 137 + sig.set(result); 138 + return; 139 + } 140 + 141 + // Only go async for the merge case 142 + result.container.merged.promise.then(async (merged) => { 143 + const cur = untracked(sig.get); 144 + const curCID = "merged" in cur.container 145 + ? (await cur.container.merged.promise).cid 146 + : cur.container.cid; 147 + if (merged.cid !== curCID) { 148 + sig.set(result); 149 + } 150 + }); 96 151 }); 152 + 153 + return sig.get; 154 + }; 155 + 156 + // Local 157 + const local = { 158 + facets: this.local("facets"), 159 + playlistItems: this.local("playlistItems"), 160 + themes: this.local("themes"), 161 + tracks: this.local("tracks"), 97 162 }; 98 163 99 164 // Container signals 100 165 const facets = state( 101 - computed(() => local()?.facets?.collection()), 166 + local.facets.get, 102 167 remote.facets.collection, 103 168 ); 104 169 105 170 const playlistItems = state( 106 - computed(() => local()?.playlistItems?.collection()), 171 + local.playlistItems.get, 107 172 remote.playlistItems.collection, 108 173 ); 109 174 110 175 const themes = state( 111 - computed(() => local()?.themes?.collection()), 176 + local.themes.get, 112 177 remote.themes.collection, 113 178 ); 114 179 115 180 const tracks = state( 116 - computed(() => local()?.tracks?.collection()), 181 + local.tracks.get, 117 182 remote.tracks.collection, 118 183 ); 119 184 185 + // Output manager 120 186 this.facets = this.managerProp( 121 - computed(() => local()?.facets), 187 + { save: this.putLocalFn("facets", local.facets) }, 122 188 remote.facets, 123 189 facets, 124 190 ); 125 191 126 192 this.playlistItems = this.managerProp( 127 - computed(() => local()?.playlistItems), 193 + { save: this.putLocalFn("playlistItems", local.playlistItems) }, 128 194 remote.playlistItems, 129 195 playlistItems, 130 196 ); 131 197 132 198 this.themes = this.managerProp( 133 - computed(() => local()?.themes), 199 + { save: this.putLocalFn("themes", local.themes) }, 134 200 remote.themes, 135 201 themes, 136 202 ); 137 203 138 204 this.tracks = this.managerProp( 139 - computed(() => local()?.tracks), 205 + { save: this.putLocalFn("tracks", local.tracks) }, 140 206 remote.tracks, 141 207 tracks, 142 208 ); ··· 144 210 this.ready = () => true; 145 211 146 212 // Effects 147 - this.effect(() => { 148 - const l = local(); 149 - if (!l) return; 150 - 151 - this.effect(async () => { 152 - if (remote.facets.state() !== "loaded") return; 153 - const s = facets(); 154 - if (s.diverged) { 155 - const bytes = this.save( 156 - "merged" in s.container 157 - ? await s.container.merged.promise 158 - : s.container, 159 - ); 160 - if (l && s.local) l.facets.save(bytes); 161 - if (s.remote) remote.facets.save(bytes); 162 - } 163 - }); 164 - 165 - this.effect(async () => { 166 - if (remote.playlistItems.state() !== "loaded") return; 167 - const s = playlistItems(); 168 - if (s.diverged) { 169 - const bytes = this.save( 170 - "merged" in s.container 171 - ? await s.container.merged.promise 172 - : s.container, 173 - ); 174 - if (l && s.local) l.playlistItems.save(bytes); 175 - if (s.remote) remote.playlistItems.save(bytes); 176 - } 177 - }); 178 - 179 - this.effect(async () => { 180 - if (remote.themes.state() !== "loaded") return; 181 - const s = themes(); 182 - if (s.diverged) { 183 - const bytes = this.save( 184 - "merged" in s.container 185 - ? await s.container.merged.promise 186 - : s.container, 187 - ); 188 - if (l && s.local) l.themes.save(bytes); 189 - if (s.remote) remote.themes.save(bytes); 190 - } 191 - }); 192 - 193 - this.effect(async () => { 194 - if (remote.tracks.state() !== "loaded") return; 195 - const s = tracks(); 196 - if (s.diverged) { 197 - const bytes = this.save( 198 - "merged" in s.container 199 - ? await s.container.merged.promise 200 - : s.container, 201 - ); 202 - if (l && s.local) l.tracks.save(bytes); 203 - if (s.remote) remote.tracks.save(bytes); 204 - } 205 - }); 206 - }); 207 - } 208 - 209 - // SIGNALS 213 + // this.effect(async () => { 214 + // if (remote.facets.state() !== "loaded") return; 215 + // const s = facets(); 216 + // if (s.diverged) { 217 + // const bytes = this.save( 218 + // "merged" in s.container 219 + // ? await s.container.merged.promise 220 + // : s.container, 221 + // ); 222 + // local.facets.set(bytes); 223 + // this.putLocal("facets", bytes); 224 + // if (s.remote) remote.facets.save(bytes); 225 + // } 226 + // }); 210 227 211 - #localOutput = signal( 212 - /** @type {OutputElement<Uint8Array | undefined> | undefined} */ (undefined), 213 - ); 228 + // this.effect(async () => { 229 + // if (remote.playlistItems.state() !== "loaded") return; 230 + // const s = playlistItems(); 231 + // if (s.diverged) { 232 + // const bytes = this.save( 233 + // "merged" in s.container 234 + // ? await s.container.merged.promise 235 + // : s.container, 236 + // ); 237 + // local.playlistItems.set(bytes); 238 + // this.putLocal("playlistItems", bytes); 239 + // if (s.remote) remote.playlistItems.save(bytes); 240 + // } 241 + // }); 214 242 215 - // LIFECYCLE 243 + // this.effect(async () => { 244 + // if (remote.themes.state() !== "loaded") return; 245 + // const s = themes(); 246 + // if (s.diverged) { 247 + // const bytes = this.save( 248 + // "merged" in s.container 249 + // ? await s.container.merged.promise 250 + // : s.container, 251 + // ); 252 + // local.themes.set(bytes); 253 + // this.putLocal("themes", bytes); 254 + // if (s.remote) remote.themes.save(bytes); 255 + // } 256 + // }); 216 257 217 - /** 218 - * @override 219 - */ 220 - connectedCallback() { 221 - super.connectedCallback(); 222 - 223 - /** @type {OutputElement<Uint8Array | undefined> | null} */ 224 - const local = this.root().querySelector("dop-indexed-db"); 225 - if (!local) throw new Error("Can't find local output"); 226 - 227 - // When defined 228 - customElements.whenDefined(local.localName).then(() => { 229 - this.#localOutput.value = local; 230 - }); 258 + // this.effect(async () => { 259 + // if (remote.tracks.state() !== "loaded") return; 260 + // const s = tracks(); 261 + // if (s.diverged) { 262 + // const bytes = this.save( 263 + // "merged" in s.container 264 + // ? await s.container.merged.promise 265 + // : s.container, 266 + // ); 267 + // local.tracks.set(bytes); 268 + // this.putLocal("tracks", bytes); 269 + // if (s.remote) remote.tracks.save(bytes); 270 + // } 271 + // }); 231 272 } 232 273 233 274 // DATA FUNCTIONS ··· 253 294 254 295 /** @type {Record<string, string>} */ 255 296 const current = { ...inventory.current }; 297 + 298 + remSet.forEach((id) => { 299 + delete current[id]; 300 + }); 256 301 257 302 /** @type Promise<void>[] */ 258 303 const promises = []; ··· 314 359 * @returns {Promise<Container<T>>} 315 360 */ 316 361 async merge(a, b) { 362 + console.log("MERGE", a, b); 363 + 317 364 const removedA = new Set(a.inventory.removed); 318 365 const removedB = new Set(b.inventory.removed); 319 366 const allRemoved = removedA.union(removedB); ··· 337 384 const data = []; 338 385 339 386 // Construct `current` and `data` 340 - for await (const id of allIds) { 387 + /** @type {Promise<void>[]} */ 388 + const cidPromises = []; 389 + 390 + for (const id of allIds) { 341 391 if (allRemoved.has(id)) continue; 342 392 343 393 if (id in currentA && id in currentB) { ··· 349 399 continue; 350 400 } 351 401 352 - const isANewerThanB = Temporal.ZonedDateTime.compare( 353 - Temporal.ZonedDateTime.from(itemA.updatedAt), 354 - Temporal.ZonedDateTime.from(itemB.updatedAt), 355 - ); 402 + const isANewerThanB = itemA.updatedAt && itemB.updatedAt 403 + ? compareTimestamps(itemA.updatedAt, itemB.updatedAt) > 0 404 + : false; 356 405 357 406 const newestItem = isANewerThanB ? itemA : itemB; 358 407 const oldItem = isANewerThanB ? itemB : itemA; ··· 362 411 363 412 deepDiff.applyDiff(newestItem, mergedItem); 364 413 365 - const cid = await CID.create(0x71, encode(mergedItem)); 414 + data.push(mergedItem); 366 415 367 - data.push(mergedItem); 368 - current[id] = cid; 416 + cidPromises.push( 417 + CID.create(0x71, encode(mergedItem)).then((cid) => { 418 + current[id] = cid; 419 + }), 420 + ); 369 421 } else { 370 422 const item = mapA.get(id) ?? mapB.get(id); 371 423 ··· 375 427 } 376 428 } 377 429 } 430 + 431 + await Promise.all(cidPromises); 378 432 379 433 // New inventory 380 434 const updatedInventory = { current, removed: Array.from(allRemoved) }; ··· 399 453 400 454 /** 401 455 * @template {{ id: string; updatedAt: string }} T 402 - * @param {SignalReader<{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> } | undefined>} local 456 + * @param {{ save: (bytes: Uint8Array) => Promise<void> | void }} local 403 457 * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 404 458 * @param {SignalReader<{ container: Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }}>} container 405 459 * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} ··· 428 482 previous: c, 429 483 }); 430 484 485 + console.log("Save:", newItems); 431 486 const bytes = this.save(adjustedContainer); 432 487 433 - await local()?.save(bytes); 488 + console.log("Bytes:", bytes); 489 + await local.save(bytes); 434 490 }, 435 - state: computed(() => local()?.state() ?? "sleeping"), 491 + state: computed(() => "loaded"), 436 492 }; 437 493 } 438 494 439 - // RENDER 495 + // INDEXED-DB 496 + 497 + /** 498 + * @param {string} name 499 + */ 500 + local(name) { 501 + const s = signal(/** @type {Uint8Array | undefined} */ (undefined), { 502 + eager: true, 503 + }); 504 + 505 + this.getLocal(name).then(s.set); 506 + 507 + return s; 508 + } 509 + 510 + /** 511 + * @param {string} name 512 + * @returns {Promise<Uint8Array | undefined>} 513 + */ 514 + getLocal(name) { 515 + return IDB.get(`${IDB_PREFIX}/${this.#cat(name)}`); 516 + } 517 + 518 + /** @param {string} name; @param {Uint8Array} data */ 519 + putLocal(name, data) { 520 + return IDB.set(`${IDB_PREFIX}/${this.#cat(name)}`, data); 521 + } 440 522 441 523 /** 442 - * @param {RenderArg} _ 524 + * @param {string} name 525 + * @param {Signal<Uint8Array | undefined>} signal 443 526 */ 444 - render({ html }) { 445 - return html` 446 - <dop-indexed-db 447 - namespace="${ifDefined(this.getAttribute(`namespace`))}" 448 - ></dop-indexed-db> 449 - `; 527 + putLocalFn = 528 + (name, signal) => /** @param {Uint8Array} data */ async (data) => { 529 + signal.value = data; 530 + await this.putLocal(name, data); 531 + }; 532 + 533 + // 🛠️ 534 + 535 + get namespace() { 536 + return this.hasAttribute("namespace") 537 + ? this.getAttribute("namespace") + "/" 538 + : ""; 539 + } 540 + 541 + /** @param {string} name */ 542 + #cat(name) { 543 + return `${this.namespace}${name}`; 450 544 } 451 545 } 452 546
+4
src/themes/webamp/configurators/input/element.js
··· 240 240 * @param {string} uri 241 241 */ 242 242 async addSource(uri) { 243 + const now = new Date().toISOString(); 244 + 243 245 /** @type {Track} */ 244 246 const track = { 245 247 $type: "sh.diffuse.output.track", 246 248 id: TID.now(), 249 + createdAt: now, 250 + updatedAt: now, 247 251 kind: "placeholder", 248 252 uri, 249 253 };