refactor api client to use string method

hoping to use this for requestKey/deduping next
This commit is contained in:
austinried
2022-03-24 15:04:10 +09:00
parent 8412c33923
commit 234326135b
5 changed files with 210 additions and 238 deletions

View File

@@ -20,10 +20,9 @@ import {
GetSongResponse, GetSongResponse,
GetTopSongsResponse, GetTopSongsResponse,
Search3Response, Search3Response,
SubsonicResponse,
} from '@app/subsonic/responses' } from '@app/subsonic/responses'
import PromiseQueue from '@app/util/PromiseQueue' 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 { WritableDraft } from 'immer/dist/types/types-external'
import pick from 'lodash.pick' import pick from 'lodash.pick'
@@ -99,9 +98,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetArtistsResponse> let response: GetArtistsResponse
try { try {
response = await client.getArtists() response = await client.fetch('getArtists')
} catch { } catch {
return return
} }
@@ -121,9 +120,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetArtistResponse> let response: GetArtistResponse
try { try {
response = await client.getArtist({ id }) response = await client.fetch('getArtist', { id })
} catch { } catch {
return return
} }
@@ -145,9 +144,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetArtistInfo2Response> let response: GetArtistInfo2Response
try { try {
response = await client.getArtistInfo2({ id }) response = await client.fetch('getArtistInfo2', { id })
} catch { } catch {
return return
} }
@@ -165,9 +164,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetTopSongsResponse> let response: GetTopSongsResponse
try { try {
response = await client.getTopSongs({ artist: artistName, count: 50 }) response = await client.fetch('getTopSongs', { artist: artistName, count: 50 })
} catch { } catch {
return return
} }
@@ -189,9 +188,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetAlbumResponse> let response: GetAlbumResponse
try { try {
response = await client.getAlbum({ id }) response = await client.fetch('getAlbum', { id })
} catch { } catch {
return return
} }
@@ -215,9 +214,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetPlaylistsResponse> let response: GetPlaylistsResponse
try { try {
response = await client.getPlaylists() response = await client.fetch('getPlaylists', {})
} catch { } catch {
return return
} }
@@ -237,9 +236,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetPlaylistResponse> let response: GetPlaylistResponse
try { try {
response = await client.getPlaylist({ id }) response = await client.fetch('getPlaylist', { id })
} catch { } catch {
return return
} }
@@ -263,9 +262,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return return
} }
let response: SubsonicResponse<GetSongResponse> let response: GetSongResponse
try { try {
response = await client.getSong({ id }) response = await client.fetch('getSong', { id })
} catch { } catch {
return return
} }
@@ -285,9 +284,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return [] return []
} }
let response: SubsonicResponse<GetAlbumList2Response> let response: GetAlbumList2Response
try { try {
response = await client.getAlbumList2(params) response = await client.fetch('getAlbumList2', params)
} catch { } catch {
return [] return []
} }
@@ -308,9 +307,9 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
return empty return empty
} }
let response: SubsonicResponse<Search3Response> let response: Search3Response
try { try {
response = await client.search3(params) response = await client.fetch('search3', params)
} catch { } catch {
return empty return empty
} }
@@ -366,7 +365,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
}) })
try { try {
await client.star(params) await client.fetch('star', params)
} catch { } catch {
set(state => { set(state => {
if (originalValue !== null) { if (originalValue !== null) {
@@ -405,7 +404,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
}) })
try { try {
await client.unstar(params) await client.fetch('unstar', params)
} catch { } catch {
set(state => { set(state => {
if (originalValue !== null) { if (originalValue !== null) {
@@ -441,7 +440,7 @@ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice =
for (const id in albumsToGet) { for (const id in albumsToGet) {
songCoverArtQueue songCoverArtQueue
.enqueue(() => client.getAlbum({ id })) .enqueue(() => client.fetch('getAlbum', { id }))
.then(res => { .then(res => {
const album = mapAlbum(res.data.album) const album = mapAlbum(res.data.album)

View File

@@ -220,7 +220,7 @@ export const createSettingsSlice = (set: SetStore, get: GetStore): SettingsSlice
} }
try { try {
await client.ping() await client.fetch('ping')
return true return true
} catch { } catch {
return false return false

View File

@@ -298,7 +298,7 @@ export const createTrackPlayerSlice = (set: SetStore, get: GetStore): TrackPlaye
} }
try { try {
await client.scrobble({ id }) await client.fetch('scrobble', { id })
} catch {} } catch {}
}, },

View File

@@ -32,6 +32,7 @@ import {
GetPlaylistsResponse, GetPlaylistsResponse,
GetSongResponse, GetSongResponse,
GetTopSongsResponse, GetTopSongsResponse,
NullResponse,
Search3Response, Search3Response,
SubsonicResponse, SubsonicResponse,
} from '@app/subsonic/responses' } from '@app/subsonic/responses'
@@ -54,6 +55,48 @@ export class SubsonicApiError extends Error {
} }
} }
type ResponseType<T extends SubsonicResponse> = (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<NullResponse>,
getArtists: (xml => new GetArtistsResponse(xml)) as ResponseType<GetArtistsResponse>,
getIndexes: (xml => new GetIndexesResponse(xml)) as ResponseType<GetIndexesResponse>,
getMusicDirectory: (xml => new GetMusicDirectoryResponse(xml)) as ResponseType<GetMusicDirectoryResponse>,
getAlbum: (xml => new GetAlbumResponse(xml)) as ResponseType<GetAlbumResponse>,
getArtistInfo: (xml => new GetArtistInfoResponse(xml)) as ResponseType<GetArtistInfoResponse>,
getArtistInfo2: (xml => new GetArtistInfo2Response(xml)) as ResponseType<GetArtistInfo2Response>,
getArtist: (xml => new GetArtistResponse(xml)) as ResponseType<GetArtistResponse>,
getTopSongs: (xml => new GetTopSongsResponse(xml)) as ResponseType<GetTopSongsResponse>,
getSong: (xml => new GetSongResponse(xml)) as ResponseType<GetSongResponse>,
getAlbumList: (xml => new GetAlbumListResponse(xml)) as ResponseType<GetAlbumListResponse>,
getAlbumList2: (xml => new GetAlbumList2Response(xml)) as ResponseType<GetAlbumList2Response>,
getPlaylists: (xml => new GetPlaylistsResponse(xml)) as ResponseType<GetPlaylistsResponse>,
getPlaylist: (xml => new GetPlaylistResponse(xml)) as ResponseType<GetPlaylistResponse>,
scrobble: (xml => new NullResponse(xml)) as ResponseType<NullResponse>,
star: (xml => new NullResponse(xml)) as ResponseType<NullResponse>,
unstar: (xml => new NullResponse(xml)) as ResponseType<NullResponse>,
search3: (xml => new Search3Response(xml)) as ResponseType<Search3Response>,
}
export class SubsonicApiClient { export class SubsonicApiClient {
address: string address: string
username: string username: string
@@ -129,96 +172,14 @@ export class SubsonicApiClient {
return params return params
} }
// async fetch<T extends keyof typeof Methods>(
// System method: T,
// ...params: T extends Extract<keyof RequestParams, T> ? [RequestParams[Extract<keyof RequestParams, T>]] : []
): Promise<ReturnType<typeof Methods[T]>> {
async ping(): Promise<SubsonicResponse<null>> { const xml = await this.apiGetXml(method, params.length > 0 ? params[0] : undefined)
const xml = await this.apiGetXml('ping') return Methods[method](xml) as ReturnType<typeof Methods[T]>
return new SubsonicResponse<null>(xml, null)
} }
//
// Browsing
//
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
const xml = await this.apiGetXml('getArtists')
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml))
}
async getIndexes(params?: GetIndexesParams): Promise<SubsonicResponse<GetIndexesResponse>> {
const xml = await this.apiGetXml('getIndexes', params)
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml))
}
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<SubsonicResponse<GetMusicDirectoryResponse>> {
const xml = await this.apiGetXml('getMusicDirectory', params)
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml))
}
async getAlbum(params: GetAlbumParams): Promise<SubsonicResponse<GetAlbumResponse>> {
const xml = await this.apiGetXml('getAlbum', params)
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml))
}
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
const xml = await this.apiGetXml('getArtistInfo', params)
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml))
}
async getArtistInfo2(params: GetArtistInfo2Params): Promise<SubsonicResponse<GetArtistInfo2Response>> {
const xml = await this.apiGetXml('getArtistInfo2', params)
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml))
}
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
const xml = await this.apiGetXml('getArtist', params)
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml))
}
async getTopSongs(params: GetTopSongsParams): Promise<SubsonicResponse<GetTopSongsResponse>> {
const xml = await this.apiGetXml('getTopSongs', params)
return new SubsonicResponse<GetTopSongsResponse>(xml, new GetTopSongsResponse(xml))
}
async getSong(params: GetSongParams): Promise<SubsonicResponse<GetSongResponse>> {
const xml = await this.apiGetXml('getSong', params)
return new SubsonicResponse<GetSongResponse>(xml, new GetSongResponse(xml))
}
//
// Album/song lists
//
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
const xml = await this.apiGetXml('getAlbumList', params)
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml))
}
async getAlbumList2(params: GetAlbumList2Params): Promise<SubsonicResponse<GetAlbumList2Response>> {
const xml = await this.apiGetXml('getAlbumList2', params)
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml))
}
//
// Playlists
//
async getPlaylists(params?: GetPlaylistsParams): Promise<SubsonicResponse<GetPlaylistsResponse>> {
const xml = await this.apiGetXml('getPlaylists', params)
return new SubsonicResponse<GetPlaylistsResponse>(xml, new GetPlaylistsResponse(xml))
}
async getPlaylist(params: GetPlaylistParams): Promise<SubsonicResponse<GetPlaylistResponse>> {
const xml = await this.apiGetXml('getPlaylist', params)
return new SubsonicResponse<GetPlaylistResponse>(xml, new GetPlaylistResponse(xml))
}
//
// Media retrieval
//
getCoverArtUri(params?: GetCoverArtParams): string { getCoverArtUri(params?: GetCoverArtParams): string {
return this.buildUrl('getCoverArt', params) return this.buildUrl('getCoverArt', params)
} }
@@ -226,32 +187,4 @@ export class SubsonicApiClient {
streamUri(params: StreamParams): string { streamUri(params: StreamParams): string {
return this.buildUrl('stream', params) return this.buildUrl('stream', params)
} }
//
// Media annotation
//
async scrobble(params: ScrobbleParams): Promise<SubsonicResponse<undefined>> {
const xml = await this.apiGetXml('scrobble', params)
return new SubsonicResponse<undefined>(xml, undefined)
}
async star(params: StarParams): Promise<SubsonicResponse<undefined>> {
const xml = await this.apiGetXml('star', params)
return new SubsonicResponse<undefined>(xml, undefined)
}
async unstar(params: StarParams): Promise<SubsonicResponse<undefined>> {
const xml = await this.apiGetXml('unstar', params)
return new SubsonicResponse<undefined>(xml, undefined)
}
//
// Searching
//
async search3(params: Search3Params): Promise<SubsonicResponse<Search3Response>> {
const xml = await this.apiGetXml('search3', params)
return new SubsonicResponse<Search3Response>(xml, new Search3Response(xml))
}
} }

