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: add `artwork` action to input components

+147 -1
+13
src/components/configurator/input/worker.js
··· 25 25 //////////////////////////////////////////// 26 26 27 27 /** 28 + * @type {ActionsWithTunnel<Actions>['artwork']} 29 + */ 30 + export async function artwork({ data, ports }) { 31 + const uri = data; 32 + const scheme = uri.split(":", 1)[0]; 33 + const input = grabInput(scheme, ports); 34 + if (!input) return null; 35 + 36 + return await input.artwork(uri); 37 + } 38 + 39 + /** 28 40 * @type {ActionsWithTunnel<Actions>['consult']} 29 41 */ 30 42 export async function consult({ data, ports }) { ··· 217 229 218 230 ostiary((context) => { 219 231 rpc(context, { 232 + artwork, 220 233 consult, 221 234 detach, 222 235 groupConsult,
+8
src/components/input/https/worker.js
··· 18 18 //////////////////////////////////////////// 19 19 20 20 /** 21 + * @type {Actions['artwork']} 22 + */ 23 + export async function artwork(_uri) { 24 + return null; 25 + } 26 + 27 + /** 21 28 * @type {Actions['consult']} 22 29 */ 23 30 export async function consult(fileUriOrScheme) { ··· 133 140 // Setup RPC 134 141 135 142 rpc(context, { 143 + artwork, 136 144 consult, 137 145 detach, 138 146 groupConsult,
+8
src/components/input/icecast/worker.js
··· 19 19 //////////////////////////////////////////// 20 20 21 21 /** 22 + * @type {Actions['artwork']} 23 + */ 24 + export async function artwork(_uri) { 25 + return null; 26 + } 27 + 28 + /** 22 29 * @type {Actions['consult']} 23 30 */ 24 31 export async function consult(fileUriOrScheme) { ··· 142 149 143 150 ostiary((context) => { 144 151 rpc(context, { 152 + artwork, 145 153 consult, 146 154 detach, 147 155 groupConsult,
+8
src/components/input/local/worker.js
··· 24 24 //////////////////////////////////////////// 25 25 26 26 /** 27 + * @type {Actions['artwork']} 28 + */ 29 + export async function artwork(_uri) { 30 + return null; 31 + } 32 + 33 + /** 27 34 * @type {Actions['consult']} 28 35 */ 29 36 export async function consult(fileUriOrScheme) { ··· 231 238 232 239 ostiary((context) => { 233 240 rpc(context, { 241 + artwork, 234 242 consult, 235 243 detach, 236 244 groupConsult,
+8
src/components/input/opensubsonic/worker.js
··· 30 30 //////////////////////////////////////////// 31 31 32 32 /** 33 + * @type {Actions['artwork']} 34 + */ 35 + export async function artwork(_uri) { 36 + return null; 37 + } 38 + 39 + /** 33 40 * @type {Actions['consult']} 34 41 */ 35 42 export async function consult(fileUriOrScheme) { ··· 293 300 // Setup RPC 294 301 295 302 rpc(context, { 303 + artwork, 296 304 consult, 297 305 detach, 298 306 groupConsult,
+8
src/components/input/s3/worker.js
··· 27 27 //////////////////////////////////////////// 28 28 29 29 /** 30 + * @type {Actions['artwork']} 31 + */ 32 + export async function artwork(_uri) { 33 + return null; 34 + } 35 + 36 + /** 30 37 * @type {Actions['consult']} 31 38 */ 32 39 export async function consult(fileUriOrScheme) { ··· 241 248 // Setup RPC 242 249 243 250 rpc(context, { 251 + artwork, 244 252 consult, 245 253 detach, 246 254 groupConsult,
+2 -1
src/components/input/types.d.ts
··· 20 20 export type GroupConsult = Record<string, ConsultGrouping>; 21 21 22 22 export type InputActions = { 23 - consult(fileUriOrScheme: string): Promise<Consult>; 23 + artwork(uri: string): Promise<Uint8Array | null>; 24 + consult(uriOrScheme: string): Promise<Consult>; 24 25 detach(args: { fileUriOrScheme: string; tracks: Track[] }): Promise<Track[]>; 25 26 groupConsult(uris: string[]): Promise<GroupConsult>; 26 27 list(tracks: Track[]): Promise<Track[]>;
+33
tests/components/configurator/input/test.ts
··· 82 82 expect(result["ipfs"]?.uris).toEqual(["ipfs://QmA", "ipfs://QmB"]); 83 83 }); 84 84 85 + it("artwork with an unsupported scheme returns null", async () => { 86 + const result = await testWeb(async () => { 87 + const mod = await import( 88 + "~/components/configurator/input/element.js" 89 + ); 90 + const configurator = new mod.CLASS(); 91 + document.body.append(configurator); 92 + return configurator.artwork("ipfs://QmSomeCid"); 93 + }); 94 + 95 + expect(result).toBe(null); 96 + }); 97 + 98 + it("artwork delegates to the appropriate input and returns null", async () => { 99 + const result = await testWeb(async () => { 100 + const mod = await import( 101 + "~/components/configurator/input/element.js" 102 + ); 103 + const HttpsInput = await import( 104 + "~/components/input/https/element.js" 105 + ); 106 + 107 + const configurator = new mod.CLASS(); 108 + const httpsInput = new HttpsInput.CLASS(); 109 + configurator.appendChild(httpsInput); 110 + document.body.append(configurator); 111 + 112 + return configurator.artwork("https://example.com/audio.mp3"); 113 + }); 114 + 115 + expect(result).toBe(null); 116 + }); 117 + 85 118 it("inputs maps child input elements by their SCHEME", async () => { 86 119 const result = await testWeb(async () => { 87 120 const mod = await import(
+11
tests/components/input/https/test.ts
··· 134 134 expect(remaining[1].id).toBe("4"); 135 135 }); 136 136 137 + it("artwork returns null", async () => { 138 + const result = await testWeb(async () => { 139 + const HttpsInput = await import("~/components/input/https/element.js"); 140 + const input = new HttpsInput.CLASS(); 141 + document.body.append(input); 142 + return await input.artwork("https://example.com/audio.mp3"); 143 + }); 144 + 145 + expect(result).toBe(null); 146 + }); 147 + 137 148 it("has correct SCHEME property", async () => { 138 149 const scheme = await testWeb(async () => { 139 150 const HttpsInput = await import("~/components/input/https/element.js");
+11
tests/components/input/icecast/test.ts
··· 146 146 expect(remaining.length).toBe(2); 147 147 }); 148 148 149 + it("artwork returns null", async () => { 150 + const result = await testWeb(async () => { 151 + const mod = await import("~/components/input/icecast/element.js"); 152 + const input = new mod.CLASS(); 153 + document.body.append(input); 154 + return await input.artwork("icecast://radio.example.com/stream.mp3"); 155 + }); 156 + 157 + expect(result).toBe(null); 158 + }); 159 + 149 160 it("sources returns an entry with icecast:// URI for each track", async () => { 150 161 const sources = await testWeb(async () => { 151 162 const mod = await import("~/components/input/icecast/element.js");
+11
tests/components/input/local/test.ts
··· 125 125 expect(result).toBe(null); 126 126 }); 127 127 128 + it("artwork returns null", async () => { 129 + const result = await testWeb(async () => { 130 + const mod = await import("~/components/input/local/element.js"); 131 + const input = new mod.CLASS(); 132 + document.body.append(input); 133 + return await input.artwork("local://tid-aaa/track.mp3"); 134 + }); 135 + 136 + expect(result).toBe(null); 137 + }); 138 + 128 139 it("groupConsult returns empty object for unknown TIDs", async () => { 129 140 const result = await testWeb(async () => { 130 141 const mod = await import("~/components/input/local/element.js");
+13
tests/components/input/opensubsonic/test.ts
··· 89 89 expect(remaining.length).toBe(1); 90 90 }); 91 91 92 + it("artwork returns null", async () => { 93 + const result = await testWeb(async () => { 94 + const mod = await import("~/components/input/opensubsonic/element.js"); 95 + const input = new mod.CLASS(); 96 + document.body.append(input); 97 + return await input.artwork( 98 + "opensubsonic://user:pass@subsonic.example.com?songId=123&tls=t", 99 + ); 100 + }); 101 + 102 + expect(result).toBe(null); 103 + }); 104 + 92 105 it("detach with specific server URI removes only matching tracks", async () => { 93 106 const remaining = await testWeb(async () => { 94 107 const mod = await import("~/components/input/opensubsonic/element.js");
+13
tests/components/input/s3/test.ts
··· 120 120 expect(remaining[0].id).toBe("2"); 121 121 }); 122 122 123 + it("artwork returns null", async () => { 124 + const result = await testWeb(async () => { 125 + const mod = await import("~/components/input/s3/element.js"); 126 + const input = new mod.CLASS(); 127 + document.body.append(input); 128 + return await input.artwork( 129 + "s3://key:secret@s3.amazonaws.com/music/track1.mp3?bucketName=my-bucket&region=us-east-1", 130 + ); 131 + }); 132 + 133 + expect(result).toBe(null); 134 + }); 135 + 123 136 it("sources returns labels with bucket name and host", async () => { 124 137 const sources = await testWeb(async () => { 125 138 const mod = await import("~/components/input/s3/element.js");