Experiment to rebuild Diffuse using web applets.
at main 157 lines 4.1 kB view raw
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}