import SparkMD5 from "spark-md5"; export interface Artist { id: string; name: string; coverArt?: string; } export interface Album { id: string; name: string; artist: string; artistId: string; coverArt?: string; } export interface Playlist { id: string; name: string; coverArt?: string; readonly?: boolean; owner?: string; public?: boolean; } export interface Song { id: string; title: string; artist: string; album: string; duration?: number; coverArt?: string; albumId?: string; starred?: string; userRating?: number; discNumber?: number; track?: number; replayGain?: { trackGain?: number; albumGain?: number; trackPeak?: number; albumPeak?: number; }; } export interface Credentials { server: string; username: string; token: string; salt: string; } export let credentials: Credentials | null = null; export const setCredentials = (c: Credentials | null) => { credentials = c; c ? localStorage.setItem("tinysub_credentials", JSON.stringify(c)) : localStorage.removeItem("tinysub_credentials"); }; export const asArray = (v: any) => (Array.isArray(v) ? v : v ? [v] : []); const getParams = (params: any = {}) => { if (!credentials) throw "Auth required"; const p = new URLSearchParams({ u: credentials.username, t: credentials.token, s: credentials.salt, v: "1.16.1", c: "tinysub", f: "json", }); Object.entries(params).forEach(([k, v]) => { if (Array.isArray(v)) v.forEach((val) => p.append(k, val)); else if (v !== undefined) p.append(k, String(v)); }); return p; }; export const buildUrl = (method: string, params: any = {}) => `${credentials!.server.replace(/\/$/, "")}/rest/${method}?${getParams(params)}`; const request = async (method: string, params: any = {}, isPost = false) => { const url = buildUrl(method, isPost ? {} : params); const options: RequestInit = isPost ? { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: getParams(params).toString(), } : {}; const res = await fetch(isPost ? url.split("?")[0] : url, options); const response = (await res.json())["subsonic-response"]; if (response.status === "failed") throw response.error?.message || "API error"; return response; }; export const songCache = new Map(); export const internSong = (s: any): Song => { if (!s || !s.id) return s; const cached = songCache.get(s.id); if (cached) { Object.assign(cached, s); return cached; } const fresh = $state({ ...s }); songCache.set(s.id, fresh); return fresh; }; export const api = { ping: () => request("ping"), artists: () => request("getArtists").then((response) => asArray(response.artists?.index).flatMap((index: any) => asArray(index.artist), ), ), playlists: () => request("getPlaylists").then((response) => asArray(response.playlists?.playlist), ), artist: (id: string) => request("getArtist", { id }).then((response) => asArray(response.artist.album), ), album: (id: string) => request("getAlbum", { id }).then((response) => asArray(response.album.song).map(internSong), ), playlist: (id: string) => request("getPlaylist", { id }).then((response) => asArray(response.playlist.entry).map(internSong), ), search: (query: string) => request("search3", { query }).then((response) => { const results = response.searchResult3 || {}; if (results.song) results.song = asArray(results.song).map(internSong); return results; }), stream: (id: string) => buildUrl("stream", { id }), art: (id: string, size = 128) => buildUrl("getCoverArt", { id, size }), star: (id: string) => request("star", { id }), unstar: (id: string) => request("unstar", { id }), setRating: (id: string, rating: number) => request("setRating", { id, rating }), lyricsById: (id: string) => request("getLyricsBySongId", { id }), lyrics: (artist: string, title: string) => request("getLyrics", { artist, title }), createPlaylist: (name?: string, songId?: string[], playlistId?: string) => request("createPlaylist", { name, songId, playlistId }, true), updatePlaylist: ( playlistId: string, name?: string, songIdToAdd?: string[], songIndexToRemove?: number[], isPublic?: boolean, ) => request( "updatePlaylist", { playlistId, name, songIdToAdd, songIndexToRemove, public: isPublic, }, true, ), deletePlaylist: (id: string) => request("deletePlaylist", { id }), savePlayQueue: (id: string[], current?: string, position?: number) => request("savePlayQueue", { id, current, position }, true), getPlayQueue: () => request("getPlayQueue"), scrobble: (id: string, submission = true) => request("scrobble", { id, submission }), }; export const createToken = (password: string) => { const salt = Math.random().toString(36).substring(2, 8); return { token: SparkMD5.hash(password + salt), salt }; };