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