From 234326135b7af96cb91b941e7ca515f45c632556 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:04:10 +0900 Subject: [PATCH] refactor api client to use string method hoping to use this for requestKey/deduping next --- app/state/library.ts | 49 ++++---- app/state/settings.ts | 2 +- app/state/trackplayer.ts | 2 +- app/subsonic/api.ts | 165 ++++++++------------------- app/subsonic/responses.ts | 230 ++++++++++++++++++++++---------------- 5 files changed, 210 insertions(+), 238 deletions(-) diff --git a/app/state/library.ts b/app/state/library.ts index 60c8222..553cad6 100644 --- a/app/state/library.ts +++ b/app/state/library.ts @@ -20,10 +20,9 @@ import { GetSongResponse, GetTopSongsResponse, Search3Response, - SubsonicResponse, } from '@app/subsonic/responses' import PromiseQueue from '@app/util/PromiseQueue' -import { reduceById, mergeById } from '@app/util/state' +import { mergeById, reduceById } from '@app/util/state' import { WritableDraft } from 'immer/dist/types/types-external' import pick from 'lodash.pick' @@ -99,9 +98,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetArtistsResponse try { - response = await client.getArtists() + response = await client.fetch('getArtists') } catch { return } @@ -121,9 +120,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetArtistResponse try { - response = await client.getArtist({ id }) + response = await client.fetch('getArtist', { id }) } catch { return } @@ -145,9 +144,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetArtistInfo2Response try { - response = await client.getArtistInfo2({ id }) + response = await client.fetch('getArtistInfo2', { id }) } catch { return } @@ -165,9 +164,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetTopSongsResponse try { - response = await client.getTopSongs({ artist: artistName, count: 50 }) + response = await client.fetch('getTopSongs', { artist: artistName, count: 50 }) } catch { return } @@ -189,9 +188,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetAlbumResponse try { - response = await client.getAlbum({ id }) + response = await client.fetch('getAlbum', { id }) } catch { return } @@ -215,9 +214,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetPlaylistsResponse try { - response = await client.getPlaylists() + response = await client.fetch('getPlaylists', {}) } catch { return } @@ -237,9 +236,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetPlaylistResponse try { - response = await client.getPlaylist({ id }) + response = await client.fetch('getPlaylist', { id }) } catch { return } @@ -263,9 +262,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return } - let response: SubsonicResponse + let response: GetSongResponse try { - response = await client.getSong({ id }) + response = await client.fetch('getSong', { id }) } catch { return } @@ -285,9 +284,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return [] } - let response: SubsonicResponse + let response: GetAlbumList2Response try { - response = await client.getAlbumList2(params) + response = await client.fetch('getAlbumList2', params) } catch { return [] } @@ -308,9 +307,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = return empty } - let response: SubsonicResponse + let response: Search3Response try { - response = await client.search3(params) + response = await client.fetch('search3', params) } catch { return empty } @@ -366,7 +365,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = }) try { - await client.star(params) + await client.fetch('star', params) } catch { set(state => { if (originalValue !== null) { @@ -405,7 +404,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = }) try { - await client.unstar(params) + await client.fetch('unstar', params) } catch { set(state => { if (originalValue !== null) { @@ -441,7 +440,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice = for (const id in albumsToGet) { songCoverArtQueue - .enqueue(() => client.getAlbum({ id })) + .enqueue(() => client.fetch('getAlbum', { id })) .then(res => { const album = mapAlbum(res.data.album) diff --git a/app/state/settings.ts b/app/state/settings.ts index b42cb52..bb2aa7d 100644 --- a/app/state/settings.ts +++ b/app/state/settings.ts @@ -220,7 +220,7 @@ export const createSettingsSlice = (set: SetStore, get: GetStore): SettingsSlice } try { - await client.ping() + await client.fetch('ping') return true } catch { return false diff --git a/app/state/trackplayer.ts b/app/state/trackplayer.ts index e11b366..6c09358 100644 --- a/app/state/trackplayer.ts +++ b/app/state/trackplayer.ts @@ -298,7 +298,7 @@ export const createTrackPlayerSlice = (set: SetStore, get: GetStore): TrackPlaye } try { - await client.scrobble({ id }) + await client.fetch('scrobble', { id }) } catch {} }, diff --git a/app/subsonic/api.ts b/app/subsonic/api.ts index 3552a72..a69fe27 100644 --- a/app/subsonic/api.ts +++ b/app/subsonic/api.ts @@ -32,6 +32,7 @@ import { GetPlaylistsResponse, GetSongResponse, GetTopSongsResponse, + NullResponse, Search3Response, SubsonicResponse, } from '@app/subsonic/responses' @@ -54,6 +55,48 @@ export class SubsonicApiError extends Error { } } +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 @@ -129,96 +172,14 @@ export class SubsonicApiClient { return params } - // - // System - // - - async ping(): Promise> { - const xml = await this.apiGetXml('ping') - return new SubsonicResponse(xml, null) + 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 } - // - // 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)) - } - - async getSong(params: GetSongParams): Promise> { - const xml = await this.apiGetXml('getSong', params) - return new SubsonicResponse(xml, new GetSongResponse(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 - // - getCoverArtUri(params?: GetCoverArtParams): string { return this.buildUrl('getCoverArt', params) } @@ -226,32 +187,4 @@ export class SubsonicApiClient { 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)) - } } diff --git a/app/subsonic/responses.ts b/app/subsonic/responses.ts index 41944ba..c7a3af7 100644 --- a/app/subsonic/responses.ts +++ b/app/subsonic/responses.ts @@ -12,128 +12,161 @@ import { export type ResponseStatus = 'ok' | 'failed' -export class SubsonicResponse { +export class SubsonicResponse { status: ResponseStatus version: string - data: T - constructor(xml: Document, data: T) { - this.data = data + constructor(xml: Document) { this.status = xml.documentElement.getAttribute('status') as ResponseStatus this.version = xml.documentElement.getAttribute('version') as string } } +export class NullResponse extends SubsonicResponse { + data = null +} + // // Browsing // -export class GetArtistsResponse { - ignoredArticles: string - artists: ArtistID3Element[] = [] +export class GetArtistsResponse extends SubsonicResponse { + data: { + ignoredArticles: string + artists: ArtistID3Element[] + } constructor(xml: Document) { - this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string + super(xml) - const artistElements = xml.getElementsByTagName('artist') - for (let i = 0; i < artistElements.length; i++) { - this.artists.push(new ArtistID3Element(artistElements[i])) + this.data = { + ignoredArticles: xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') || '', + artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistID3Element(i)), } } } -export class GetArtistResponse { - artist: ArtistID3Element - albums: AlbumID3Element[] = [] +export class GetArtistResponse extends SubsonicResponse { + data: { + artist: ArtistID3Element + albums: AlbumID3Element[] + } constructor(xml: Document) { - this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0]) + super(xml) - const albumElements = xml.getElementsByTagName('album') - for (let i = 0; i < albumElements.length; i++) { - this.albums.push(new AlbumID3Element(albumElements[i])) + this.data = { + artist: new ArtistID3Element(xml.getElementsByTagName('artist')[0]), + albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumID3Element(i)), } } } -export class GetIndexesResponse { - ignoredArticles: string - lastModified: number - artists: ArtistElement[] = [] +export class GetIndexesResponse extends SubsonicResponse { + data: { + ignoredArticles: string + lastModified: number + artists: ArtistElement[] + } constructor(xml: Document) { + super(xml) + const indexesElement = xml.getElementsByTagName('indexes')[0] - this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string - this.lastModified = parseInt(indexesElement.getAttribute('lastModified') as string, 10) - - const artistElements = xml.getElementsByTagName('artist') - for (let i = 0; i < artistElements.length; i++) { - this.artists.push(new ArtistElement(artistElements[i])) + this.data = { + ignoredArticles: indexesElement.getAttribute('ignoredArticles') || '', + lastModified: parseInt(indexesElement.getAttribute('lastModified') || '0', 10), + artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistElement(i)), } } } -export class GetArtistInfoResponse { - artistInfo: ArtistInfoElement - - constructor(xml: Document) { - this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]) +export class GetArtistInfoResponse extends SubsonicResponse { + data: { + artistInfo: ArtistInfoElement } -} - -export class GetArtistInfo2Response { - artistInfo: ArtistInfo2Element constructor(xml: Document) { - this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]) - } -} + super(xml) -export class GetMusicDirectoryResponse { - directory: DirectoryElement - children: ChildElement[] = [] - - constructor(xml: Document) { - this.directory = new DirectoryElement(xml.getElementsByTagName('directory')[0]) - - const childElements = xml.getElementsByTagName('child') - for (let i = 0; i < childElements.length; i++) { - this.children.push(new ChildElement(childElements[i])) + this.data = { + artistInfo: new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]), } } } -export class GetAlbumResponse { - album: AlbumID3Element - songs: ChildElement[] = [] +export class GetArtistInfo2Response extends SubsonicResponse { + data: { + artistInfo: ArtistInfo2Element + } constructor(xml: Document) { - this.album = new AlbumID3Element(xml.getElementsByTagName('album')[0]) + super(xml) - const childElements = xml.getElementsByTagName('song') - for (let i = 0; i < childElements.length; i++) { - this.songs.push(new ChildElement(childElements[i])) + this.data = { + artistInfo: new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]), } } } -export class GetTopSongsResponse { - songs: ChildElement[] = [] +export class GetMusicDirectoryResponse extends SubsonicResponse { + data: { + directory: DirectoryElement + children: ChildElement[] + } constructor(xml: Document) { - const childElements = xml.getElementsByTagName('song') - for (let i = 0; i < childElements.length; i++) { - this.songs.push(new ChildElement(childElements[i])) + super(xml) + + this.data = { + directory: new DirectoryElement(xml.getElementsByTagName('directory')[0]), + children: Array.from(xml.getElementsByTagName('child')).map(i => new ChildElement(i)), } } } -export class GetSongResponse { - song: ChildElement +export class GetAlbumResponse extends SubsonicResponse { + data: { + album: AlbumID3Element + songs: ChildElement[] + } constructor(xml: Document) { - this.song = new ChildElement(xml.getElementsByTagName('song')[0]) + super(xml) + + this.data = { + album: new AlbumID3Element(xml.getElementsByTagName('album')[0]), + songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)), + } + } +} + +export class GetTopSongsResponse extends SubsonicResponse { + data: { + songs: ChildElement[] + } + + constructor(xml: Document) { + super(xml) + + this.data = { + songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)), + } + } +} + +export class GetSongResponse extends SubsonicResponse { + data: { + song: ChildElement + } + + constructor(xml: Document) { + super(xml) + + this.data = { + song: new ChildElement(xml.getElementsByTagName('song')[0]), + } } } @@ -141,13 +174,16 @@ export class GetSongResponse { // Album/song lists // -class BaseGetAlbumListResponse { - albums: T[] = [] +class BaseGetAlbumListResponse extends SubsonicResponse { + data: { + albums: T[] + } - constructor(xml: Document, albumType: new (e: Element) => T) { - const albumElements = xml.getElementsByTagName('album') - for (let i = 0; i < albumElements.length; i++) { - this.albums.push(new albumType(albumElements[i])) + constructor(xml: Document, AlbumType: new (e: Element) => T) { + super(xml) + + this.data = { + albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumType(i)), } } } @@ -168,22 +204,31 @@ export class GetAlbumList2Response extends BaseGetAlbumListResponse new PlaylistElement(i)), } } } -export class GetPlaylistResponse { - playlist: PlaylistWithSongsElement +export class GetPlaylistResponse extends SubsonicResponse { + data: { + playlist: PlaylistWithSongsElement + } constructor(xml: Document) { - this.playlist = new PlaylistWithSongsElement(xml.getElementsByTagName('playlist')[0]) + super(xml) + + this.data = { + playlist: new PlaylistWithSongsElement(xml.getElementsByTagName('playlist')[0]), + } } } @@ -191,25 +236,20 @@ export class GetPlaylistResponse { // Searching // -export class Search3Response { - artists: ArtistID3Element[] = [] - albums: AlbumID3Element[] = [] - songs: ChildElement[] = [] +export class Search3Response extends SubsonicResponse { + data: { + artists: ArtistID3Element[] + albums: AlbumID3Element[] + songs: ChildElement[] + } constructor(xml: Document) { - const artistElements = xml.getElementsByTagName('artist') - for (let i = 0; i < artistElements.length; i++) { - this.artists.push(new ArtistID3Element(artistElements[i])) - } + super(xml) - const albumElements = xml.getElementsByTagName('album') - for (let i = 0; i < albumElements.length; i++) { - this.albums.push(new AlbumID3Element(albumElements[i])) - } - - const songElements = xml.getElementsByTagName('song') - for (let i = 0; i < songElements.length; i++) { - this.songs.push(new ChildElement(songElements[i])) + this.data = { + artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistID3Element(i)), + albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumID3Element(i)), + songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)), } } }