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: finish dasl-sync transformer

+218 -89
+12
src/common/utils.js
··· 138 138 } 139 139 140 140 /** 141 + * @param {string} str 142 + * @returns {string} 143 + */ 144 + export function safeDecodeURIComponent(str) { 145 + try { 146 + return decodeURIComponent(str); 147 + } catch { 148 + return str; 149 + } 150 + } 151 + 152 + /** 141 153 * @param {Track} track 142 154 * @returns {Promise<string>} 143 155 */
+4 -3
src/components/input/opensubsonic/common.js
··· 3 3 4 4 import { SCHEME } from "./constants.js"; 5 5 import { cachedConsult } from "@components/input/common.js"; 6 + import { safeDecodeURIComponent } from "@common/utils.js"; 6 7 import { SubsonicAPIWithoutFetch } from "./class.js"; 7 8 8 9 /** ··· 69 70 export function createClient(server) { 70 71 return new SubsonicAPIWithoutFetch({ 71 72 url: `http${server.tls ? "s" : ""}://${server.host}`, 72 - auth: server.apiKey ? { apiKey: decodeURIComponent(server.apiKey) } : { 73 - username: decodeURIComponent(server.username || ""), 74 - password: decodeURIComponent(server.password || ""), 73 + auth: server.apiKey ? { apiKey: safeDecodeURIComponent(server.apiKey) } : { 74 + username: safeDecodeURIComponent(server.username || ""), 75 + password: safeDecodeURIComponent(server.password || ""), 75 76 }, 76 77 }); 77 78 }
+5 -3
src/components/input/opensubsonic/worker.js
··· 1 - import * as URI from "fast-uri"; 2 1 import * as TID from "@atcute/tid"; 3 2 import { ostiary, rpc } from "@common/worker.js"; 4 3 5 4 import { SCHEME } from "./constants.js"; 6 - import { removeUndefinedValuesFromRecord } from "@common/utils.js"; 5 + import { 6 + removeUndefinedValuesFromRecord, 7 + safeDecodeURIComponent, 8 + } from "@common/utils.js"; 7 9 import { detach as detachUtil, groupKey } from "../common.js"; 8 10 import { 9 11 autoTypeToTrackKind, ··· 111 113 servers[sid] = parsed.server; 112 114 113 115 cache[sid] ??= {}; 114 - cache[sid][decodeURIComponent(parsed.path)] = t; 116 + cache[sid][safeDecodeURIComponent(parsed.path)] = t; 115 117 }); 116 118 117 119 /**
+2 -1
src/components/input/s3/common.js
··· 4 4 import QS from "query-string"; 5 5 6 6 import { cachedConsult } from "@components/input/common.js"; 7 + import { safeDecodeURIComponent } from "@common/utils.js"; 7 8 import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants.js"; 8 9 9 10 /** ··· 174 175 }; 175 176 176 177 const path = 177 - (bucket.path.replace(/\/$/, "") + decodeURIComponent(uri.path || "")) 178 + (bucket.path.replace(/\/$/, "") + safeDecodeURIComponent(uri.path || "")) 178 179 .replace( 179 180 /^\//, 180 181 "",
+3 -3
src/components/orchestrator/output/element.js
··· 54 54 } 55 55 case "do-output__dc-output__s3": { 56 56 import("@components/output/bytes/s3/element.js"); 57 - import("@components/transformer/output/bytes/automerge/element.js"); 57 + import("@components/transformer/output/bytes/dasl-sync/element.js"); 58 58 break; 59 59 } 60 60 } ··· 176 176 ></dtor-atproto-sync> 177 177 178 178 <!-- S3 --> 179 - <dtob-automerge 179 + <dtob-dasl-sync 180 180 id="do-output__dc-output__s3" 181 181 namespace="s3" 182 182 output-selector="#do-output__dob-s3" 183 183 label="S3" 184 - ></dtob-automerge> 184 + ></dtob-dasl-sync> 185 185 </dc-output> 186 186 187 187 <!-- REFINER -->
+7 -6
src/components/orchestrator/scoped-tracks/element.js
··· 5 5 } from "@common/element.js"; 6 6 import { batch, computed, signal } from "@common/signal.js"; 7 7 import { filterByPlaylist } from "@common/playlist.js"; 8 - import { trackURIBase, uniqueTrackURIs } from "@common/track.js"; 9 8 10 9 /** 11 10 * @import {Track} from "@definitions/types.d.ts" ··· 96 95 actions.getTracksAvailable(), 97 96 actions.getTracksSearch(), 98 97 actions.getTracksFinal(), 99 - ]).then(([available, search, final]) => batch(() => { 100 - this.#tracksAvailable.value = available; 101 - this.#tracksSearch.value = search; 102 - this.#tracksFinal.value = final; 103 - })); 98 + ]).then(([available, search, final]) => 99 + batch(() => { 100 + this.#tracksAvailable.value = available; 101 + this.#tracksSearch.value = search; 102 + this.#tracksFinal.value = final; 103 + }) 104 + ); 104 105 } 105 106 106 107 // Super
+185 -71
src/components/transformer/output/bytes/dasl-sync/element.js
··· 31 31 * @template {{ id: string; updatedAt: string }} T 32 32 * @param {SignalReader<Uint8Array | undefined>} localCollection 33 33 * @param {SignalReader<Uint8Array | undefined>} remoteCollection 34 - * @returns {SignalReader<{ container: Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T>>; promise: Promise<Container<T>> } }; diverged: boolean; local: boolean; remote: boolean; }>} 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 35 */ 36 36 const state = (localCollection, remoteCollection) => { 37 37 return computed(() => { ··· 70 70 /** @type {Container<T> | undefined} */ (undefined), 71 71 ); 72 72 73 - let promise; 73 + /** 74 + * @type {Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }} 75 + */ 76 + let container = r; 74 77 75 78 if (diverged.local || diverged.remote) { 76 - promise = this.merge(l, r).then(mergedSignal.set).then( 77 - mergedSignal.get, 78 - ); 79 + const promise = this.merge(l, r).then((c) => { 80 + mergedSignal.set(c); 81 + return c; 82 + }); 83 + 84 + container = { 85 + local: l, 86 + merged: { promise, signal: mergedSignal.get }, 87 + }; 79 88 } 80 89 81 90 return { 82 - container: diverged.local || diverged.remote 83 - ? { local: l, merged: { promise, signal: mergedSignal.get } } 84 - : r, 91 + container, 85 92 diverged: diverged.local || diverged.remote, 86 93 local: diverged.local, 87 94 remote: diverged.remote, ··· 89 96 }); 90 97 }; 91 98 99 + // Container signals 92 100 const facets = state( 93 101 computed(() => local()?.facets?.collection()), 94 102 remote.facets.collection, ··· 109 117 remote.tracks.collection, 110 118 ); 111 119 112 - this.facets = { 113 - collection: computed(() => { 114 - const container = facets().container; 120 + this.facets = this.managerProp( 121 + computed(() => local()?.facets), 122 + remote.facets, 123 + facets, 124 + ); 115 125 116 - if ("merged" in container) { 117 - return container.merged.signal() ?? container.local; 118 - } 126 + this.playlistItems = this.managerProp( 127 + computed(() => local()?.playlistItems), 128 + remote.playlistItems, 129 + playlistItems, 130 + ); 119 131 120 - return container; 121 - }), 122 - reload: remote.facets.reload, 132 + this.themes = this.managerProp( 133 + computed(() => local()?.themes), 134 + remote.themes, 135 + themes, 136 + ); 123 137 124 - /** @param {Facet[]} data */ 125 - save: async (data) => { 126 - let container = facets().container; 138 + this.tracks = this.managerProp( 139 + computed(() => local()?.tracks), 140 + remote.tracks, 141 + tracks, 142 + ); 127 143 128 - if ("merged" in container) { 129 - container = await container.merged.promise; 130 - } 144 + this.ready = () => true; 131 145 132 - container; 133 - }, 134 - }; 146 + // Effects 147 + this.effect(() => { 148 + const l = local(); 149 + if (!l) return; 135 150 136 - this.playlistItems = undefined; 137 - this.themes = undefined; 138 - this.tracks = undefined; 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 + }); 139 164 140 - this.ready = () => true; 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 + }); 141 178 142 - // Effects 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 + }); 143 207 } 144 208 145 209 // SIGNALS ··· 166 230 }); 167 231 } 168 232 169 - // 🛠️ 233 + // DATA FUNCTIONS 234 + 235 + /** 236 + * @template {{ id: string; updatedAt: string }} T 237 + * @param {{ previous: Container<T>, collection: T[] }} _ 238 + * @returns {Promise<Container<T>>} 239 + */ 240 + async updateContainer({ previous, collection }) { 241 + const inventory = previous.inventory; 242 + 243 + const collIds = collection.map(({ id }) => id); 244 + 245 + const currSet = new Set(Object.keys(inventory.current)); 246 + const collSet = new Set(collIds); 247 + 248 + const newSet = collSet.difference(currSet); 249 + const remSet = currSet.difference(collSet); 250 + 251 + const alreadyRemoved = new Set(inventory.removed); 252 + const allRemoved = alreadyRemoved.union(remSet); 253 + 254 + /** @type {Record<string, string>} */ 255 + const current = { ...inventory.current }; 256 + 257 + /** @type Promise<void>[] */ 258 + const promises = []; 259 + 260 + collection.forEach((a) => { 261 + if (!newSet.has(a.id)) return; 262 + 263 + // Item is new, calculate CID and add it to the `current` dictionary 264 + const encoded = encode(a); 265 + 266 + promises.push((async () => { 267 + const cid = await CID.create(0x71, encoded); 268 + current[a.id] = cid; 269 + })()); 270 + }); 271 + 272 + await Promise.all(promises); 273 + 274 + const newInventory = { 275 + current, 276 + removed: Array.from(allRemoved), 277 + }; 278 + 279 + return { 280 + // TODO: Do we need this? Too big of a perf penalty? 281 + cid: await CID.create(0x71, encode(newInventory)), 282 + data: collection, 283 + inventory: newInventory, 284 + }; 285 + } 170 286 171 287 /** 172 288 * @template {{ id: string; updatedAt: string }} T ··· 272 388 273 389 /** 274 390 * @template {{ id: string; updatedAt: string }} T 275 - * @param {{ previous: Container<T> | undefined, collection: T[] }} _ 276 - * @returns {Promise<Container<T>>} 391 + * @param {Container<T>} container 392 + * @returns {Uint8Array} 277 393 */ 278 - async save({ previous, collection }) { 279 - const inventory = previous?.inventory ?? { current: {}, removed: [] }; 280 - 281 - const collIds = collection.map(({ id }) => id); 282 - 283 - const currSet = new Set(Object.keys(inventory.current)); 284 - const collSet = new Set(collIds); 285 - 286 - const newSet = collSet.difference(currSet); 287 - const remSet = currSet.difference(collSet); 288 - 289 - const alreadyRemoved = new Set(inventory.removed); 290 - const allRemoved = alreadyRemoved.union(remSet); 394 + save(container) { 395 + return encode(container); 396 + } 291 397 292 - /** @type {Record<string, string>} */ 293 - const current = { ...inventory.current }; 398 + // OUTPUT MANAGER FUNCTIONS 294 399 295 - /** @type Promise<void>[] */ 296 - const promises = []; 400 + /** 401 + * @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 403 + * @param {{ collection: SignalReader<Uint8Array | undefined>, reload: () => Promise<void>, save: (bytes: Uint8Array) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} remote 404 + * @param {SignalReader<{ container: Container<T> | { local: Container<T>; merged: { signal: SignalReader<Container<T> | undefined>; promise: Promise<Container<T>> } }}>} container 405 + * @returns {{ collection: SignalReader<T[]>, reload: () => Promise<void>, save: (items: T[]) => Promise<void>, state: SignalReader<"loading" | "loaded" | "sleeping"> }} 406 + */ 407 + managerProp(local, remote, container) { 408 + return { 409 + collection: computed(() => { 410 + const c = container().container; 297 411 298 - collection.forEach((a) => { 299 - if (!newSet.has(a.id)) return; 412 + if ("merged" in c) { 413 + return c.merged.signal()?.data ?? c.local?.data; 414 + } 300 415 301 - // Item is new, calculate CID and add it to the `current` dictionary 302 - const encoded = encode(a); 416 + return c.data; 417 + }), 418 + reload: remote.reload, 419 + save: async (/** @type {T[]} */ newItems) => { 420 + let c = container().container; 303 421 304 - promises.push((async () => { 305 - const cid = await CID.create(0x71, encoded); 306 - current[a.id] = cid; 307 - })()); 308 - }); 422 + if ("merged" in c) { 423 + c = await c.merged.promise; 424 + } 309 425 310 - await Promise.all(promises); 426 + const adjustedContainer = await this.updateContainer({ 427 + collection: newItems, 428 + previous: c, 429 + }); 311 430 312 - const newInventory = { 313 - current, 314 - removed: Array.from(allRemoved), 315 - }; 431 + const bytes = this.save(adjustedContainer); 316 432 317 - return { 318 - // TODO: Do we need this? Too big of a perf penalty? 319 - cid: await CID.create(0x71, encode(newInventory)), 320 - data: collection, 321 - inventory: newInventory, 433 + await local()?.save(bytes); 434 + }, 435 + state: computed(() => local()?.state() ?? "sleeping"), 322 436 }; 323 437 } 324 438 ··· 343 457 //////////////////////////////////////////// 344 458 345 459 export const CLASS = DaslBytesSyncOutputTransformer; 346 - export const NAME = "dtob-automerge"; 460 + export const NAME = "dtob-dasl-sync"; 347 461 348 462 customElements.define(NAME, CLASS);
-2
src/themes/webamp/browser/element.js
··· 228 228 const playlist = this.$scope.value?.playlist(); 229 229 const searchTerm = this.$scope.value?.searchTerm() ?? ""; 230 230 231 - console.log(tracks, this.$provider.value) 232 - 233 231 // Virtual list 234 232 const totalTracks = tracks.length; 235 233 const { startIndex, endIndex: rawEnd } = this.#computeWindow();