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.

at v4 210 lines 4.7 kB view raw
1import * as URI from "fast-uri"; 2import QS from "query-string"; 3 4import { SCHEME } from "./constants.js"; 5import { cachedConsult } from "~/components/input/common.js"; 6import { safeDecodeURIComponent } from "~/common/utils.js"; 7import { SubsonicAPIWithoutFetch } from "./class.js"; 8 9/** 10 * @import {Child} from "subsonic-api" 11 * @import {Track} from "~/definitions/types.d.ts"; 12 * @import {Server} from "./types.d.ts"; 13 */ 14 15/** 16 * @param {Child["type"]} type 17 * @returns {Track["kind"]} 18 */ 19export function autoTypeToTrackKind(type) { 20 switch (type?.toLowerCase()) { 21 case "audiobook": 22 return "audiobook"; 23 24 case "music": 25 return "music"; 26 27 case "podcast": 28 return "podcast"; 29 30 default: 31 return "miscellaneous"; 32 } 33} 34 35/** 36 * @param {Server} server 37 * @param {{ songId: string; path?: string }} [args] 38 */ 39export function buildURI(server, args) { 40 return URI.serialize({ 41 scheme: SCHEME, 42 userinfo: server.apiKey 43 ? encodeURIComponent(server.apiKey) 44 : `${encodeURIComponent(server.username || "")}:${ 45 encodeURIComponent(server.password || "") 46 }`, 47 host: server.host.replace(/^https?:\/\//, ""), 48 path: args?.path, 49 query: QS.stringify({ 50 songId: args?.songId, 51 tls: server.tls ? "t" : "f", 52 }), 53 }); 54} 55 56/** 57 * @param {Server} server 58 */ 59export async function consultServer(server) { 60 const client = createClient(server); 61 const resp = await client.ping().catch(() => undefined); 62 return resp?.status?.toLowerCase() === "ok"; 63} 64 65export const consultServerCached = cachedConsult(consultServer, serverId); 66 67/** 68 * @param {Server} server 69 */ 70export function createClient(server) { 71 return new SubsonicAPIWithoutFetch({ 72 url: `http${server.tls ? "s" : ""}://${server.host}`, 73 auth: server.apiKey ? { apiKey: safeDecodeURIComponent(server.apiKey) } : { 74 username: safeDecodeURIComponent(server.username || ""), 75 password: safeDecodeURIComponent(server.password || ""), 76 }, 77 }); 78} 79 80/** 81 * @param {Track[]} tracks 82 */ 83export function groupTracksByServer(tracks) { 84 /** @type {Record<string, { server: Server; tracks: Track[] }>} */ 85 const acc = {}; 86 87 tracks.forEach((track) => { 88 const parsed = parseURI(track.uri); 89 if (!parsed) return; 90 91 const id = serverId(parsed.server); 92 93 if (acc[id]) { 94 acc[id].tracks.push(track); 95 } else { 96 acc[id] = { server: parsed.server, tracks: [track] }; 97 } 98 }); 99 100 return acc; 101} 102 103/** 104 * @param {string[]} uris 105 */ 106export function groupUrisByServer(uris) { 107 /** @type {Record<string, { server: Server; uris: string[] }>} */ 108 const acc = {}; 109 110 uris.forEach((uri) => { 111 const parsed = parseURI(uri); 112 if (!parsed) return; 113 114 const id = serverId(parsed.server); 115 116 if (acc[id]) { 117 acc[id].uris.push(uri); 118 } else { 119 acc[id] = { server: parsed.server, uris: [uri] }; 120 } 121 }); 122 123 return acc; 124} 125 126/** 127 * Parse an opensubsonic URI. 128 * 129 * ``` 130 * opensubsonic://username:password@server-host:port/path?tls=f 131 * ``` 132 * 133 * @param {string} uriString 134 * @returns {{ path: string | undefined; server: Server; songId: string | undefined } | undefined} 135 */ 136export function parseURI(uriString) { 137 const uri = URI.parse(uriString); 138 if (uri.scheme !== SCHEME) return undefined; 139 if (!uri.host) return undefined; 140 141 let apiKey = undefined; 142 let username = undefined; 143 let password = undefined; 144 145 if (uri.userinfo?.includes(":")) { 146 // Username + Password 147 const [u, p] = uri.userinfo.split(":"); 148 username = u; 149 password = p; 150 if (!username || !password) return undefined; 151 } else { 152 // API key 153 apiKey = uri.userinfo; 154 if (!apiKey) return undefined; 155 } 156 157 const qs = QS.parse(uri.query || ""); 158 159 const server = { 160 apiKey, 161 host: uri.port ? `${uri.host}:${uri.port}` : uri.host, 162 password, 163 tls: qs.tls === "f" ? false : true, 164 username, 165 }; 166 167 const path = uri.path; 168 const songId = typeof qs.songId === "string" ? qs.songId : undefined; 169 170 return { path, server, songId }; 171} 172 173/** 174 * @param {Track[]} tracks 175 */ 176export function serversFromTracks(tracks) { 177 /** @type {Record<string, Server>} */ 178 const acc = {}; 179 180 tracks.forEach((track) => { 181 const parsed = parseURI(track.uri); 182 if (!parsed) return; 183 184 const id = serverId(parsed.server); 185 if (acc[id]) return; 186 187 acc[id] = parsed.server; 188 }); 189 190 return acc; 191} 192 193/** 194 * @param {Server} server 195 */ 196export function serverId(server) { 197 const parts = { 198 host: server.host, 199 query: `tls=${server.tls ? "t" : "f"}`, 200 }; 201 202 const uri = server.apiKey 203 ? URI.serialize({ ...parts, userinfo: server.apiKey }) 204 : URI.serialize({ 205 ...parts, 206 userinfo: `${server.username}:${server.password}`, 207 }); 208 209 return btoa(uri); 210}