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: connect / opensubsonic

+209 -9
+7
src/_data/facets.json
··· 37 37 "desc": "Use your AT Protocol identity for user-data storage." 38 38 }, 39 39 { 40 + "url": "facets/connect/opensubsonic/index.html", 41 + "title": "Connect / OpenSubsonic", 42 + "category": "Data", 43 + "featured": true, 44 + "desc": "Connect to an OpenSubsonic server for audio input." 45 + }, 46 + { 40 47 "url": "facets/connect/s3/index.html", 41 48 "title": "Connect / S3", 42 49 "category": "Data",
+15 -9
src/facets/connect/common.js
··· 32 32 * @param {TemplateResult} config.formFields - Form body content (inputs, footnotes, etc.) 33 33 * @param {(mode: 'input' | 'output') => Promise<void>} config.onSubmit 34 34 * @param {boolean} [config.hasInput] - Whether to show the "Add audio input" button (default: true) 35 + * @param {boolean} [config.hasOutput] - Whether to show the "Use as userdata storage" button (default: true) 35 36 * @param {() => Promise<void>} [config.onOutputActivate] - Called instead of opening the dialog when output is already configured but inactive 36 37 * 37 38 * @returns {{ setItems: (items: ConnectItem[]) => void, setError: (message: string | null) => void }} ··· 43 44 formFields, 44 45 onSubmit, 45 46 hasInput = true, 47 + hasOutput = true, 46 48 onOutputActivate, 47 49 }, 48 50 ) { ··· 66 68 </wa-button> 67 69 ` 68 70 : nothing} 69 - <wa-button 70 - id="connect-add-output-btn" 71 - variant="brand" 72 - appearance="filled" 73 - > 74 - <wa-icon slot="start" library="phosphor/fill" name="person"></wa-icon> 75 - Use as userdata storage 76 - </wa-button> 71 + ${hasOutput 72 + ? html` 73 + <wa-button 74 + id="connect-add-output-btn" 75 + variant="brand" 76 + appearance="filled" 77 + > 78 + <wa-icon slot="start" library="phosphor/fill" name="person"></wa-icon> 79 + Use as userdata storage 80 + </wa-button> 81 + ` 82 + : nothing} 77 83 </div> 78 84 <wa-callout id="connect-card-error" variant="danger" hidden></wa-callout> 79 85 <wa-divider id="connect-divider" hidden></wa-divider> ··· 201 207 currentItems = items; 202 208 divider.hidden = items.length === 0; 203 209 list.hidden = items.length === 0; 204 - outputBtn.hidden = items.some((i) => i.isOutput && i.isSelectedOutput); 210 + if (outputBtn) outputBtn.hidden = items.some((i) => i.isOutput && i.isSelectedOutput); 205 211 litRender( 206 212 html` 207 213 ${items.map(
+15
src/facets/connect/opensubsonic/index.html
··· 1 + <style> 2 + @import "./vendor/@awesome.me/webawesome/styles/webawesome.css" layer(wa); 3 + @import "./facets/connect/common.css" layer(connect); 4 + 5 + @layer base, diffuse, wa; 6 + </style> 7 + 8 + <main class="wa-theme-default"></main> 9 + 10 + <script type="module" src="facets/connect/opensubsonic/index.inline.js"></script> 11 + 12 + <script type="module"> 13 + await customElements.whenDefined("wa-card"); 14 + document.querySelector("main")?.classList.add("has-loaded"); 15 + </script>
+172
src/facets/connect/opensubsonic/index.inline.js
··· 1 + import "@awesome.me/webawesome/dist/components/input/input.js"; 2 + import "@awesome.me/webawesome/dist/components/select/select.js"; 3 + import "@awesome.me/webawesome/dist/components/option/option.js"; 4 + 5 + import * as TID from "@atcute/tid"; 6 + import { html } from "lit-html"; 7 + 8 + import * as Output from "~/common/output.js"; 9 + import { SCHEME } from "~/components/input/opensubsonic/constants.js"; 10 + import { 11 + buildURI, 12 + parseURI, 13 + serverId, 14 + } from "~/components/input/opensubsonic/common.js"; 15 + import { effect } from "~/common/signal.js"; 16 + import foundation from "~/common/foundation.js"; 17 + 18 + import { setup } from "~/facets/connect/common.js"; 19 + 20 + /** 21 + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 22 + * @import { default as WaSelect } from "@awesome.me/webawesome/dist/components/select/select.js" 23 + * @import { Server } from "~/components/input/opensubsonic/types.d.ts" 24 + */ 25 + 26 + document.title = "Connect OpenSubsonic | Diffuse"; 27 + 28 + //////////////////////////////////////////// 29 + // SETUP 30 + //////////////////////////////////////////// 31 + 32 + const [inputConfigurator, outputOrchestrator, sourcesOrchestrator] = 33 + await Promise.all([ 34 + foundation.configurator.input(), 35 + foundation.orchestrator.output(), 36 + foundation.orchestrator.sources(), 37 + ]); 38 + 39 + await Promise.all([ 40 + customElements.whenDefined(inputConfigurator.localName), 41 + customElements.whenDefined(outputOrchestrator.localName), 42 + customElements.whenDefined(sourcesOrchestrator.localName), 43 + ]); 44 + 45 + //////////////////////////////////////////// 46 + // UI 47 + //////////////////////////////////////////// 48 + 49 + const { setItems, setError } = setup({ 50 + title: "OpenSubsonic", 51 + hasOutput: false, 52 + 53 + description: html` 54 + <p> 55 + Connect to an OpenSubsonic server to use it as audio input. 56 + </p> 57 + <p class="wa-caption-xs"> 58 + Supports authentication via username + password or an API key. 59 + </p> 60 + `, 61 + 62 + formFields: html` 63 + <wa-input 64 + id="oss-host" 65 + label="Host" 66 + placeholder="music.example.com" 67 + required 68 + ></wa-input> 69 + <wa-select id="oss-tls" label="Use HTTPS / TLS?" value="true"> 70 + <wa-option value="true">Yes</wa-option> 71 + <wa-option value="false">No</wa-option> 72 + </wa-select> 73 + <wa-input id="oss-username" label="Username"></wa-input> 74 + <wa-input id="oss-password" label="Password" type="password"></wa-input> 75 + <p class="wa-caption-xs">Or use an API key instead of username + password:</p> 76 + <wa-input id="oss-apikey" label="API key" type="password"></wa-input> 77 + <p class="wa-caption-xs">* Host is required</p> 78 + `, 79 + 80 + onSubmit: () => addServer(), 81 + }); 82 + 83 + const hostInput = /** @type {WaInput} */ (document.querySelector("#oss-host")); 84 + const tlsSelect = /** @type {WaSelect} */ (document.querySelector("#oss-tls")); 85 + const usernameInput = 86 + /** @type {WaInput} */ (document.querySelector("#oss-username")); 87 + const passwordInput = 88 + /** @type {WaInput} */ (document.querySelector("#oss-password")); 89 + const apikeyInput = 90 + /** @type {WaInput} */ (document.querySelector("#oss-apikey")); 91 + 92 + //////////////////////////////////////////// 93 + // REACTIVE LIST 94 + //////////////////////////////////////////// 95 + 96 + effect(() => { 97 + const inputSources = sourcesOrchestrator.sources()[SCHEME] ?? []; 98 + 99 + /** @type {Map<string, { server: Server; uri: string }>} */ 100 + const allServers = new Map(); 101 + 102 + for (const source of inputSources) { 103 + const parsed = parseURI(source.uri); 104 + if (!parsed) continue; 105 + 106 + const id = serverId(parsed.server); 107 + if (!allServers.has(id)) { 108 + allServers.set(id, { server: parsed.server, uri: source.uri }); 109 + } 110 + } 111 + 112 + setItems( 113 + [...allServers.values()].map(({ server, uri }) => ({ 114 + name: server.host, 115 + detail: server.tls ? "https" : "http", 116 + isInput: true, 117 + isOutput: false, 118 + isSelectedOutput: false, 119 + onRemove: () => removeServer(uri), 120 + })), 121 + ); 122 + }); 123 + 124 + //////////////////////////////////////////// 125 + // ACTIONS 126 + //////////////////////////////////////////// 127 + 128 + /** @param {string} uri */ 129 + async function removeServer(uri) { 130 + setError(null); 131 + try { 132 + const tracks = await Output.data(outputOrchestrator.tracks); 133 + const detachedTracks = await inputConfigurator.detach({ 134 + fileUriOrScheme: uri, 135 + tracks, 136 + }); 137 + 138 + if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); 139 + } catch (err) { 140 + setError(err instanceof Error ? err.message : "Failed to remove server"); 141 + } 142 + } 143 + 144 + async function addServer() { 145 + const host = hostInput.value?.trim(); 146 + const tls = tlsSelect.value !== "false"; 147 + const username = usernameInput.value?.trim() || undefined; 148 + const password = passwordInput.value?.trim() || undefined; 149 + const apiKey = apikeyInput.value?.trim() || undefined; 150 + 151 + if (!host) return; 152 + 153 + /** @type {Server} */ 154 + const server = { host, tls, username, password, apiKey }; 155 + const uri = buildURI(server); 156 + 157 + const now = new Date().toISOString(); 158 + const tracksCol = outputOrchestrator.tracks.collection(); 159 + const existingTracks = tracksCol.state === "loaded" ? tracksCol.data : []; 160 + 161 + await outputOrchestrator.tracks.save([ 162 + ...existingTracks, 163 + { 164 + $type: "sh.diffuse.output.track", 165 + id: TID.now(), 166 + createdAt: now, 167 + updatedAt: now, 168 + kind: "placeholder", 169 + uri, 170 + }, 171 + ]); 172 + }