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

feat: add artwork/input component

+141
+61
src/components/artwork/input/element.js
··· 1 + import { DiffuseElement, query } from "~/common/element.js"; 2 + 3 + /** 4 + * @import {ProxiedActions} from "~/common/worker.d.ts" 5 + * @import {InputElement} from "~/components/input/types.d.ts" 6 + * @import {Actions} from "~/components/artwork/types.d.ts" 7 + */ 8 + 9 + //////////////////////////////////////////// 10 + // ELEMENT 11 + //////////////////////////////////////////// 12 + 13 + /** 14 + * @implements {ProxiedActions<Actions>} 15 + */ 16 + class InputArtwork extends DiffuseElement { 17 + static NAME = "diffuse/artwork/input"; 18 + static WORKER_URL = "components/artwork/input/worker.js"; 19 + 20 + constructor() { 21 + super(); 22 + 23 + /** @type {ProxiedActions<Actions>} */ 24 + const p = this.workerProxy(); 25 + 26 + this.get = p.get; 27 + } 28 + 29 + // LIFECYCLE 30 + 31 + /** @override */ 32 + async connectedCallback() { 33 + super.connectedCallback(); 34 + 35 + /** @type {InputElement} */ 36 + this.input = query(this, "input-selector"); 37 + 38 + await customElements.whenDefined(this.input.localName); 39 + } 40 + 41 + // WORKERS 42 + 43 + /** 44 + * @override 45 + */ 46 + dependencies() { 47 + if (!this.input) throw new Error("Input element not defined yet"); 48 + return { input: this.input }; 49 + } 50 + } 51 + 52 + export default InputArtwork; 53 + 54 + //////////////////////////////////////////// 55 + // REGISTER 56 + //////////////////////////////////////////// 57 + 58 + export const CLASS = InputArtwork; 59 + export const NAME = "da-input"; 60 + 61 + customElements.define(NAME, InputArtwork);
+32
src/components/artwork/input/worker.js
··· 1 + import { ostiary, rpc, workerProxy } from "~/common/worker.js"; 2 + 3 + /** 4 + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" 5 + * @import {InputActions} from "~/components/input/types.d.ts" 6 + * @import {Actions} from "~/components/artwork/types.d.ts" 7 + */ 8 + 9 + //////////////////////////////////////////// 10 + // ACTIONS 11 + //////////////////////////////////////////// 12 + 13 + /** 14 + * @type {ActionsWithTunnel<Actions>['get']} 15 + */ 16 + export async function get({ data: track, ports }) { 17 + /** @type {ProxiedActions<InputActions>} */ 18 + const input = workerProxy(() => { 19 + ports.input.start(); 20 + return ports.input; 21 + }); 22 + 23 + return await input.artwork(track.uri); 24 + } 25 + 26 + //////////////////////////////////////////// 27 + // ⚡️ 28 + //////////////////////////////////////////// 29 + 30 + ostiary((context) => { 31 + rpc(context, { get }); 32 + });
+1
src/components/configurator/input/element.js
··· 27 27 /** @type {ProxiedActions<Actions>} */ 28 28 const proxy = this.workerProxy(); 29 29 30 + this.artwork = proxy.artwork; 30 31 this.consult = proxy.consult; 31 32 this.detach = proxy.detach; 32 33 this.groupConsult = proxy.groupConsult;
+1
src/components/input/https/element.js
··· 28 28 /** @type {ProxiedActions<InputActions>} */ 29 29 this.proxy = this.workerProxy(); 30 30 31 + this.artwork = this.proxy.artwork; 31 32 this.consult = this.proxy.consult; 32 33 this.detach = this.proxy.detach; 33 34 this.groupConsult = this.proxy.groupConsult;
+1
src/components/input/icecast/element.js
··· 28 28 /** @type {ProxiedActions<InputActions>} */ 29 29 this.proxy = this.workerProxy(); 30 30 31 + this.artwork = this.proxy.artwork; 31 32 this.consult = this.proxy.consult; 32 33 this.detach = this.proxy.detach; 33 34 this.groupConsult = this.proxy.groupConsult;
+1
src/components/input/local/element.js
··· 37 37 /** @type {ProxiedActions<InputActions>} */ 38 38 this.proxy = this.workerProxy(); 39 39 40 + this.artwork = this.proxy.artwork; 40 41 this.consult = this.proxy.consult; 41 42 this.detach = this.proxy.detach; 42 43 this.groupConsult = this.proxy.groupConsult;
+1
src/components/input/opensubsonic/element.js
··· 28 28 /** @type {ProxiedActions<InputActions>} */ 29 29 this.proxy = this.workerProxy(); 30 30 31 + this.artwork = this.proxy.artwork; 31 32 this.consult = this.proxy.consult; 32 33 this.detach = this.proxy.detach; 33 34 this.groupConsult = this.proxy.groupConsult;
+1
src/components/input/s3/element.js
··· 29 29 /** @type {ProxiedActions<InputActions & { demo: () => Demo }>} */ 30 30 this.proxy = this.workerProxy(); 31 31 32 + this.artwork = this.proxy.artwork; 32 33 this.consult = this.proxy.consult; 33 34 this.detach = this.proxy.detach; 34 35 this.groupConsult = this.proxy.groupConsult;
+42
tests/components/artwork/input/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/artwork/input", () => { 7 + it("delegates to input.artwork and returns null when input has no artwork", async () => { 8 + const result = await testWeb(async () => { 9 + const HttpsInput = await import("~/components/input/https/element.js"); 10 + const InputArtwork = await import( 11 + "~/components/artwork/input/element.js" 12 + ); 13 + 14 + const input = new HttpsInput.CLASS(); 15 + input.id = "test-https-input-artwork"; 16 + document.body.append(input); 17 + 18 + const artwork = new InputArtwork.CLASS(); 19 + artwork.setAttribute("input-selector", "#test-https-input-artwork"); 20 + document.body.append(artwork); 21 + 22 + await customElements.whenDefined(input.localName); 23 + await customElements.whenDefined(artwork.localName); 24 + 25 + const blob = await fetch("http://localhost:3000/testing/sample/audio.mp3") 26 + .then((r) => r.blob()); 27 + const blobUri = URL.createObjectURL(blob); 28 + 29 + const result = await artwork.get({ 30 + $type: "sh.diffuse.output.track" as const, 31 + id: "input-artwork-test", 32 + uri: blobUri, 33 + }); 34 + 35 + URL.revokeObjectURL(blobUri); 36 + 37 + return result ?? null; 38 + }); 39 + 40 + expect(result).toBe(null); 41 + }); 42 + });