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.

feat: automerge transformer

+181 -14
+9 -2
_config.ts
··· 1 1 import { dotenvRun } from "@dotenv-run/esbuild"; 2 - import { builtinModules } from "node:module"; 3 2 import lume from "lume/mod.ts"; 4 3 5 4 import esbuild from "lume/plugins/esbuild.ts"; ··· 11 10 import { ensureDirSync } from "@std/fs/ensure-dir"; 12 11 import { walkSync } from "@std/fs/walk"; 13 12 import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; 13 + import { wasmLoader } from "esbuild-plugin-wasm"; 14 14 15 15 const site = lume({ 16 16 dest: "./dist", ··· 27 27 site.use(esbuild({ 28 28 extensions: [".js"], 29 29 options: { 30 + alias: { 31 + "@automerge/automerge": "https://esm.sh/@automerge/automerge@^3.2.3", 32 + }, 30 33 bundle: true, 34 + format: "esm", 31 35 minify: false, 32 36 // outExtension: { ".js": ".min.js" }, 37 + platform: "browser", 33 38 plugins: [ 34 - nodeModulesPolyfillPlugin(), 35 39 dotenvRun({ 36 40 files: [".env"], 37 41 }), 42 + nodeModulesPolyfillPlugin(), 43 + wasmLoader(), 38 44 ], 39 45 splitting: true, 46 + target: "esnext", 40 47 }, 41 48 })); 42 49
+3 -1
deno.jsonc
··· 5 5 "imports": { 6 6 "98.css": "npm:98.css@^0.1.21", 7 7 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.6", 8 + "@automerge/automerge": "npm:@automerge/automerge@^3.2.3", 8 9 "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.4", 9 10 "@fry69/deep-diff": "jsr:@fry69/deep-diff@^0.1.10", 10 11 "@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1", ··· 40 41 "@dotenv-run/esbuild": "npm:@dotenv-run/esbuild@^1.5.1", 41 42 "@styles/": "./src/styles/", 42 43 "@themes/": "./src/themes/", 43 - "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", 44 44 45 45 // Build 46 46 "@atcute/lex-cli": "npm:@atcute/lex-cli@^2.3.1", 47 47 "@std/fs": "jsr:@std/fs@^1.0.19", 48 48 "@std/path": "jsr:@std/path@^1.1.2", 49 + "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", 50 + "esbuild-plugin-wasm": "npm:esbuild-plugin-wasm@^1.1.0", 49 51 "lume/": "https://cdn.jsdelivr.net/gh/lumeland/lume@3.1.4/", 50 52 "lume/jsx-runtime": "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/jsx-runtime.ts", 51 53 },
+33
src/common/utils.js
··· 88 88 } 89 89 90 90 /** 91 + * @template {Record<string, any>} T 92 + * @param {T} rec 93 + */ 94 + export function removeUndefinedValuesFromRecord(rec) { 95 + const recClone = { ...rec }; 96 + 97 + Object.entries(recClone).forEach(([key, value]) => { 98 + if (value === undefined) { 99 + delete recClone[key]; 100 + } 101 + }); 102 + 103 + return recClone; 104 + } 105 + 106 + /** 107 + * @template {Record<string, any>} T 108 + * @param {T} rec 109 + */ 110 + export function recursivelyCloneRecords(rec) { 111 + const recClone = { ...rec }; 112 + 113 + Object.entries(recClone).forEach(([key, value]) => { 114 + if (typeof value === "object") { 115 + /** @ts-ignore */ 116 + recClone[key] = recursivelyCloneRecords(value); 117 + } 118 + }); 119 + 120 + return recClone; 121 + } 122 + 123 + /** 91 124 * @param {Track} track 92 125 * @returns {Promise<string>} 93 126 */
+5 -4
src/components/input/opensubsonic/worker.js
··· 2 2 import { ostiary, rpc } from "@common/worker.js"; 3 3 4 4 import { SCHEME } from "./constants.js"; 5 + import { removeUndefinedValuesFromRecord } from "@common/utils.js"; 5 6 import { detach as detachUtil, groupKeyHash } from "../common.js"; 6 7 import { 7 8 autoTypeToTrackKind, ··· 156 157 kind: autoTypeToTrackKind(song.type), 157 158 uri: buildURI(server, { songId: song.id, path }), 158 159 159 - stats: { 160 + stats: removeUndefinedValuesFromRecord({ 160 161 albumGain: undefined, 161 162 bitrate: song.bitRate ? song.bitRate * 1000 : undefined, 162 163 bitsPerSample: undefined, ··· 167 168 numberOfChannels: undefined, 168 169 sampleRate: undefined, 169 170 trackGain: undefined, 170 - }, 171 - tags: { 171 + }), 172 + tags: removeUndefinedValuesFromRecord({ 172 173 album: song.album, 173 174 albumartist: song.albumArtists?.[0]?.name, 174 175 albumartists: song.albumArtists?.map((a) => a.name), ··· 223 224 work: undefined, 224 225 writers: undefined, 225 226 year: song.year, 226 - }, 227 + }), 227 228 }; 228 229 229 230 return track;
+18 -2
src/components/orchestrator/output/element.js
··· 1 - import { ifDefined } from "lit-html/directives/if-defined.js" 1 + import { ifDefined } from "lit-html/directives/if-defined.js"; 2 2 import { DEFAULT_GROUP, DiffuseElement } from "@common/element.js"; 3 3 4 4 import "@components/configurator/output/element.js"; 5 5 import "@components/output/polymorphic/indexed-db/element.js"; 6 + // import "@components/transformer/output/bytes/automerge/element.js"; 6 7 import "@components/transformer/output/refiner/default/element.js"; 7 8 import "@components/transformer/output/string/json/element.js"; 8 9 ··· 16 17 // ELEMENT 17 18 //////////////////////////////////////////// 18 19 20 + /** 21 + * A default setup for managing output. 22 + */ 19 23 class OutputOrchestrator extends DiffuseElement { 20 24 static NAME = "diffuse/orchestrator/output"; 21 25 ··· 42 46 * @param {RenderArg} _ 43 47 */ 44 48 render({ html }) { 45 - const group = this.group === DEFAULT_GROUP ? undefined : this.group 49 + const group = this.group === DEFAULT_GROUP ? undefined : this.group; 46 50 47 51 return html` 52 + <!--<dop-indexed-db 53 + id="do-output__dop-indexed-db__bytes--automerge" 54 + group="${ifDefined(group)}" 55 + namespace="bytes/automerge" 56 + ></dop-indexed-db>--> 57 + 48 58 <dop-indexed-db 49 59 id="do-output__dop-indexed-db__json" 50 60 group="${ifDefined(group)}" ··· 56 66 id="do-output__dtos-json" 57 67 output-selector="#do-output__dop-indexed-db__json" 58 68 ></dtos-json> 69 + 70 + <!--<dtob-automerge 71 + id="do-output__dtob-automerge" 72 + output-selector="#do-output__dop-indexed-db__bytes--automerge" 73 + ></dtob-automerge>--> 59 74 </dc-output> 60 75 76 + <!-- Entry --> 61 77 <dtor-default 62 78 id="do-output__output" 63 79 output-selector="#do-output__dc-output"
+9 -4
src/components/processor/metadata/common.js
··· 3 3 import { HttpClient } from "@tokenizer/http"; 4 4 import { tokenizer as rangeTokenizer } from "@tokenizer/range"; 5 5 6 + import { removeUndefinedValuesFromRecord } from "@common/utils.js"; 7 + 6 8 /** 7 9 * @import { TrackStats, TrackTags } from "@definitions/types.d.ts"; 8 10 * @import { Extraction, Urls } from "./types.d.ts"; ··· 54 56 } 55 57 56 58 /** @type {TrackStats} */ 57 - const stats = { 59 + const statsFull = { 58 60 albumGain: meta.format.albumGain, 59 61 bitrate: meta.format.bitrate, 60 62 bitsPerSample: meta.format.bitsPerSample, ··· 68 70 }; 69 71 70 72 /** @type {TrackTags} */ 71 - const tags = { 73 + const tagsFull = { 72 74 album: meta.common.album, 73 75 albumartist: meta.common.albumartist, 74 76 albumartists: Array.isArray(meta.common.albumartist) ··· 93 95 date: meta.common.date, 94 96 disc: { 95 97 no: meta.common.disk.no || 1, 96 - of: meta.common.disk.of ?? undefined, 98 + ...(meta.common.disk.of && { of: meta.common.disk.of }), 97 99 }, 98 100 djmixers: meta.common.djmixer, 99 101 engineers: meta.common.engineer, ··· 128 130 titlesort: meta.common.titlesort, 129 131 track: { 130 132 no: meta.common.track.no || 1, 131 - of: meta.common.track.of ?? undefined, 133 + ...(meta.common.track.of && { of: meta.common.track.of }), 132 134 }, 133 135 work: meta.common.work, 134 136 writers: meta.common.writer, 135 137 year: meta.common.year, 136 138 }; 139 + 140 + const stats = removeUndefinedValuesFromRecord(statsFull); 141 + const tags = removeUndefinedValuesFromRecord(tagsFull); 137 142 138 143 return { 139 144 artwork: includeArtwork ? meta.common.picture : undefined,
+13
src/components/transformer/output/bytes/automerge/constants.js
··· 1 + import * as Automerge from "@automerge/automerge"; 2 + import { base64 } from "iso-base/rfc4648"; 3 + 4 + /** 5 + * @import { TracksDocument } from "./types.d.ts"; 6 + */ 7 + 8 + /** @type {Automerge.Doc<TracksDocument>} */ 9 + export const INITIAL_TRACKS_DOCUMENT = Automerge.load( 10 + base64.decode( 11 + "hW9Kg3QEcPYAeAEQhsIBj6DgCDtXSHEiZhcqigHxj0/xVpP8KdUJQ8e6qVEgaz7v6CpLuCGB58iHmx4plQYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf9Xbz8sGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA", 12 + ), 13 + );
+73
src/components/transformer/output/bytes/automerge/element.js
··· 1 + import * as Automerge from "@automerge/automerge"; 2 + import { isUint8Array } from "iso-base/utils"; 3 + 4 + import { computed } from "@common/signal.js"; 5 + import { OutputTransformer } from "../../base.js"; 6 + import { INITIAL_TRACKS_DOCUMENT } from "./constants.js"; 7 + import { recursivelyCloneRecords } from "@toko/diffuse/common/utils.js"; 8 + 9 + /** 10 + * @import { SignalReader } from "@common/signal.d.ts"; 11 + * @import { OutputManagerDeputy } from "@components/output/types.d.ts" 12 + * @import { Track } from "@definitions/types.d.ts" 13 + * @import { TracksDocument } from "./types.d.ts" 14 + */ 15 + 16 + /** 17 + * @extends {OutputTransformer<Uint8Array>} 18 + */ 19 + class AutomergeBytesOutputTransformer extends OutputTransformer { 20 + constructor() { 21 + super(); 22 + 23 + const base = this.base(); 24 + 25 + /** @type {SignalReader<Automerge.Doc<TracksDocument>>} */ 26 + const document = computed(() => { 27 + const value = base.tracks.collection(); 28 + 29 + if (isUint8Array(value)) { 30 + return Automerge.load(value); 31 + } else if (value == undefined) { 32 + return INITIAL_TRACKS_DOCUMENT; 33 + } else { 34 + // TODO: Better error 35 + throw new Error("Invalid data type"); 36 + } 37 + }); 38 + 39 + /** @type {OutputManagerDeputy<Track[]>} */ 40 + const manager = { 41 + tracks: { 42 + ...base.tracks, 43 + collection: computed(() => document().collection), 44 + save: async (newTracks) => { 45 + const doc = Automerge.change(document(), (d) => { 46 + const clonedCollection = newTracks.map((track) => { 47 + return recursivelyCloneRecords(track); 48 + }); 49 + 50 + d.collection = clonedCollection; 51 + }); 52 + 53 + const bytes = Automerge.save(doc); 54 + await base.tracks.save(bytes); 55 + }, 56 + }, 57 + }; 58 + 59 + // Assign manager properties to class 60 + this.tracks = manager.tracks; 61 + } 62 + } 63 + 64 + export default AutomergeBytesOutputTransformer; 65 + 66 + //////////////////////////////////////////// 67 + // REGISTER 68 + //////////////////////////////////////////// 69 + 70 + export const CLASS = AutomergeBytesOutputTransformer; 71 + export const NAME = "dtob-automerge"; 72 + 73 + customElements.define(NAME, CLASS);
+3
src/components/transformer/output/bytes/automerge/types.d.ts
··· 1 + import type { Track } from "@definitions/types.d.ts"; 2 + 3 + export type TracksDocument = { collection: Track[] };
+14
src/components/transformer/output/bytes/automerge/utils.js
··· 1 + import * as Automerge from "@automerge/automerge"; 2 + import { base64 } from "iso-base/rfc4648"; 3 + 4 + /** 5 + * Generate a new tracks document to put in the `INITIAL_TRACKS_DOCUMENT` constant. 6 + */ 7 + export function initTracksDoc() { 8 + const doc = Automerge.change(Automerge.init(), (doc) => { 9 + doc.collection = []; 10 + }); 11 + 12 + const bytes = Automerge.save(doc); 13 + return base64.encode(bytes); 14 + }
+1 -1
src/index.vto
··· 156 156 transformers: 157 157 - title: "Output / Bytes / Automerge" 158 158 desc: "Translate data to and from an Automerge CRDT." 159 - todo: true 159 + url: "components/transformer/output/bytes/automerge/element.js" 160 160 - title: "Output / Bytes / Cambria lenses" 161 161 desc: "Uses the Cambria library to seamlessly translate between data schemas so that no data migration is needed." 162 162 todo: true