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: add demo

+123 -38
+1
_config.ts
··· 1 + import { builtinModules } from "node:module"; 1 2 import lume from "lume/mod.ts"; 2 3 3 4 import esbuild from "lume/plugins/esbuild.ts";
+5 -6
src/components/configurator/input/worker.js
··· 60 60 Object.keys(groups).map(async (scheme) => { 61 61 const input = grabInput(scheme, ports); 62 62 63 + console.log("🔮", scheme); 64 + 63 65 if (!input) { 64 66 return { 65 67 [scheme]: { 66 68 available: false, 67 69 reason: "Unsupported scheme", 70 + scheme, 68 71 tracks: groups[scheme] ?? [], 69 72 }, 70 73 }; ··· 85 88 export async function list({ data, ports }) { 86 89 const groups = await groupConsult({ data, ports }); 87 90 88 - Object.keys(ports).forEach((scheme) => { 89 - if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 90 - }); 91 - 92 - const promises = Object.entries(groups).map( 93 - async ([scheme, { available, tracks }]) => { 91 + const promises = Object.values(groups).map( 92 + async ({ available, scheme, tracks }) => { 94 93 if (!available) return tracks; 95 94 96 95 const input = grabInput(scheme, ports);
+12
src/components/input/common.js
··· 1 + import { base64url } from "iso-base/rfc4648"; 2 + 3 + /** 4 + * @param {string} scheme 5 + * @param {string} groupId 6 + */ 7 + export async function groupKeyHash(scheme, groupId) { 8 + const rawBytes = new TextEncoder().encode(`${scheme}://${groupId}`); 9 + const hashedBytes = await crypto.subtle.digest("SHA-256", rawBytes); 10 + return base64url.encode(new Uint8Array(hashedBytes)); 11 + } 12 + 1 13 /** 2 14 * @param {string} filename 3 15 */
+9 -6
src/components/input/opensubsonic/worker.js
··· 16 16 serverId, 17 17 serversFromTracks, 18 18 } from "./common.js"; 19 + import { groupKeyHash } from "../common.js"; 19 20 20 21 /** 21 22 * @import {Child, SubsonicAPI} from "subsonic-api" ··· 73 74 74 75 /** @type {ConsultGrouping} */ 75 76 const grouping = available 76 - ? { available, tracks } 77 - : { available, reason: "Server ping failed", tracks }; 77 + ? { available, scheme: SCHEME, tracks } 78 + : { available, reason: "Server ping failed", scheme: SCHEME, tracks }; 78 79 79 80 return { 80 - // key: `${SCHEME}:${serverId}`, 81 - key: SCHEME, 81 + key: await groupKeyHash(SCHEME, serverId), 82 82 grouping, 83 83 }; 84 84 }, ··· 98 98 /** @type {Record<string, Record<string, Track>>} */ 99 99 const cache = {}; 100 100 101 + /** @type {Record<string, Server>} */ 102 + const servers = {}; 103 + 101 104 cachedTracks.forEach((t) => { 102 105 const parsed = parseURI(t.uri); 103 106 if (!parsed || !parsed.path) return; 104 107 105 - const sid = serverId(parsed?.server); 108 + const sid = serverId(parsed.server); 109 + servers[sid] = parsed.server; 106 110 107 111 cache[sid] ??= {}; 108 112 cache[sid][URI.unescapeComponent(parsed.path)] = t; ··· 131 135 return songs; 132 136 } 133 137 134 - const servers = await loadServers(); 135 138 const promises = Object.values(servers).map(async (server) => { 136 139 const client = createClient(server); 137 140 const sid = serverId(server);
+14 -8
src/components/input/s3/worker.js
··· 1 - import { isAudioFile } from "@components/input/common.js"; 1 + import { groupKeyHash, isAudioFile } from "@components/input/common.js"; 2 2 import { 3 3 bucketId, 4 4 bucketsFromTracks, ··· 16 16 import { saveBuckets } from "./common.js"; 17 17 18 18 /** 19 - * @import { InputActions as Actions } from "@components/input/types.d.ts"; 19 + * @import { InputActions as Actions, ConsultGrouping } from "@components/input/types.d.ts"; 20 20 * @import { Track } from "@definitions/types.d.ts" 21 21 * @import { Bucket } from "./types.d.ts" 22 22 */ ··· 67 67 const promises = Object.entries(groups).map( 68 68 async ([bucketId, { bucket, tracks }]) => { 69 69 const available = await consultBucket(bucket); 70 + 71 + /** @type {ConsultGrouping} */ 70 72 const grouping = available 71 - ? { available, tracks } 72 - : { available, reason: "Bucket unavailable", tracks }; 73 + ? { available, scheme: SCHEME, tracks } 74 + : { available, reason: "Bucket unavailable", scheme: SCHEME, tracks }; 73 75 74 76 return { 75 - key: `${SCHEME}:${bucketId}`, 77 + key: await groupKeyHash(SCHEME, bucketId), 76 78 grouping, 77 79 }; 78 80 }, ··· 81 83 const entries = (await Promise.all(promises)).map(( 82 84 entry, 83 85 ) => [entry.key, entry.grouping]); 86 + 84 87 return Object.fromEntries(entries); 85 88 } 86 89 ··· 90 93 export async function list(cachedTracks = []) { 91 94 /** @type {Record<string, Record<string, Track>>} */ 92 95 const cache = {}; 96 + 97 + /** @type {Record<string, Bucket>} */ 98 + const buckets = {}; 93 99 94 100 cachedTracks.forEach((t) => { 95 101 const parsed = parseURI(t.uri); 96 102 if (!parsed) return; 97 103 98 - const bid = bucketId(parsed?.bucket); 104 + const bid = bucketId(parsed.bucket); 105 + buckets[bid] = parsed.bucket; 99 106 100 107 if (cache[bid]) { 101 108 cache[bid][parsed.path] = t; ··· 104 111 } 105 112 }); 106 113 107 - const buckets = await loadBuckets(); 108 114 const promises = Object.values(buckets).map(async (bucket) => { 109 115 const client = createClient(bucket); 110 116 const bid = bucketId(bucket); ··· 193 199 secretKey: atob("Z0hPQkdHRzU1aXc0a0RDbjdjWlRJYTVTUDRZWnpERkRzQnFCYWI4Mg=="), 194 200 }; 195 201 196 - const uri = buildURI(bucket, ""); 202 + const uri = buildURI(bucket); 197 203 198 204 /** @type {Track} */ 199 205 const track = {
+2 -2
src/components/input/types.d.ts
··· 14 14 | { supported: true; consult: "undetermined" | boolean }; 15 15 16 16 export type ConsultGrouping = 17 - | { available: false; reason: string; tracks: Track[] } 18 - | { available: true; tracks: Track[] }; 17 + | { available: false; reason: string; scheme: string; tracks: Track[] } 18 + | { available: true; scheme: string; tracks: Track[] }; 19 19 20 20 export type GroupConsult = Record<string, ConsultGrouping>; 21 21
+7 -5
src/components/orchestrator/process-tracks/element.js
··· 75 75 await customElements.whenDefined(output.localName); 76 76 77 77 // Process whenever tracks are initially loaded 78 - this.effect(() => { 79 - const state = output.tracks.state(); 80 - if (state !== "loaded") return; 78 + if (this.hasAttribute("process-when-ready")) { 79 + this.effect(() => { 80 + const state = output.tracks.state(); 81 + if (state !== "loaded") return; 81 82 82 - untracked(() => this.process()); 83 - }); 83 + untracked(() => this.process()); 84 + }); 85 + } 84 86 } 85 87 86 88 // WORKERS
-1
src/components/orchestrator/process-tracks/worker.js
··· 43 43 */ 44 44 async (promise, track) => { 45 45 const acc = await promise; 46 - 47 46 if (track.tags && track.stats) return [...acc, track]; 48 47 49 48 const resGet = await input.resolve({
+1 -3
src/components/processor/metadata/worker.js
··· 21 21 * @returns {Extraction} 22 22 */ 23 23 (err) => { 24 - console.warn("Metadata processor error:", err); 25 - console.log(args); 26 - 24 + console.warn("Metadata processor error:", err, args); 27 25 return {}; 28 26 }, 29 27 );
+38 -5
src/index.vto
··· 198 198 <p>Diffuse is not your typical streaming service, you have to add sources of audio. This button here adds a few sample audio files.</p> 199 199 200 200 <p> 201 - <small> 202 - <strong>TODO:</strong> 203 - Implement button 204 - </small> 201 + <button id="add-sample-content"> 202 + <span>Add sample content</span> 203 + </button> 205 204 </p> 206 205 207 206 <p> ··· 360 359 361 360 --> 362 361 362 + <di-s3></di-s3> 363 + <do-input></do-input> 364 + <do-output></do-output> 365 + <dp-metadata></dp-metadata> 363 366 367 + <do-process-tracks 368 + input-selector="do-input" 369 + output-selector="do-output" 370 + metadata-processor-selector="dp-metadata" 371 + ></do-process-tracks> 364 372 365 373 <!-- 366 374 367 375 ################################### 368 - # COMPONENTS 376 + # SCRIPTS 369 377 ################################### 370 378 371 379 --> 380 + 381 + <script type="module"> 382 + import "./components/processor/metadata/element.js" 383 + 384 + import * as S3Input from "./components/input/s3/element.js"; 385 + import * as Input from "./components/orchestrator/input/element.js"; 386 + import * as Output from "./components/orchestrator/output/element.js"; 387 + import * as ProcessTracks from "./components/orchestrator/process-tracks/element.js"; 388 + 389 + import { component } from "./common/element.js"; 390 + 391 + const s3Input = component(S3Input) 392 + const output = component(Output) 393 + const processTracks = component(ProcessTracks) 394 + 395 + async function addSampleContent() { 396 + const demo = await s3Input.demo() 397 + const tracks = await output.tracks.collection() 398 + 399 + await output.tracks.save([ ...tracks, demo.track ]) 400 + await processTracks.process() 401 + } 402 + 403 + document.querySelector("#add-sample-content")?.addEventListener("click", addSampleContent) 404 + </script>
+14 -1
src/styles/diffuse/colors.css
··· 3 3 --color-1: oklch(4.1308% 0.25306 109.22); 4 4 --color-2: oklch(98.369% 0.01834 67.664); 5 5 --color-3: oklch(26.787% 0.00168 186.65); 6 - --accent: oklch(86.947% 0.25527 28.789); 6 + 7 + /* Orange/Red */ 8 + /*--accent: oklch(86.947% 0.25527 28.789);*/ 9 + /*--accent: hsl(51, 100%, 50%);*/ 10 + /*--accent: #9e86b8;*/ 11 + 12 + /* Green */ 13 + /*--accent: hsl(120, 73.4%, 74.9%);*/ 14 + --accent: hsl(82, 39%, 30.2%); 7 15 /*--accent: hsl(80, 60.5%, 34.7%);*/ 16 + 17 + /* Blue */ 18 + /*--accent: hsl(203, 92%, 75.5%);*/ 8 19 9 20 --bg-color: var(--color-2); 10 21 --text-color: var(--color-1); ··· 14 25 :root { 15 26 --bg-color: var(--color-3); 16 27 --text-color: var(--color-2); 28 + 29 + --accent: #9e86b8; 17 30 } 18 31 }
+17
src/styles/diffuse/page.css
··· 13 13 text-underline-offset: 6px; 14 14 } 15 15 16 + button { 17 + background: var(--accent); 18 + border: 0; 19 + border-radius: var(--radius-md); 20 + color: var(--bg-color); 21 + cursor: pointer; 22 + font-family: inherit; 23 + font-weight: 500; 24 + line-height: var(--leading-tight); 25 + padding: var(--space-2xs) var(--space-xs); 26 + 27 + & > span { 28 + display: inline-block; 29 + padding-top: 1px; 30 + } 31 + } 32 + 16 33 h1 { 17 34 margin: var(--space-lg) 0 var(--space-xl); 18 35 padding-top: var(--space-2xs);
+1 -1
src/themes/webamp/index.js
··· 2 2 import "@components/input/s3/element.js"; 3 3 import "@components/orchestrator/input/element.js"; 4 4 import "@components/orchestrator/output/element.js"; 5 - // import "@components/orchestrator/process-tracks/element.js"; 5 + import "@components/orchestrator/process-tracks/element.js"; 6 6 import "@components/orchestrator/queue-tracks/element.js"; 7 7 import "@components/orchestrator/search-tracks/element.js"; 8 8 import "@components/processor/metadata/element.js";
+2
src/themes/webamp/index.vto
··· 160 160 input-selector="#input" 161 161 metadata-processor-selector="dp-metadata" 162 162 output-selector="#output" 163 + 164 + process-when-ready 163 165 ></do-process-tracks> 164 166 165 167 <do-queue-tracks