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 https input

+645 -15
+1
.gitignore
··· 3 3 /_site 4 4 /_vendor 5 5 /dist 6 + /docs/plans/* 6 7 /src/definitions/types/ 7 8 /vendor
+15 -2
deno.jsonc
··· 43 43 "webamp": "npm:webamp@^2.2.0", 44 44 45 45 // Paths 46 - "@src/": "./src/", 46 + "@testing/": "./src/testing/", 47 47 "@tests/": "./tests/", 48 48 49 49 "@common/": "./src/common/", ··· 70 70 "exports": { 71 71 // .js 72 72 "./common/facets/foundation.js": "./src/common/facets/foundation.js", 73 + "./common/facets/utils.js": "./src/common/facets/utils.js", 73 74 "./common/element.js": "./src/common/element.js", 74 75 "./common/signal.js": "./src/common/signal.js", 75 76 "./common/worker.js": "./src/common/worker.js", ··· 88 89 "./components/input/opensubsonic/constants.js": "./src/components/input/opensubsonic/constants.js", 89 90 "./components/input/opensubsonic/element.js": "./src/components/input/opensubsonic/element.js", 90 91 "./components/input/opensubsonic/worker.js": "./src/components/input/opensubsonic/worker.js", 92 + "./components/input/https/common.js": "./src/components/input/https/common.js", 93 + "./components/input/https/constants.js": "./src/components/input/https/constants.js", 94 + "./components/input/https/element.js": "./src/components/input/https/element.js", 95 + "./components/input/https/worker.js": "./src/components/input/https/worker.js", 91 96 "./components/input/s3/common.js": "./src/components/input/s3/common.js", 92 97 "./components/input/s3/constants.js": "./src/components/input/s3/constants.js", 93 98 "./components/input/s3/element.js": "./src/components/input/s3/element.js", ··· 115 120 "./components/processor/search/element.js": "./src/components/processor/search/element.js", 116 121 "./components/processor/search/worker.js": "./src/components/processor/search/worker.js", 117 122 "./components/transformer/output/base.js": "./src/components/transformer/output/base.js", 123 + "./components/transformer/output/bytes/automerge/constants.js": "./src/components/transformer/output/bytes/automerge/constants.js", 124 + "./components/transformer/output/bytes/automerge/element.js": "./src/components/transformer/output/bytes/automerge/element.js", 125 + "./components/transformer/output/bytes/automerge/utils.js": "./src/components/transformer/output/bytes/automerge/utils.js", 126 + "./components/transformer/output/bytes/json/element.js": "./src/components/transformer/output/bytes/json/element.js", 118 127 "./components/transformer/output/refiner/default/element.js": "./src/components/transformer/output/refiner/default/element.js", 119 128 "./components/transformer/output/string/json/element.js": "./src/components/transformer/output/string/json/element.js", 120 129 ··· 127 136 "./components/input/opensubsonic/types.d.ts": "./src/components/input/opensubsonic/types.d.ts", 128 137 "./components/input/s3/types.d.ts": "./src/components/input/s3/types.d.ts", 129 138 "./components/input/types.d.ts": "./src/components/input/types.d.ts", 130 - "./components/orchestrator/auto-queue/types.d.ts": "./src/components/orchestrator/auto-queue/types.d.ts", 131 139 "./components/orchestrator/process-tracks/types.d.ts": "./src/components/orchestrator/process-tracks/types.d.ts", 132 140 "./components/orchestrator/scoped-tracks/types.d.ts": "./src/components/orchestrator/scoped-tracks/types.d.ts", 133 141 "./components/output/polymorphic/indexed-db/types.d.ts": "./src/components/output/polymorphic/indexed-db/types.d.ts", ··· 135 143 "./components/processor/artwork/types.d.ts": "./src/components/processor/artwork/types.d.ts", 136 144 "./components/processor/metadata/types.d.ts": "./src/components/processor/metadata/types.d.ts", 137 145 "./components/processor/search/types.d.ts": "./src/components/processor/search/types.d.ts", 146 + "./components/transformer/output/bytes/automerge/types.d.ts": "./src/components/transformer/output/bytes/automerge/types.d.ts", 138 147 "./definitions/types.d.ts": "./src/definitions/types.d.ts", 139 148 140 149 // .ts ··· 159 168 "serve": { 160 169 "description": "Run and serve the site for development", 161 170 "command": "deno task lume -s", 171 + }, 172 + "test": { 173 + "description": "Run tests", 174 + "command": "deno test -A --doc --ignore=README.md", 162 175 }, 163 176 }, 164 177 "compilerOptions": {
+212
src/components/input/https/common.js
··· 1 + /** 2 + * @import {Track} from "@definitions/types.d.ts" 3 + */ 4 + 5 + /** 6 + * Group tracks by host. 7 + * 8 + * @param {Track[]} tracks 9 + * @returns {Record<string, { host: string; tracks: Track[] }>} 10 + * 11 + * @example Group tracks by domain 12 + * ```ts 13 + * import { expect } from "@std/expect"; 14 + * import { groupTracksByHost } from "./common.js"; 15 + * import type { Track } from "@definitions/types.d.ts"; 16 + * 17 + * const tracks: Track[] = [ 18 + * { 19 + * $type: "sh.diffuse.output.track", 20 + * id: "1", 21 + * uri: "https://example.com/a.mp3", 22 + * }, 23 + * { 24 + * $type: "sh.diffuse.output.track", 25 + * id: "2", 26 + * uri: "https://cdn.example.com/b.mp3", 27 + * }, 28 + * { 29 + * $type: "sh.diffuse.output.track", 30 + * id: "3", 31 + * uri: "https://example.com/c.mp3", 32 + * }, 33 + * ]; 34 + * 35 + * const groups = groupTracksByHost(tracks); 36 + * expect(Object.keys(groups).length).toBe(2); 37 + * expect(groups["example.com"].tracks.length).toBe(2); 38 + * expect(groups["cdn.example.com"].tracks.length).toBe(1); 39 + * ``` 40 + * 41 + * @example Group tracks by host including port 42 + * ```ts 43 + * import { expect } from "@std/expect"; 44 + * import { groupTracksByHost } from "./common.js"; 45 + * import type { Track } from "@definitions/types.d.ts"; 46 + * 47 + * const tracks: Track[] = [ 48 + * { 49 + * $type: "sh.diffuse.output.track", 50 + * id: "1", 51 + * uri: "https://example.com/a.mp3", 52 + * }, 53 + * { 54 + * $type: "sh.diffuse.output.track", 55 + * id: "2", 56 + * uri: "https://example.com:8443/b.mp3", 57 + * }, 58 + * ]; 59 + * 60 + * const groups = groupTracksByHost(tracks); 61 + * expect(Object.keys(groups).length).toBe(2); 62 + * expect(groups["example.com"].tracks.length).toBe(1); 63 + * expect(groups["example.com:8443"].tracks.length).toBe(1); 64 + * ``` 65 + */ 66 + export function groupTracksByHost(tracks) { 67 + /** @type {Record<string, { host: string; tracks: Track[] }>} */ 68 + const acc = {}; 69 + 70 + tracks.forEach((track) => { 71 + const parsed = parseURI(track.uri); 72 + if (!parsed) return; 73 + 74 + const host = parsed.host; 75 + 76 + if (acc[host]) { 77 + acc[host].tracks.push(track); 78 + } else { 79 + acc[host] = { host, tracks: [track] }; 80 + } 81 + }); 82 + 83 + return acc; 84 + } 85 + 86 + /** 87 + * Extract unique hosts from tracks. 88 + * 89 + * @param {Track[]} tracks 90 + * @returns {Record<string, string>} 91 + * 92 + * @example Extract unique hosts 93 + * ```ts 94 + * import { expect } from "@std/expect"; 95 + * import { hostsFromTracks } from "./common.js"; 96 + * import type { Track } from "@definitions/types.d.ts"; 97 + * 98 + * const tracks: Track[] = [ 99 + * { 100 + * $type: "sh.diffuse.output.track", 101 + * id: "1", 102 + * uri: "https://example.com/a.mp3", 103 + * }, 104 + * { 105 + * $type: "sh.diffuse.output.track", 106 + * id: "2", 107 + * uri: "https://example.com/b.mp3", 108 + * }, 109 + * { 110 + * $type: "sh.diffuse.output.track", 111 + * id: "3", 112 + * uri: "https://cdn.example.com/c.mp3", 113 + * }, 114 + * ]; 115 + * 116 + * const hosts = hostsFromTracks(tracks); 117 + * expect(Object.keys(hosts).length).toBe(2); 118 + * expect(hosts["example.com"]).toBe("example.com"); 119 + * expect(hosts["cdn.example.com"]).toBe("cdn.example.com"); 120 + * ``` 121 + */ 122 + export function hostsFromTracks(tracks) { 123 + /** @type {Record<string, string>} */ 124 + const acc = {}; 125 + 126 + tracks.forEach((track) => { 127 + const parsed = parseURI(track.uri); 128 + if (!parsed) return; 129 + 130 + const host = parsed.host; 131 + if (acc[host]) return; 132 + 133 + acc[host] = host; 134 + }); 135 + 136 + return acc; 137 + } 138 + 139 + /** 140 + * Parse an HTTPS URI. 141 + * Validates and extracts components from a standard HTTPS URL. 142 + * 143 + * @param {string} uriString 144 + * @returns {{ url: string; domain: string; path: string; host: string } | undefined} 145 + * 146 + * @example Parse a valid HTTPS URI 147 + * ```ts 148 + * import { expect } from "@std/expect"; 149 + * import { parseURI } from "./common.js"; 150 + * 151 + * const result = parseURI("https://example.com/song.mp3"); 152 + * expect(result?.domain).toBe("example.com"); 153 + * expect(result?.host).toBe("example.com"); 154 + * expect(result?.path).toBe("/song.mp3"); 155 + * expect(result?.url).toBe("https://example.com/song.mp3"); 156 + * ``` 157 + * 158 + * @example Parse HTTPS URI with port 159 + * ```ts 160 + * import { expect } from "@std/expect"; 161 + * import { parseURI } from "./common.js"; 162 + * 163 + * const result = parseURI("https://example.com:8443/audio.mp3"); 164 + * expect(result?.domain).toBe("example.com"); 165 + * expect(result?.host).toBe("example.com:8443"); 166 + * expect(result?.path).toBe("/audio.mp3"); 167 + * ``` 168 + * 169 + * @example Parse HTTPS URI with query parameters 170 + * ```ts 171 + * import { expect } from "@std/expect"; 172 + * import { parseURI } from "./common.js"; 173 + * 174 + * const result = parseURI("https://example.com/song.mp3?token=abc123"); 175 + * expect(result?.domain).toBe("example.com"); 176 + * expect(result?.path).toBe("/song.mp3"); 177 + * expect(result?.url).toContain("token=abc123"); 178 + * ``` 179 + * 180 + * @example Reject non-HTTPS URI 181 + * ```ts 182 + * import { expect } from "@std/expect"; 183 + * import { parseURI } from "./common.js"; 184 + * 185 + * const result = parseURI("http://example.com/song.mp3"); 186 + * expect(result).toBeUndefined(); 187 + * ``` 188 + * 189 + * @example Reject invalid URI 190 + * ```ts 191 + * import { expect } from "@std/expect"; 192 + * import { parseURI } from "./common.js"; 193 + * 194 + * const result = parseURI("not-a-url"); 195 + * expect(result).toBeUndefined(); 196 + * ``` 197 + */ 198 + export function parseURI(uriString) { 199 + try { 200 + const url = new URL(uriString); 201 + if (url.protocol !== "https:") return undefined; 202 + 203 + return { 204 + url: url.href, 205 + domain: url.hostname, 206 + host: url.host, // includes port if present 207 + path: url.pathname, 208 + }; 209 + } catch { 210 + return undefined; 211 + } 212 + }
+1
src/components/input/https/constants.js
··· 1 + export const SCHEME = "https";
+60
src/components/input/https/element.js
··· 1 + import { DiffuseElement } from "@common/element.js"; 2 + import { SCHEME } from "./constants.js"; 3 + import { hostsFromTracks } from "./common.js"; 4 + 5 + /** 6 + * @import {InputActions, InputSchemeProvider} from "@components/input/types.d.ts" 7 + * @import {ProxiedActions} from "@common/worker.d.ts" 8 + * @import {Track} from "@definitions/types.d.ts" 9 + */ 10 + 11 + //////////////////////////////////////////// 12 + // ELEMENT 13 + //////////////////////////////////////////// 14 + 15 + /** 16 + * @implements {ProxiedActions<InputActions>} 17 + * @implements {InputSchemeProvider} 18 + */ 19 + class HttpsInput extends DiffuseElement { 20 + static NAME = "diffuse/input/https"; 21 + static WORKER_URL = "components/input/https/worker.js"; 22 + 23 + SCHEME = SCHEME; 24 + 25 + constructor() { 26 + super(); 27 + 28 + /** @type {ProxiedActions<InputActions>} */ 29 + this.proxy = this.workerProxy(); 30 + 31 + this.consult = this.proxy.consult; 32 + this.detach = this.proxy.detach; 33 + this.groupConsult = this.proxy.groupConsult; 34 + this.list = this.proxy.list; 35 + this.resolve = this.proxy.resolve; 36 + } 37 + 38 + // 🛠️ 39 + 40 + /** @param {Track[]} tracks */ 41 + sources(tracks) { 42 + const hosts = Object.values(hostsFromTracks(tracks)); 43 + 44 + return hosts.map((host) => ({ 45 + label: host, 46 + uri: `https://${host}`, 47 + })); 48 + } 49 + } 50 + 51 + export default HttpsInput; 52 + 53 + //////////////////////////////////////////// 54 + // REGISTER 55 + //////////////////////////////////////////// 56 + 57 + export const CLASS = HttpsInput; 58 + export const NAME = "di-https"; 59 + 60 + customElements.define(NAME, CLASS);
+163
src/components/input/https/worker.js
··· 1 + import { ostiary, rpc } from "@common/worker.js"; 2 + import { 3 + detach as detachUtil, 4 + groupKeyHash, 5 + } from "@components/input/common.js"; 6 + 7 + import { groupTracksByHost, parseURI } from "./common.js"; 8 + import { SCHEME } from "./constants.js"; 9 + 10 + /** 11 + * @import { InputActions as Actions, ConsultGrouping } from "@components/input/types.d.ts"; 12 + */ 13 + 14 + //////////////////////////////////////////// 15 + // ACTIONS 16 + //////////////////////////////////////////// 17 + 18 + /** 19 + * @type {Actions['consult']} 20 + */ 21 + export async function consult(fileUriOrScheme) { 22 + if (!fileUriOrScheme.includes(":")) { 23 + return { supported: true, consult: "undetermined" }; 24 + } 25 + 26 + const parsed = parseURI(fileUriOrScheme); 27 + if (!parsed) { 28 + return { supported: false, reason: "Invalid HTTPS URL" }; 29 + } 30 + 31 + // Ping the URL to check if it's reachable 32 + try { 33 + const controller = new AbortController(); 34 + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 35 + 36 + const response = await fetch(parsed.url, { 37 + method: "HEAD", 38 + signal: controller.signal, 39 + }); 40 + 41 + clearTimeout(timeoutId); 42 + return { supported: true, consult: response.ok }; 43 + } catch (error) { 44 + return { supported: true, consult: false }; 45 + } 46 + } 47 + 48 + /** 49 + * @type {Actions['detach']} 50 + */ 51 + export async function detach(args) { 52 + return detachUtil({ 53 + ...args, 54 + 55 + inputScheme: SCHEME, 56 + handleFileUri: ({ fileURI, tracks }) => { 57 + const result = parseURI(fileURI); 58 + if (!result) return tracks; 59 + 60 + const did = result.host; 61 + const groups = groupTracksByHost(tracks); 62 + 63 + delete groups[did]; 64 + 65 + return Object.values(groups).map((a) => a.tracks).flat(1); 66 + }, 67 + }); 68 + } 69 + 70 + /** 71 + * @type {Actions['groupConsult']} 72 + */ 73 + export async function groupConsult(tracks) { 74 + const groups = groupTracksByHost(tracks); 75 + 76 + const promises = Object.entries(groups).map( 77 + async ([_domainId, { host, tracks }]) => { 78 + // Pick one track to test reachability 79 + const testTrack = tracks[0]; 80 + let available = false; 81 + 82 + if (testTrack) { 83 + try { 84 + const controller = new AbortController(); 85 + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 86 + 87 + const response = await fetch(testTrack.uri, { 88 + method: "HEAD", 89 + signal: controller.signal, 90 + }); 91 + 92 + clearTimeout(timeoutId); 93 + available = response.ok; 94 + } catch { 95 + available = false; 96 + } 97 + } 98 + 99 + /** @type {ConsultGrouping} */ 100 + const grouping = available 101 + ? { available, scheme: SCHEME, tracks } 102 + : { available, reason: "Host unreachable", scheme: SCHEME, tracks }; 103 + 104 + return { 105 + key: await groupKeyHash(SCHEME, host), 106 + grouping, 107 + }; 108 + }, 109 + ); 110 + 111 + const entries = (await Promise.all(promises)).map(( 112 + entry, 113 + ) => [entry.key, entry.grouping]); 114 + 115 + return Object.fromEntries(entries); 116 + } 117 + 118 + /** 119 + * @type {Actions['list']} 120 + */ 121 + export async function list(cachedTracks = []) { 122 + // HTTPS input doesn't discover tracks automatically. 123 + // It only manages URLs that were added manually. 124 + // Just return the cached tracks that match our scheme. 125 + return cachedTracks.filter((track) => { 126 + const parsed = parseURI(track.uri); 127 + return !!parsed; 128 + }); 129 + } 130 + 131 + /** 132 + * @type {Actions['resolve']} 133 + */ 134 + export async function resolve({ method, uri }) { 135 + const parsed = parseURI(uri); 136 + if (!parsed) return undefined; 137 + 138 + // HTTPS URLs don't need resolution - they're already accessible. 139 + // Just return the URL as-is with a far-future expiration. 140 + const expiresInSeconds = 60 * 60 * 24 * 365; // 1 year 141 + const expiresAtSeconds = Math.round(Date.now() / 1000) + expiresInSeconds; 142 + 143 + return { 144 + url: parsed.url, 145 + expiresAt: expiresAtSeconds, 146 + }; 147 + } 148 + 149 + //////////////////////////////////////////// 150 + // ⚡️ 151 + //////////////////////////////////////////// 152 + 153 + ostiary((context) => { 154 + // Setup RPC 155 + 156 + rpc(context, { 157 + consult, 158 + detach, 159 + groupConsult, 160 + list, 161 + resolve, 162 + }); 163 + });
+3 -3
src/index.vto
··· 48 48 title: "Opensubsonic" 49 49 desc: > 50 50 Add any (open)subsonic server. 51 - - title: "HTTPS (Raw)" 51 + - url: "components/input/https/element.js" 52 + title: "HTTPS" 52 53 desc: > 53 - Enables usage of tracks with a HTTPS URI. 54 - todo: true 54 + HTTPS URLs to audio files or streams. 55 55 - title: "HTTPS (JSON)" 56 56 desc: > 57 57 Generate tracks based on HTTPS servers that provide JSON (directory) listings.
+1 -1
src/testing/index.vto
··· 11 11 { 12 12 "imports": { 13 13 "@components/": "./components/", 14 - "@src/": "./" 14 + "@testing/": "./testing/" 15 15 } 16 16 } 17 17 </script>
+5 -5
tests/components/engine/queue/test.ts
··· 2 2 import { expect } from "@std/expect"; 3 3 4 4 import { testWeb } from "@tests/common/index.ts"; 5 - import { tracks } from "@src/testing/sample/tracks.js"; 5 + import { tracks } from "@testing/sample/tracks.js"; 6 6 7 7 describe("components/engine/queue", () => { 8 8 it("adds tracks", async () => { ··· 12 12 13 13 document.body.append(engine); 14 14 15 - const { tracks } = await import("@src/testing/sample/tracks.js"); 15 + const { tracks } = await import("@testing/sample/tracks.js"); 16 16 17 17 await engine.add({ tracks }); 18 18 return engine.future(); ··· 31 31 32 32 document.body.append(engine); 33 33 34 - const { tracks } = await import("@src/testing/sample/tracks.js"); 34 + const { tracks } = await import("@testing/sample/tracks.js"); 35 35 36 36 await engine.supply({ tracks }); 37 37 await engine.fill({ amount: 1, shuffled: false }); ··· 51 51 52 52 document.body.append(engine); 53 53 54 - const { tracks } = await import("@src/testing/sample/tracks.js"); 54 + const { tracks } = await import("@testing/sample/tracks.js"); 55 55 56 56 await engine.add({ tracks }); 57 57 await engine.shift(); ··· 72 72 73 73 document.body.append(engine); 74 74 75 - const { tracks } = await import("@src/testing/sample/tracks.js"); 75 + const { tracks } = await import("@testing/sample/tracks.js"); 76 76 77 77 await engine.add({ tracks }); 78 78 await engine.shift();
+180
tests/components/input/https/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 + import type { Track } from "@definitions/types.d.ts"; 6 + 7 + describe("components/input/https", () => { 8 + it("resolves HTTPS URI to same URL", async () => { 9 + const resolved = await testWeb(async () => { 10 + const HttpsInput = await import("@components/input/https/element.js"); 11 + const input = new HttpsInput.CLASS(); 12 + document.body.append(input); 13 + 14 + return await input.resolve({ 15 + uri: "https://example.com/audio.mp3", 16 + }); 17 + }); 18 + 19 + if (resolved && "url" in resolved) { 20 + expect(resolved.url).toBe("https://example.com/audio.mp3"); 21 + expect(resolved.expiresAt).toBeGreaterThan(Date.now() / 1000); 22 + } 23 + }); 24 + 25 + it("lists only HTTPS tracks from mixed collection", async () => { 26 + const tracks = await testWeb(async () => { 27 + const HttpsInput = await import("@components/input/https/element.js"); 28 + const input = new HttpsInput.CLASS(); 29 + document.body.append(input); 30 + 31 + const cachedTracks: Track[] = [ 32 + { 33 + $type: "sh.diffuse.output.track", 34 + id: "1", 35 + uri: "https://example.com/a.mp3", 36 + }, 37 + { 38 + $type: "sh.diffuse.output.track", 39 + id: "2", 40 + uri: "s3://bucket/b.mp3", 41 + }, 42 + { 43 + $type: "sh.diffuse.output.track", 44 + id: "3", 45 + uri: "https://example.com/c.mp3", 46 + }, 47 + ]; 48 + 49 + return await input.list(cachedTracks); 50 + }); 51 + 52 + expect(tracks.length).toBe(2); 53 + expect(tracks[0].id).toBe("1"); 54 + expect(tracks[1].id).toBe("3"); 55 + }); 56 + 57 + it("provides sources list from tracks", async () => { 58 + const sources = await testWeb(async () => { 59 + const HttpsInput = await import("@components/input/https/element.js"); 60 + const input = new HttpsInput.CLASS(); 61 + document.body.append(input); 62 + 63 + const tracks: Track[] = [ 64 + { 65 + $type: "sh.diffuse.output.track", 66 + id: "1", 67 + uri: "https://example.com/a.mp3", 68 + }, 69 + { 70 + $type: "sh.diffuse.output.track", 71 + id: "2", 72 + uri: "https://cdn.example.com/b.mp3", 73 + }, 74 + ]; 75 + 76 + return input.sources(tracks); 77 + }); 78 + 79 + expect(sources.length).toBe(2); 80 + expect(sources[0].label).toBeDefined(); 81 + expect(sources[0].uri).toContain("https://"); 82 + expect(sources[1].label).toBeDefined(); 83 + expect(sources[1].uri).toContain("https://"); 84 + }); 85 + 86 + it("consult returns undetermined for scheme only", async () => { 87 + const result = await testWeb(async () => { 88 + const HttpsInput = await import("@components/input/https/element.js"); 89 + const input = new HttpsInput.CLASS(); 90 + document.body.append(input); 91 + 92 + return await input.consult("https"); 93 + }); 94 + 95 + expect(result.supported).toBe(true); 96 + if (result.supported) { 97 + expect(result.consult).toBe("undetermined"); 98 + } 99 + }); 100 + 101 + it("detaches all HTTPS tracks when given scheme", async () => { 102 + const remaining = await testWeb(async () => { 103 + const HttpsInput = await import("@components/input/https/element.js"); 104 + const input = new HttpsInput.CLASS(); 105 + document.body.append(input); 106 + 107 + const tracks: Track[] = [ 108 + { 109 + $type: "sh.diffuse.output.track", 110 + id: "1", 111 + uri: "https://example.com/a.mp3", 112 + }, 113 + { 114 + $type: "sh.diffuse.output.track", 115 + id: "2", 116 + uri: "https://example.com/c.mp3", 117 + }, 118 + ]; 119 + 120 + return await input.detach({ 121 + fileUriOrScheme: "https", 122 + tracks, 123 + }); 124 + }); 125 + 126 + expect(remaining.length).toBe(0); 127 + }); 128 + 129 + it("detaches tracks from specific domain", async () => { 130 + const remaining = await testWeb(async () => { 131 + const HttpsInput = await import("@components/input/https/element.js"); 132 + const input = new HttpsInput.CLASS(); 133 + document.body.append(input); 134 + 135 + const tracks: Track[] = [ 136 + { 137 + $type: "sh.diffuse.output.track", 138 + id: "1", 139 + uri: "https://example.com/a.mp3", 140 + }, 141 + { 142 + $type: "sh.diffuse.output.track", 143 + id: "2", 144 + uri: "https://cdn.example.com/b.mp3", 145 + }, 146 + { 147 + $type: "sh.diffuse.output.track", 148 + id: "3", 149 + uri: "https://example.com/c.mp3", 150 + }, 151 + { 152 + $type: "sh.diffuse.output.track", 153 + id: "4", 154 + uri: "https://cdn.example.com/d.mp3", 155 + }, 156 + ]; 157 + 158 + return await input.detach({ 159 + fileUriOrScheme: "https://example.com", 160 + tracks, 161 + }); 162 + }); 163 + 164 + expect(remaining.length).toBe(2); 165 + expect(remaining[0].id).toBe("2"); 166 + expect(remaining[1].id).toBe("4"); 167 + }); 168 + 169 + it("has correct SCHEME property", async () => { 170 + const scheme = await testWeb(async () => { 171 + const HttpsInput = await import("@components/input/https/element.js"); 172 + const input = new HttpsInput.CLASS(); 173 + document.body.append(input); 174 + 175 + return input.SCHEME; 176 + }); 177 + 178 + expect(scheme).toBe("https"); 179 + }); 180 + });
+4 -4
tests/components/processor/search/test.ts
··· 2 2 import { expect } from "@std/expect"; 3 3 4 4 import { testWeb } from "@tests/common/index.ts"; 5 - import { trackA, trackB } from "@src/testing/sample/tracks.js"; 5 + import { trackA, trackB } from "@testing/sample/tracks.js"; 6 6 7 7 describe("components/processor/search", () => { 8 8 it("finds tracks by album", async () => { ··· 15 15 document.body.append(processor); 16 16 17 17 // Add sample tracks to the supply first 18 - const { tracks } = await import("@src/testing/sample/tracks.js"); 18 + const { tracks } = await import("@testing/sample/tracks.js"); 19 19 await processor.supply({ tracks }); 20 20 21 21 // Search for a specific term ··· 35 35 document.body.append(processor); 36 36 37 37 // Add sample tracks to the supply first 38 - const { tracks } = await import("@src/testing/sample/tracks.js"); 38 + const { tracks } = await import("@testing/sample/tracks.js"); 39 39 await processor.supply({ tracks }); 40 40 41 41 // Search for a specific term ··· 55 55 document.body.append(processor); 56 56 57 57 // Add sample tracks to the supply first 58 - const { tracks } = await import("@src/testing/sample/tracks.js"); 58 + const { tracks } = await import("@testing/sample/tracks.js"); 59 59 await processor.supply({ tracks }); 60 60 61 61 // Search for a specific term