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}