mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
* 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
240 lines
6.3 KiB
TypeScript
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))
|
|
}
|
|
}
|