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: input configurator

+287 -135
-12
src/components/configurator/common.js
··· 1 - import QS from "query-string"; 2 - 3 - /** 4 - * @param {Location} loc 5 - * @returns {Record<string, Worker>} 6 - */ 7 - export function connectionsFromQuery(loc) { 8 - const qs = QS.parse(loc.search); 9 - console.log(qs); 10 - 11 - return {}; 12 - }
+108
src/components/configurator/input/element.js
··· 1 + import { DiffuseElement, workerProxy } from "@common/element.js"; 2 + import { transfer, workerLink, workerTunnel } from "@common/worker.js"; 3 + 4 + /** 5 + * @import {ProxiedActions, Tunnel} from "@common/worker.d.ts" 6 + * @import {InputActions, InputElement} from "@components/input/types.d.ts" 7 + * @import {AdditionalActions} from "./types.d.ts" 8 + */ 9 + 10 + /** 11 + * @typedef {{ element: InputElement, tunnel: Tunnel, worker: Worker | SharedWorker }} Input 12 + */ 13 + 14 + //////////////////////////////////////////// 15 + // ELEMENT 16 + //////////////////////////////////////////// 17 + 18 + /** 19 + * @implements {ProxiedActions<InputActions>} 20 + */ 21 + class InputConfigurator extends DiffuseElement { 22 + static NAME = "diffuse/configurator/input"; 23 + static WORKER_URL = "components/configurator/input/worker.js"; 24 + 25 + constructor() { 26 + super(); 27 + 28 + /** @type {ProxiedActions<InputActions>} */ 29 + const proxy = workerProxy(this.workerLink); 30 + 31 + this.consult = proxy.consult; 32 + this.contextualize = proxy.contextualize; 33 + this.groupConsult = proxy.groupConsult; 34 + this.list = proxy.list; 35 + this.resolve = proxy.resolve; 36 + } 37 + 38 + // WORKER 39 + 40 + /** 41 + * @override 42 + */ 43 + createWorker() { 44 + const worker = super.createWorker(); 45 + this.configureWorker(worker); 46 + return worker; 47 + } 48 + 49 + // 🛠️ 50 + 51 + /** 52 + * @param {Worker | SharedWorker} worker 53 + */ 54 + async configureWorker(worker) { 55 + const inputs = await this.inputTunnels(); 56 + 57 + // Configure worker with input ports 58 + const args = transfer({ 59 + ports: Object.fromEntries(inputs.map((input) => { 60 + return [input.element.SCHEME, input.tunnel.port]; 61 + })), 62 + }, inputs.map((i) => i.tunnel.port)); 63 + 64 + /** @type {ProxiedActions<AdditionalActions>} */ 65 + const proxy = workerProxy(() => workerLink(worker)); 66 + proxy.configure(args); 67 + } 68 + 69 + async inputTunnels() { 70 + const inputElements = this.querySelectorAll(":scope > *"); 71 + const inputs = await Array.from(inputElements).reduce( 72 + /** 73 + * @param {Promise<Array<Input>>} acc 74 + * @param {Element} el 75 + */ 76 + async (acc, el) => { 77 + const rec = await acc; 78 + await customElements.whenDefined(el.localName); 79 + 80 + const element = /** @type {InputElement} */ (el); 81 + const worker = element.worker(); 82 + const tunnel = workerTunnel(worker); 83 + 84 + const item = { 85 + element, 86 + tunnel, 87 + worker, 88 + }; 89 + 90 + return [...rec, item]; 91 + }, 92 + Promise.resolve([]), 93 + ); 94 + 95 + return inputs; 96 + } 97 + } 98 + 99 + export default InputConfigurator; 100 + 101 + //////////////////////////////////////////// 102 + // REGISTER 103 + //////////////////////////////////////////// 104 + 105 + export const CLASS = InputConfigurator; 106 + export const NAME = "dc-input"; 107 + 108 + customElements.define(NAME, CLASS);
+3
src/components/configurator/input/types.d.ts
··· 1 + export type AdditionalActions = { 2 + configure: (args: { ports: { [S in string]: MessagePort } }) => void; 3 + };
+150 -109
src/components/configurator/input/worker.js
··· 1 1 import * as URI from "uri-js"; 2 2 3 3 import { groupTracksPerScheme } from "@common/index.js"; 4 - import { connectionsFromQuery } from "../common.js"; 4 + import { ostiary, rpc, workerProxy } from "@common/worker.js"; 5 5 6 6 /** 7 7 * @import {Track} from "@definitions/types.d.ts"; 8 - * @import {GroupConsult, InputActions as Actions} from "@components/input/types.d.ts" 8 + * @import {GroupConsult, InputActions} from "@components/input/types.d.ts" 9 + * @import {ProxiedActions} from "@common/worker.d.ts" 10 + * @import {AdditionalActions} from "./types.d.ts" 9 11 */ 12 + 13 + /** @type {Record<string, ProxiedActions<InputActions>>} */ 14 + const inputs = {}; 10 15 11 16 //////////////////////////////////////////// 12 - // ⚡️ 17 + // ACTIONS 13 18 //////////////////////////////////////////// 14 19 15 - // const connections = connectionsFromQuery(location); 16 - 17 - // /** 18 - // * @param {string} scheme 19 - // */ 20 - // function isSupportedScheme(scheme) { 21 - // return !!connections[scheme]; 22 - // } 20 + /** 21 + * @type {AdditionalActions["configure"]} 22 + */ 23 + export function configure({ ports }) { 24 + Object.keys(ports).forEach((key) => { 25 + inputs[key.toLowerCase()] = workerProxy(() => { 26 + const port = ports[key]; 27 + port.start(); 28 + return port; 29 + }); 30 + }); 31 + } 23 32 24 33 //////////////////////////////////////////// 25 - // ACTIONS 34 + // INPUT ACTIONS 26 35 //////////////////////////////////////////// 27 36 28 - // /** 29 - // * @type {Actions['consult']} 30 - // */ 31 - // export async function consult(fileUriOrScheme) { 32 - // const scheme = fileUriOrScheme.includes(":") 33 - // ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 34 - // : fileUriOrScheme; 37 + /** 38 + * @type {InputActions['consult']} 39 + */ 40 + export async function consult(fileUriOrScheme) { 41 + const scheme = fileUriOrScheme.includes(":") 42 + ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 43 + : fileUriOrScheme; 35 44 36 - // if (!isSupportedScheme(scheme)) { 37 - // return { supported: false, reason: "Unsupported scheme" }; 38 - // } 45 + const input = grabInput(scheme); 39 46 40 - // return await proxy(scheme, "consult")(fileUriOrScheme); 41 - // } 47 + if (!input) { 48 + return { supported: false, reason: "Unsupported scheme" }; 49 + } 42 50 43 - // /** 44 - // * @type {Actions['contextualize']} 45 - // */ 46 - // export async function contextualize(tracks) { 47 - // const groups = groupTracks(tracks); 48 - // const promises = Object.entries(groups).map( 49 - // async ([scheme, tracksGroup]) => { 50 - // if (!isSupportedScheme(scheme) || tracksGroup.length === 0) return; 51 - // return await proxy(scheme, "contextualize")(tracksGroup); 52 - // }, 53 - // ); 51 + return await input.consult(fileUriOrScheme); 52 + } 53 + 54 + /** 55 + * @type {InputActions['contextualize']} 56 + */ 57 + export async function contextualize(tracks) { 58 + const groups = groupTracks(tracks); 59 + const promises = Object.entries(groups).map( 60 + async ([scheme, tracksGroup]) => { 61 + const input = grabInput(scheme); 62 + if (!input || tracksGroup.length === 0) return; 63 + return await input.contextualize(tracksGroup); 64 + }, 65 + ); 66 + 67 + await Promise.all(promises); 68 + } 69 + 70 + /** 71 + * @type {InputActions['groupConsult']} 72 + */ 73 + export async function groupConsult(tracks) { 74 + const groups = groupTracksPerScheme(tracks); 75 + 76 + /** @type {GroupConsult[]} */ 77 + const consultations = await Promise.all( 78 + Object.keys(groups).map(async (scheme) => { 79 + const input = grabInput(scheme); 80 + 81 + if (!input) { 82 + return { 83 + [scheme]: { 84 + available: false, 85 + reason: "Unsupported scheme", 86 + tracks: groups[scheme] ?? [], 87 + }, 88 + }; 89 + } 54 90 55 - // await Promise.all(promises); 56 - // } 91 + return await input.groupConsult(groups[scheme] ?? {}); 92 + }), 93 + ); 57 94 58 - // /** 59 - // * @type {Actions['groupConsult']} 60 - // */ 61 - // export async function groupConsult(tracks) { 62 - // const groups = groupTracksPerScheme(tracks); 95 + return consultations.reduce((acc, c) => { 96 + return { ...acc, ...c }; 97 + }, {}); 98 + } 63 99 64 - // /** @type {GroupConsult[]} */ 65 - // const consultations = await Promise.all( 66 - // Object.keys(groups).map(async (scheme) => { 67 - // if (!isSupportedScheme(scheme)) { 68 - // return { 69 - // [scheme]: { 70 - // available: false, 71 - // reason: "Unsupported scheme", 72 - // tracks: groups[scheme] || [], 73 - // }, 74 - // }; 75 - // } 100 + /** 101 + * @type {InputActions['list']} 102 + */ 103 + export async function list(cachedTracks = []) { 104 + const groups = await groupConsult(cachedTracks); 105 + 106 + Object.keys(inputs).forEach((scheme) => { 107 + if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 108 + }); 76 109 77 - // return await proxy(scheme, "groupConsult")(groups[scheme] || {}); 78 - // }), 79 - // ); 110 + const promises = Object.entries(groups).map( 111 + async ([scheme, { available, tracks }]) => { 112 + if (!available) return tracks; 80 113 81 - // return consultations.reduce((acc, c) => { 82 - // return { ...acc, ...c }; 83 - // }, {}); 84 - // } 114 + const input = grabInput(scheme); 115 + if (!input) return tracks; 116 + return await input.list(tracks); 117 + }, 118 + ); 85 119 86 - // /** 87 - // * @type {Actions['list']} 88 - // */ 89 - // export async function list(cachedTracks = []) { 90 - // const groups = await groupConsult(cachedTracks); 120 + const nested = await Promise.all(promises); 121 + const tracks = nested.flat(1); 91 122 92 - // Object.keys(connections).forEach((scheme) => { 93 - // if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 94 - // }); 123 + return tracks; 124 + } 95 125 96 - // const promises = Object.entries(groups).map( 97 - // async ([scheme, { available, tracks }]) => { 98 - // if (!available) return tracks; 99 - // if (!isSupportedScheme(scheme)) return tracks; 100 - // return await proxy(scheme, "list")(tracks); 101 - // }, 102 - // ); 126 + /** 127 + * @type {InputActions['resolve']} 128 + */ 129 + export async function resolve(args) { 130 + const scheme = args.uri.split(":", 1)[0]; 131 + const input = grabInput(scheme); 132 + if (!input) return undefined; 103 133 104 - // const nested = await Promise.all(promises); 105 - // const tracks = nested.flat(1); 134 + try { 135 + return await input.resolve(args); 136 + } catch (err) { 137 + console.error( 138 + `[configurator/input] Resolve error for scheme '${scheme}'.`, 139 + err, 140 + ); 141 + } 142 + } 106 143 107 - // return tracks; 108 - // } 144 + //////////////////////////////////////////// 145 + // ⚡️ 146 + //////////////////////////////////////////// 109 147 110 - // /** 111 - // * @type {Actions['resolve']} 112 - // */ 113 - // export async function resolve(args) { 114 - // const scheme = args.uri.split(":", 1)[0]; 115 - // if (!isSupportedScheme(scheme)) return undefined; 148 + ostiary((context) => { 149 + rpc(context, { 150 + consult, 151 + contextualize, 152 + groupConsult, 153 + list, 154 + resolve, 116 155 117 - // try { 118 - // return await proxy(scheme, "resolve")(args); 119 - // } catch (err) { 120 - // console.error( 121 - // `[configurator/input] Resolve error for scheme '${scheme}'.`, 122 - // err, 123 - // ); 124 - // } 125 - // } 156 + // Additional 157 + configure, 158 + }); 159 + }); 126 160 127 161 //////////////////////////////////////////// 128 162 // 🛠️ 129 163 //////////////////////////////////////////// 130 164 131 - // /** 132 - // * @param {Track[]} tracks 133 - // */ 134 - // function groupTracks(tracks) { 135 - // const grouped = groupTracksPerScheme( 136 - // tracks, 137 - // Object.fromEntries( 138 - // Object.entries(connections).map(([k, _v]) => { 139 - // return [k, []]; 140 - // }), 141 - // ), 142 - // ); 165 + /** 166 + * @param {string} scheme 167 + */ 168 + function grabInput(scheme) { 169 + return inputs[scheme.toLowerCase()]; 170 + } 143 171 144 - // return grouped; 145 - // } 172 + /** 173 + * @param {Track[]} tracks 174 + */ 175 + function groupTracks(tracks) { 176 + const grouped = groupTracksPerScheme( 177 + tracks, 178 + Object.fromEntries( 179 + Object.keys(inputs).map((k) => { 180 + return [k, []]; 181 + }), 182 + ), 183 + ); 184 + 185 + return grouped; 186 + }
+5 -1
src/components/input/opensubsonic/element.js
··· 1 1 import { DiffuseElement, workerProxy } from "@common/element.js"; 2 + import { SCHEME } from "./constants.js"; 2 3 3 4 /** 4 - * @import {InputActions} from "@components/input/types.d.ts" 5 + * @import {InputActions, InputSchemeProvider} from "@components/input/types.d.ts" 5 6 * @import {ProxiedActions} from "@common/worker.d.ts" 6 7 */ 7 8 ··· 11 12 12 13 /** 13 14 * @implements {ProxiedActions<InputActions>} 15 + * @implements {InputSchemeProvider} 14 16 */ 15 17 class OpensubsonicInput extends DiffuseElement { 16 18 static NAME = "diffuse/input/opensubsonic"; 17 19 static WORKER_URL = "components/input/opensubsonic/worker.js"; 20 + 21 + SCHEME = SCHEME; 18 22 19 23 constructor() { 20 24 super();
+5 -1
src/components/input/s3/element.js
··· 1 1 import { DiffuseElement, workerProxy } from "@common/element.js"; 2 + import { SCHEME } from "./constants.js"; 2 3 3 4 /** 4 - * @import {InputActions} from "@components/input/types.d.ts" 5 + * @import {InputActions, InputSchemeProvider} from "@components/input/types.d.ts" 5 6 * @import {ProxiedActions} from "@common/worker.d.ts" 6 7 */ 7 8 ··· 11 12 12 13 /** 13 14 * @implements {ProxiedActions<InputActions>} 15 + * @implements {InputSchemeProvider} 14 16 */ 15 17 class S3Input extends DiffuseElement { 16 18 static NAME = "diffuse/input/s3"; 17 19 static WORKER_URL = "components/input/s3/worker.js"; 20 + 21 + SCHEME = SCHEME; 18 22 19 23 constructor() { 20 24 super();
+3
src/components/input/types.d.ts
··· 31 31 32 32 export type InputElement = 33 33 & DiffuseElement 34 + & InputSchemeProvider 34 35 & ProxiedActions<InputActions>; 36 + 37 + export type InputSchemeProvider = { SCHEME: string }; 35 38 36 39 export type ResolvedUri = undefined | { 37 40 stream: ReadableStream;
+7 -6
src/index.vto
··· 17 17 # ELEMENTS 18 18 19 19 configurators: 20 - - title: "Input" 20 + - url: "components/configurator/input/element.js" 21 + title: "Input" 21 22 desc: "Add multiple inputs." 22 - todo: true 23 - - title: "Output" 23 + - url: "components/configurator/output/element.js" 24 + title: "Output" 24 25 desc: "Allows the user to configure a specific output." 25 26 todo: true 26 - - title: "Scrobbles" 27 + - url: "components/configurator/scrobbles/element.js" 28 + title: "Scrobbles" 27 29 desc: "Configure multiple scrobblers (music trackers)." 28 30 todo: true 29 31 ··· 42 44 - url: "components/input/s3/element.js" 43 45 title: "S3" 44 46 desc: "AWS S3 and services that provide the same surface API such as Cloudflare R2." 45 - todo: true 46 47 47 48 orchestrators: 48 49 - url: "components/orchestrator/process-tracks/element.js" ··· 203 204 title: "Output", 204 205 items: output, 205 206 content: ` 206 - Output is application-derived data such as playlists. These elements can receive such data and keep it around. These are categorised by the type of data they ingest, or many types in the case of polymorphic. 207 + Output is application-derived data such as playlists. These elements can receive such data and keep it around. These are categorised by the type of data they ingest, or many types in the case of polymorphic. Optionally use transformers to convert output into the expected format. 207 208 ` 208 209 }) }} 209 210
+4 -2
src/themes/webamp/index.js
··· 1 - import "@components/orchestrator/process-tracks/element.js"; 1 + // import "@components/orchestrator/process-tracks/element.js"; 2 2 import "@components/orchestrator/queue-tracks/element.js"; 3 + import "@components/input/opensubsonic/element.js"; 4 + import "@components/input/s3/element.js"; 3 5 import "@components/output/polymorphic/indexed-db/element.js"; 4 6 import "@components/processor/metadata/element.js"; 5 7 import "@components/transformer/output/string/json/element.js"; 6 8 import "@components/transformer/output/refiner/default/element.js"; 7 9 8 - import * as Input from "@components/input/opensubsonic/element.js"; 10 + import * as Input from "@components/configurator/input/element.js"; 9 11 import * as Queue from "@components/engine/queue/element.js"; 10 12 11 13 import { component } from "@common/element.js";
+2 -4
src/themes/webamp/index.vto
··· 81 81 <dop-indexed-db></dop-indexed-db> 82 82 <dp-metadata></dp-metadata> 83 83 84 - <di-opensubsonic id="input"></di-opensubsonic> 85 - 86 - <!--<dc-input id="input"> 84 + <dc-input id="input"> 87 85 <di-opensubsonic></di-opensubsonic> 88 86 <di-s3></di-s3> 89 - </dc-input>--> 87 + </dc-input> 90 88 91 89 <!-- Transformers --> 92 90 <dtor-default id="output" output-selector="dtos-json"></dtor-default>