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 return tracks.reduce((acc: Record<string, { server: Server; tracks: Track[] }>, track: Track) => {
65 const parsed = parseURI(track.uri);
66 if (!parsed) return acc;
67
68 const id = serverId(parsed.server);
69 const obj = { server: parsed.server, tracks: acc[id] ? [...acc[id].tracks, track] : [track] };
70
71 return { ...acc, [id]: obj };
72 }, {});
73}
74
75export async function loadServers(): Promise<Record<string, Server>> {
76 const i = await IDB.get(IDB_SERVERS);
77 return i ? i : {};
78}
79
80export function parseURI(
81 uriString: string,
82): { path: string | undefined; server: Server; songId: string | undefined } | undefined {
83 const uri = URI.parse(uriString);
84 if (uri.scheme !== SCHEME) return undefined;
85 if (!uri.host) return undefined;
86
87 let apiKey: string | undefined = undefined;
88 let username: string | undefined = undefined;
89 let password: string | undefined = undefined;
90
91 if (uri.userinfo?.includes(":")) {
92 // Username + Password
93 const [u, p] = uri.userinfo.split(":");
94 username = u;
95 password = p;
96 if (!username || !password) return undefined;
97 } else {
98 // API key
99 apiKey = uri.userinfo;
100 if (!apiKey) return undefined;
101 }
102
103 const qs = QS.parse(uri.query || "");
104
105 const server = {
106 apiKey,
107 host: uri.port ? `${uri.host}:${uri.port}` : uri.host,
108 password,
109 tls: qs.tls === "f" ? false : true,
110 username,
111 };
112
113 const path = uri.path;
114 const songId = typeof qs.songId === "string" ? qs.songId : undefined;
115
116 return { path, server, songId };
117}
118
119export async function saveServers(items: Record<string, Server>) {
120 await IDB.set(IDB_SERVERS, items);
121}
122
123export function serversFromTracks(tracks: Track[]) {
124 return tracks.reduce((acc: Record<string, Server>, track: Track) => {
125 const parsed = parseURI(track.uri);
126 if (!parsed) return acc;
127
128 const id = serverId(parsed.server);
129 if (acc[id]) return acc;
130
131 return { ...acc, [id]: parsed.server };
132 }, {});
133}
134
135export function serverId(server: Server) {
136 const parts = {
137 host: server.host,
138 query: `tls=${server.tls ? "t" : "f"}`,
139 };
140
141 const uri = server.apiKey
142 ? URI.serialize({ ...parts, userinfo: server.apiKey })
143 : URI.serialize({ ...parts, userinfo: `${server.username}:${server.password}` });
144
145 return btoa(uri);
146}