import { DOMParser } from 'xmldom' import RNFS from 'react-native-fs' import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetArtistParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, GetPlaylistParams, GetPlaylistsParams, GetTopSongsParams, ScrobbleParams, Search3Params, StarParams, StreamParams, } from '@app/subsonic/params' import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, GetPlaylistResponse, GetPlaylistsResponse, GetTopSongsResponse, Search3Response, SubsonicResponse, } from '@app/subsonic/responses' import { Server } from '@app/models/settings' import paths from '@app/util/paths' import PromiseQueue from '@app/util/PromiseQueue' 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 } } const downloadQueue = new PromiseQueue(1) 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) this.params.append('t', server.token) this.params.append('s', server.salt) this.params.append('v', '1.15.0') this.params.append('c', 'subsonify-cool-unique-app-string') } 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() } } const url = `${this.address}/rest/${method}?${query}` // console.log(`${method}: ${url}`) return url } private async apiDownload(method: string, path: string, params?: { [key: string]: any }): Promise { const download = RNFS.downloadFile({ fromUrl: this.buildUrl(method, params), toFile: path, }).promise await downloadQueue.enqueue(() => download) await downloadQueue.enqueue(() => new Promise(resolve => setTimeout(resolve, 100))) return path } private async apiGetXml(method: string, params?: { [key: string]: any }): Promise { const response = await fetch(this.buildUrl(method, params)) const text = await response.text() // console.log(text) 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) { params.append(key, String(obj[key])) } return params } // // System // async ping(): Promise> { const xml = await this.apiGetXml('ping') return new SubsonicResponse(xml, null) } // // Browsing // async getArtists(): Promise> { const xml = await this.apiGetXml('getArtists') return new SubsonicResponse(xml, new GetArtistsResponse(xml)) } async getIndexes(params?: GetIndexesParams): Promise> { const xml = await this.apiGetXml('getIndexes', params) return new SubsonicResponse(xml, new GetIndexesResponse(xml)) } async getMusicDirectory(params: GetMusicDirectoryParams): Promise> { const xml = await this.apiGetXml('getMusicDirectory', params) return new SubsonicResponse(xml, new GetMusicDirectoryResponse(xml)) } async getAlbum(params: GetAlbumParams): Promise> { const xml = await this.apiGetXml('getAlbum', params) return new SubsonicResponse(xml, new GetAlbumResponse(xml)) } async getArtistInfo(params: GetArtistInfoParams): Promise> { const xml = await this.apiGetXml('getArtistInfo', params) return new SubsonicResponse(xml, new GetArtistInfoResponse(xml)) } async getArtistInfo2(params: GetArtistInfo2Params): Promise> { const xml = await this.apiGetXml('getArtistInfo2', params) return new SubsonicResponse(xml, new GetArtistInfo2Response(xml)) } async getArtist(params: GetArtistParams): Promise> { const xml = await this.apiGetXml('getArtist', params) return new SubsonicResponse(xml, new GetArtistResponse(xml)) } async getTopSongs(params: GetTopSongsParams): Promise> { const xml = await this.apiGetXml('getTopSongs', params) return new SubsonicResponse(xml, new GetTopSongsResponse(xml)) } // // Album/song lists // async getAlbumList(params: GetAlbumListParams): Promise> { const xml = await this.apiGetXml('getAlbumList', params) return new SubsonicResponse(xml, new GetAlbumListResponse(xml)) } async getAlbumList2(params: GetAlbumList2Params): Promise> { const xml = await this.apiGetXml('getAlbumList2', params) return new SubsonicResponse(xml, new GetAlbumList2Response(xml)) } // // Playlists // async getPlaylists(params?: GetPlaylistsParams): Promise> { const xml = await this.apiGetXml('getPlaylists', params) return new SubsonicResponse(xml, new GetPlaylistsResponse(xml)) } async getPlaylist(params: GetPlaylistParams): Promise> { const xml = await this.apiGetXml('getPlaylist', params) return new SubsonicResponse(xml, new GetPlaylistResponse(xml)) } // // Media retrieval // async getCoverArt(params: GetCoverArtParams): Promise { const path = `${paths.songCache}/${params.id}` return await this.apiDownload('getCoverArt', path, params) } getCoverArtUri(params?: GetCoverArtParams): string { return this.buildUrl('getCoverArt', params) } streamUri(params: StreamParams): string { return this.buildUrl('stream', params) } // // Media annotation // async scrobble(params: ScrobbleParams): Promise> { const xml = await this.apiGetXml('scrobble', params) return new SubsonicResponse(xml, undefined) } async star(params: StarParams): Promise> { const xml = await this.apiGetXml('star', params) return new SubsonicResponse(xml, undefined) } async unstar(params: StarParams): Promise> { const xml = await this.apiGetXml('unstar', params) return new SubsonicResponse(xml, undefined) } // // Searching // async search3(params: Search3Params): Promise> { const xml = await this.apiGetXml('search3', params) return new SubsonicResponse(xml, new Search3Response(xml)) } }