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