View File

@@ -12,128 +12,161 @@ import {
export type ResponseStatus = 'ok' | 'failed' export type ResponseStatus = 'ok' | 'failed'
export class SubsonicResponse<T> { export class SubsonicResponse {
status: ResponseStatus status: ResponseStatus
version: string version: string
data: T
constructor(xml: Document, data: T) { constructor(xml: Document) {
this.data = data
this.status = xml.documentElement.getAttribute('status') as ResponseStatus this.status = xml.documentElement.getAttribute('status') as ResponseStatus
this.version = xml.documentElement.getAttribute('version') as string this.version = xml.documentElement.getAttribute('version') as string
} }
} }
export class NullResponse extends SubsonicResponse {
data = null
}
// //
// Browsing // Browsing
// //
export class GetArtistsResponse { export class GetArtistsResponse extends SubsonicResponse {
data: {
ignoredArticles: string ignoredArticles: string
artists: ArtistID3Element[] = [] artists: ArtistID3Element[]
}
constructor(xml: Document) { constructor(xml: Document) {
this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string super(xml)
const artistElements = xml.getElementsByTagName('artist') this.data = {
for (let i = 0; i < artistElements.length; i++) { ignoredArticles: xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') || '',
this.artists.push(new ArtistID3Element(artistElements[i])) artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistID3Element(i)),
} }
} }
} }
export class GetArtistResponse { export class GetArtistResponse extends SubsonicResponse {
data: {
artist: ArtistID3Element artist: ArtistID3Element
albums: AlbumID3Element[] = [] albums: AlbumID3Element[]
}
constructor(xml: Document) { constructor(xml: Document) {
this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0]) super(xml)
const albumElements = xml.getElementsByTagName('album') this.data = {
for (let i = 0; i < albumElements.length; i++) { artist: new ArtistID3Element(xml.getElementsByTagName('artist')[0]),
this.albums.push(new AlbumID3Element(albumElements[i])) albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumID3Element(i)),
} }
} }
} }
export class GetIndexesResponse { export class GetIndexesResponse extends SubsonicResponse {
data: {
ignoredArticles: string ignoredArticles: string
lastModified: number lastModified: number
artists: ArtistElement[] = [] artists: ArtistElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
super(xml)
const indexesElement = xml.getElementsByTagName('indexes')[0] const indexesElement = xml.getElementsByTagName('indexes')[0]
this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string this.data = {
this.lastModified = parseInt(indexesElement.getAttribute('lastModified') as string, 10) ignoredArticles: indexesElement.getAttribute('ignoredArticles') || '',
lastModified: parseInt(indexesElement.getAttribute('lastModified') || '0', 10),
const artistElements = xml.getElementsByTagName('artist') artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistElement(i)),
for (let i = 0; i < artistElements.length; i++) {
this.artists.push(new ArtistElement(artistElements[i]))
} }
} }
} }
export class GetArtistInfoResponse { export class GetArtistInfoResponse extends SubsonicResponse {
data: {
artistInfo: ArtistInfoElement artistInfo: ArtistInfoElement
}
constructor(xml: Document) { constructor(xml: Document) {
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]) super(xml)
this.data = {
artistInfo: new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]),
}
} }
} }
export class GetArtistInfo2Response { export class GetArtistInfo2Response extends SubsonicResponse {
data: {
artistInfo: ArtistInfo2Element artistInfo: ArtistInfo2Element
}
constructor(xml: Document) { constructor(xml: Document) {
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]) super(xml)
this.data = {
artistInfo: new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]),
}
} }
} }
export class GetMusicDirectoryResponse { export class GetMusicDirectoryResponse extends SubsonicResponse {
data: {
directory: DirectoryElement directory: DirectoryElement
children: ChildElement[] = [] children: ChildElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
this.directory = new DirectoryElement(xml.getElementsByTagName('directory')[0]) super(xml)
const childElements = xml.getElementsByTagName('child') this.data = {
for (let i = 0; i < childElements.length; i++) { directory: new DirectoryElement(xml.getElementsByTagName('directory')[0]),
this.children.push(new ChildElement(childElements[i])) children: Array.from(xml.getElementsByTagName('child')).map(i => new ChildElement(i)),
} }
} }
} }
export class GetAlbumResponse { export class GetAlbumResponse extends SubsonicResponse {
data: {
album: AlbumID3Element album: AlbumID3Element
songs: ChildElement[] = [] songs: ChildElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
this.album = new AlbumID3Element(xml.getElementsByTagName('album')[0]) super(xml)
const childElements = xml.getElementsByTagName('song') this.data = {
for (let i = 0; i < childElements.length; i++) { album: new AlbumID3Element(xml.getElementsByTagName('album')[0]),
this.songs.push(new ChildElement(childElements[i])) songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)),
} }
} }
} }
export class GetTopSongsResponse { export class GetTopSongsResponse extends SubsonicResponse {
songs: ChildElement[] = [] data: {
songs: ChildElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
const childElements = xml.getElementsByTagName('song') super(xml)
for (let i = 0; i < childElements.length; i++) {
this.songs.push(new ChildElement(childElements[i])) this.data = {
songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)),
} }
} }
} }
export class GetSongResponse { export class GetSongResponse extends SubsonicResponse {
data: {
song: ChildElement song: ChildElement
}
constructor(xml: Document) { constructor(xml: Document) {
this.song = new ChildElement(xml.getElementsByTagName('song')[0]) super(xml)
this.data = {
song: new ChildElement(xml.getElementsByTagName('song')[0]),
}
} }
} }
@@ -141,13 +174,16 @@ export class GetSongResponse {
// Album/song lists // Album/song lists
// //
class BaseGetAlbumListResponse<T> { class BaseGetAlbumListResponse<T> extends SubsonicResponse {
albums: T[] = [] data: {
albums: T[]
}
constructor(xml: Document, albumType: new (e: Element) => T) { constructor(xml: Document, AlbumType: new (e: Element) => T) {
const albumElements = xml.getElementsByTagName('album') super(xml)
for (let i = 0; i < albumElements.length; i++) {
this.albums.push(new albumType(albumElements[i])) this.data = {
albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumType(i)),
} }
} }
} }
@@ -168,22 +204,31 @@ export class GetAlbumList2Response extends BaseGetAlbumListResponse<AlbumID3Elem
// Playlists // Playlists
// //
export class GetPlaylistsResponse { export class GetPlaylistsResponse extends SubsonicResponse {
playlists: PlaylistElement[] = [] data: {
playlists: PlaylistElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
const playlistElements = xml.getElementsByTagName('playlist') super(xml)
for (let i = 0; i < playlistElements.length; i++) {
this.playlists.push(new PlaylistElement(playlistElements[i])) this.data = {
playlists: Array.from(xml.getElementsByTagName('playlist')).map(i => new PlaylistElement(i)),
} }
} }
} }
export class GetPlaylistResponse { export class GetPlaylistResponse extends SubsonicResponse {
data: {
playlist: PlaylistWithSongsElement playlist: PlaylistWithSongsElement
}
constructor(xml: Document) { 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 // Searching
// //
export class Search3Response { export class Search3Response extends SubsonicResponse {
artists: ArtistID3Element[] = [] data: {
albums: AlbumID3Element[] = [] artists: ArtistID3Element[]
songs: ChildElement[] = [] albums: AlbumID3Element[]
songs: ChildElement[]
}
constructor(xml: Document) { constructor(xml: Document) {
const artistElements = xml.getElementsByTagName('artist') super(xml)
for (let i = 0; i < artistElements.length; i++) {
this.artists.push(new ArtistID3Element(artistElements[i]))
}
const albumElements = xml.getElementsByTagName('album') this.data = {
for (let i = 0; i < albumElements.length; i++) { artists: Array.from(xml.getElementsByTagName('artist')).map(i => new ArtistID3Element(i)),
this.albums.push(new AlbumID3Element(albumElements[i])) albums: Array.from(xml.getElementsByTagName('album')).map(i => new AlbumID3Element(i)),
} songs: Array.from(xml.getElementsByTagName('song')).map(i => new ChildElement(i)),
const songElements = xml.getElementsByTagName('song')
for (let i = 0; i < songElements.length; i++) {
this.songs.push(new ChildElement(songElements[i]))
} }
} }
} }