austinried 081251061d
Library store refactor (#76)
* start of music store refactor

moving stuff into a state cache
better separate it from view logic

* added paginated list/album list

* reworked fetchAlbumList to remove ui state

refactored home screen to use new method
i broke playing songs somehow, JS thread goes into a loop

* don't reset parts manually, do it all at once

* fixed perf issue related to too many rerenders

rerenders were caused by strict equality check on object/array picks
switched artistInfo to new store
updated zustand and fixed deprecation warnings

* update typescript

and use workspace tsc version for vscode

* remove old artistInfo

* switched to new playlist w/songs

removed more unused stuff

* remove unused + (slightly) rework search

* refactor star

* use only original/large imges for covers/artist

fix view artist from context menu
add loading indicators to song list and artist views (show info we have right away)

* set starred/unstar assuming it works

and correct state on error

* reorg, remove old music slice files

* added back fix for song cover art

* sort artists by localCompare name

* update licenses

* fix now playing background grey bar

* update react-native-gesture-handler

for node-fetch security alert

* fix another gradient height grey bar issue

* update licenses again

* remove thumbnail cache

* rename to remove "Library" from methods

* Revert "remove thumbnail cache"

This reverts commit e0db4931f11bbf4cd8e73102d06505c6ae85f4a6.

* use ids for lists, pull state later

* Revert "use only original/large imges for covers/artist"

This reverts commit c9aea9065ce6ebe3c8b09c10dd74d4de153d76fd.

* deep equal ListItem props for now

this needs a bigger refactor

* use immer as middleware

* refactor api client to use string method

hoping to use this for requestKey/deduping next

* use thumbnails in list items

* Revert "refactor api client to use string method"

This reverts commit 234326135b7af96cb91b941e7ca515f45c632556.

* rename/cleanup

* store servers by id

* get rid of settings selectors

* renames for clarity

remove unused estimateContentLength setting

* remove trackplayer selectors

* fix migration for library filter settings

* fixed shuffle order reporting wrong track/queue

* removed the other selectors

* don't actually need es6/react for our state

* fix slow artist sort on star

localeCompare is too slow for large lists
2022-03-28 13:30:57 +09:00

240 lines
6.3 KiB
TypeScript

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,
} 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
}
}
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<Document> {
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
}
//
// System
//
async ping(): Promise<NullResponse> {
return new NullResponse(await this.apiGetXml('ping'))
}
//
// Browsing
//
async getArtists(): Promise<GetArtistsResponse> {
return new GetArtistsResponse(await this.apiGetXml('getArtists'))
}
async getIndexes(params?: GetIndexesParams): Promise<GetIndexesResponse> {
return new GetIndexesResponse(await this.apiGetXml('getIndexes', params))
}
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<GetMusicDirectoryResponse> {
return new GetMusicDirectoryResponse(await this.apiGetXml('getMusicDirectory', params))
}
async getAlbum(params: GetAlbumParams): Promise<GetAlbumResponse> {
return new GetAlbumResponse(await this.apiGetXml('getAlbum', params))
}
async getArtistInfo(params: GetArtistInfoParams): Promise<GetArtistInfoResponse> {
return new GetArtistInfoResponse(await this.apiGetXml('getArtistInfo', params))
}
async getArtistInfo2(params: GetArtistInfo2Params): Promise<GetArtistInfo2Response> {
return new GetArtistInfo2Response(await this.apiGetXml('getArtistInfo2', params))
}
async getArtist(params: GetArtistParams): Promise<GetArtistResponse> {
return new GetArtistResponse(await this.apiGetXml('getArtist', params))
}
async getTopSongs(params: GetTopSongsParams): Promise<GetTopSongsResponse> {
return new GetTopSongsResponse(await this.apiGetXml('getTopSongs', params))
}
async getSong(params: GetSongParams): Promise<GetSongResponse> {
return new GetSongResponse(await this.apiGetXml('getSong', params))
}
//
// Album/song lists
//
async getAlbumList(params: GetAlbumListParams): Promise<GetAlbumListResponse> {
return new GetAlbumListResponse(await this.apiGetXml('getAlbumList', params))
}
async getAlbumList2(params: GetAlbumList2Params): Promise<GetAlbumList2Response> {
return new GetAlbumList2Response(await this.apiGetXml('getAlbumList2', params))
}
//
// Playlists
//
async getPlaylists(params?: GetPlaylistsParams): Promise<GetPlaylistsResponse> {
return new GetPlaylistsResponse(await this.apiGetXml('getPlaylists', params))
}
async getPlaylist(params: GetPlaylistParams): Promise<GetPlaylistResponse> {
return new GetPlaylistResponse(await this.apiGetXml('getPlaylist', params))
}
//
// Media retrieval
//
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<NullResponse> {
return new NullResponse(await this.apiGetXml('scrobble', params))
}
async star(params: StarParams): Promise<NullResponse> {
return new NullResponse(await this.apiGetXml('star', params))
}
async unstar(params: StarParams): Promise<NullResponse> {
return new NullResponse(await this.apiGetXml('unstar', params))
}
//
// Searching
//
async search3(params: Search3Params): Promise<Search3Response> {
return new Search3Response(await this.apiGetXml('search3', params))
}
}