Experiment to rebuild Diffuse using web applets.
0

Configure Feed

Select the types of activity you want to include in your feed.

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}