import { Server } from '@app/models/settings' import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetArtistParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, GetPlaylistParams, GetPlaylistsParams, GetSongParams, GetTopSongsParams, ScrobbleParams, Search3Params, StarParams, StreamParams, } from '@app/subsonic/params' import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, GetPlaylistResponse, GetPlaylistsResponse, GetSongResponse, GetTopSongsResponse, NullResponse, Search3Response, SubsonicResponse, } from '@app/subsonic/responses' import toast from '@app/util/toast' import userAgent from '@app/util/userAgent' import { DOMParser } from '@xmldom/xmldom' export class SubsonicApiError extends Error { method: string code: string constructor(method: string, xml: Document) { const errorElement = xml.getElementsByTagName('error')[0] super(errorElement.getAttribute('message') as string) this.name = method this.method = method this.code = errorElement.getAttribute('code') as string } } type ResponseType = (xml: Document) => T type RequestParams = { getIndexes: GetIndexesParams getMusicDirectory: GetMusicDirectoryParams getAlbum: GetAlbumParams getArtistInfo: GetArtistInfoParams getArtistInfo2: GetArtistInfo2Params getArtist: GetArtistParams getTopSongs: GetTopSongsParams getSong: GetSongParams getAlbumList: GetAlbumListParams getAlbumList2: GetAlbumList2Params getPlaylists: GetPlaylistsParams getPlaylist: GetPlaylistParams scrobble: ScrobbleParams star: StarParams unstar: StarParams search3: Search3Params } const Methods = { ping: (xml => new NullResponse(xml)) as ResponseType, getArtists: (xml => new GetArtistsResponse(xml)) as ResponseType, getIndexes: (xml => new GetIndexesResponse(xml)) as ResponseType, getMusicDirectory: (xml => new GetMusicDirectoryResponse(xml)) as ResponseType, getAlbum: (xml => new GetAlbumResponse(xml)) as ResponseType, getArtistInfo: (xml => new GetArtistInfoResponse(xml)) as ResponseType, getArtistInfo2: (xml => new GetArtistInfo2Response(xml)) as ResponseType, getArtist: (xml => new GetArtistResponse(xml)) as ResponseType, getTopSongs: (xml => new GetTopSongsResponse(xml)) as ResponseType, getSong: (xml => new GetSongResponse(xml)) as ResponseType, getAlbumList: (xml => new GetAlbumListResponse(xml)) as ResponseType, getAlbumList2: (xml => new GetAlbumList2Response(xml)) as ResponseType, getPlaylists: (xml => new GetPlaylistsResponse(xml)) as ResponseType, getPlaylist: (xml => new GetPlaylistResponse(xml)) as ResponseType, scrobble: (xml => new NullResponse(xml)) as ResponseType, star: (xml => new NullResponse(xml)) as ResponseType, unstar: (xml => new NullResponse(xml)) as ResponseType, search3: (xml => new Search3Response(xml)) as ResponseType, } export class SubsonicApiClient { address: string username: string private params: URLSearchParams constructor(server: Server) { this.address = server.address this.username = server.username this.params = new URLSearchParams() this.params.append('u', server.username) if (server.usePlainPassword) { this.params.append('p', server.plainPassword) } else { this.params.append('t', server.token) this.params.append('s', server.salt) } this.params.append('v', '1.13.0') this.params.append('c', 'subtracks') } private buildUrl(method: string, params?: { [key: string]: any }): string { let query = this.params.toString() if (params) { const urlParams = this.obj2Params(params) if (urlParams) { query += '&' + urlParams.toString() } } // *.view was present on all method names in API 1.14.0 and earlier return `${this.address}/rest/${method}.view?${query}` } private async apiGetXml(method: string, params?: { [key: string]: any }): Promise { let text: string try { const response = await fetch(this.buildUrl(method, params), { headers: { 'User-Agent': userAgent }, }) text = await response.text() } catch (err) { toast(`Network error: ${this.address}`) throw err } const xml = new DOMParser().parseFromString(text) if (xml.documentElement.getAttribute('status') !== 'ok') { throw new SubsonicApiError(method, xml) } return xml } private obj2Params(obj: { [key: string]: any }): URLSearchParams | undefined { const keys = Object.keys(obj) if (keys.length === 0) { return undefined } const params = new URLSearchParams() for (const key of keys) { if (obj[key] === undefined || obj[key] === null) { continue } params.append(key, String(obj[key])) } return params } async fetch( method: T, ...params: T extends Extract ? [RequestParams[Extract]] : [] ): Promise> { const xml = await this.apiGetXml(method, params.length > 0 ? params[0] : undefined) return Methods[method](xml) as ReturnType } getCoverArtUri(params?: GetCoverArtParams): string { return this.buildUrl('getCoverArt', params) } streamUri(params: StreamParams): string { return this.buildUrl('stream', params) } }