From 5ce3b5bcdfba61f4f56f036716b722479a222759 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 15 Jun 2021 10:34:15 +0900 Subject: [PATCH] reorg & impl getIndexes --- App.tsx | 5 +- src/subsonic/api.ts | 106 --------------------------------------- src/subsonic/client.ts | 91 +++++++++++++++++++++++++++++++++ src/subsonic/element.ts | 46 +++++++++++++++++ src/subsonic/response.ts | 47 +++++++++++++++++ 5 files changed, 186 insertions(+), 109 deletions(-) delete mode 100644 src/subsonic/api.ts create mode 100644 src/subsonic/client.ts create mode 100644 src/subsonic/element.ts create mode 100644 src/subsonic/response.ts diff --git a/App.tsx b/App.tsx index 7a8a592..81b05eb 100644 --- a/App.tsx +++ b/App.tsx @@ -12,7 +12,7 @@ const App = () => { export default App; -import { SubsonicApiClient } from './src/subsonic/api'; +import { SubsonicApiClient } from './src/subsonic/client'; import md5 from 'md5'; const password = 'test'; @@ -21,5 +21,4 @@ const token = md5(password + salt); const client = new SubsonicApiClient('http://navidrome.home', 'austin', token, salt); -client.ping(); -client.getArtists(); +client.getIndexes(); diff --git a/src/subsonic/api.ts b/src/subsonic/api.ts deleted file mode 100644 index f408442..0000000 --- a/src/subsonic/api.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { DOMParser } from 'xmldom'; - -export class SubsonicApiClient { - public address: string; - public username: string; - - private params: URLSearchParams - - constructor(address: string, username: string, token: string, salt: string) { - this.address = address; - this.username = username; - - this.params = new URLSearchParams(); - this.params.append('u', username); - this.params.append('t', token); - this.params.append('s', salt); - this.params.append('v', '1.15.0'); - this.params.append('c', 'subsonify-cool-unique-app-string') - } - - private async apiRequest(method: string, params?: URLSearchParams): Promise { - const url = `${this.address}/rest/${method}?${(params || this.params).toString()}`; - - const response = await fetch(url); - const text = await response.text(); - - console.log(text); - - const xml = new DOMParser().parseFromString(text); - if (xml.documentElement.getAttribute('status') !== 'ok') { - throw new SubsonicApiException(); - } - - return xml; - } - - public async ping(): Promise> { - const xml = await this.apiRequest('ping'); - const response = new SubsonicResponse(xml, null); - - console.log(response.status); - console.log(response.version); - - return response; - } - - public async getArtists(): Promise> { - const xml = await this.apiRequest('getArtists'); - const data = new ArtistID3Response(xml); - const response = new SubsonicResponse(xml, data); - - console.log(response.status); - console.log(response.version); - console.log(response.data.artists); - - return response; - } -} - -class SubsonicApiException { - -} - -type ResponseStatus = 'ok' | 'failed'; - -class SubsonicResponse { - public status: ResponseStatus; - public version: string; - public data: T; - - constructor(xml: Document, data: T) { - this.data = data; - this.status = xml.documentElement.getAttribute('status') as ResponseStatus; - this.version = xml.documentElement.getAttribute('version') as string; - } -} - -interface ArtistID3 { - id: string; - name: string; - coverArt?: string; - albumCount: number; - starred?: Date; -} - -class ArtistID3Response { - public ignoredArticles: string; - public artists: ArtistID3[] = []; - - constructor(xml: Document) { - this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string; - - const artistElements = xml.getElementsByTagName('artist'); - for (let i = 0; i < artistElements.length; i++) { - const a = artistElements[i]; - - this.artists.push({ - id: a.getAttribute('id') as string, - name: a.getAttribute('name') as string, - coverArt: a.getAttribute('coverArt') || undefined, - albumCount: parseInt(a.getAttribute('albumCount') as string), - starred: a.getAttribute('starred') ? new Date(a.getAttribute('starred') as string) : undefined, - }); - } - } -} diff --git a/src/subsonic/client.ts b/src/subsonic/client.ts new file mode 100644 index 0000000..e802441 --- /dev/null +++ b/src/subsonic/client.ts @@ -0,0 +1,91 @@ +import { DOMParser } from 'xmldom'; +import { GetArtistsResponse, GetIndexesResponse, SubsonicResponse } from './response'; + +export class SubsonicApiClient { + address: string; + username: string; + + private params: URLSearchParams + + constructor(address: string, username: string, token: string, salt: string) { + this.address = address; + this.username = username; + + this.params = new URLSearchParams(); + this.params.append('u', username); + this.params.append('t', token); + this.params.append('s', salt); + this.params.append('v', '1.15.0'); + this.params.append('c', 'subsonify-cool-unique-app-string') + } + + private async apiRequest(method: string, params?: URLSearchParams): Promise { + let query = this.params.toString(); + if (params !== undefined && Array.from(params as any).length > 0) { + query += '&' + params.toString(); + } + + const url = `${this.address}/rest/${method}?${query}`; + console.log(url); + + const response = await fetch(url); + 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; + } + + async ping(): Promise> { + const xml = await this.apiRequest('ping'); + const response = new SubsonicResponse(xml, null); + + return response; + } + + async getArtists(): Promise> { + const xml = await this.apiRequest('getArtists'); + const data = new GetArtistsResponse(xml); + const response = new SubsonicResponse(xml, data); + + return response; + } + + async getIndexes(ifModifiedSince?: number): Promise> { + const params = new URLSearchParams(); + console.log(params); + if (ifModifiedSince !== undefined) { + params.append('ifModifiedSince', ifModifiedSince.toString()); + } + + const xml = await this.apiRequest('getIndexes', params); + const data = new GetIndexesResponse(xml); + const response = new SubsonicResponse(xml, data); + + console.log(response.status); + console.log(response.version); + console.log(response.data.lastModified); + + return response; + } +} + +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; + } +} diff --git a/src/subsonic/element.ts b/src/subsonic/element.ts new file mode 100644 index 0000000..4c7af58 --- /dev/null +++ b/src/subsonic/element.ts @@ -0,0 +1,46 @@ +export class BaseArtist { + id: string; + name: string; + starred?: string; + + constructor(e: Element) { + this.id = e.getAttribute('id') as string; + this.name = e.getAttribute('name') as string; + + if (e.getAttribute('starred') !== null) { + this.starred = e.getAttribute('starred') as string; + } + } +} + +export class ArtistID3 extends BaseArtist { + coverArt?: string; + albumCount?: number; + + constructor(e: Element) { + super(e); + + if (e.getAttribute('coverArt') !== null) { + this.coverArt = e.getAttribute('coverArt') as string; + } + if (e.getAttribute('albumCount') !== null) { + this.albumCount = parseInt(e.getAttribute('albumCount') as string); + } + } +} + +export class Artist extends BaseArtist { + userRating?: number; + averageRating?: number; + + constructor(e: Element) { + super(e); + + if (e.getAttribute('userRating') !== null) { + this.userRating = parseInt(e.getAttribute('userRating') as string); + } + if (e.getAttribute('averageRating') !== null) { + this.averageRating = parseFloat(e.getAttribute('averageRating') as string); + } + } +} diff --git a/src/subsonic/response.ts b/src/subsonic/response.ts new file mode 100644 index 0000000..0040922 --- /dev/null +++ b/src/subsonic/response.ts @@ -0,0 +1,47 @@ +import { Artist, ArtistID3 } from "./element"; + +export type ResponseStatus = 'ok' | 'failed'; + +export class SubsonicResponse { + status: ResponseStatus; + version: string; + data: T; + + constructor(xml: Document, data: T) { + this.data = data; + this.status = xml.documentElement.getAttribute('status') as ResponseStatus; + this.version = xml.documentElement.getAttribute('version') as string; + } +} + +export class GetArtistsResponse { + ignoredArticles: string; + artists: ArtistID3[] = []; + + constructor(xml: Document) { + this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string; + + const artistElements = xml.getElementsByTagName('artist'); + for (let i = 0; i < artistElements.length; i++) { + this.artists.push(new ArtistID3(artistElements[i])); + } + } +} + +export class GetIndexesResponse { + ignoredArticles: string; + lastModified: number; + artists: Artist[] = []; + + constructor(xml: Document) { + const indexesElement = xml.getElementsByTagName('indexes')[0]; + + this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string; + this.lastModified = parseInt(indexesElement.getAttribute('lastModified') as string); + + const artistElements = xml.getElementsByTagName('artist'); + for (let i = 0; i < artistElements.length; i++) { + this.artists.push(new Artist(artistElements[i])); + } + } +}