From b51d0eb685a926fff3e4309f8d517317d65241e8 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 15 Jun 2021 09:07:43 +0900 Subject: [PATCH] first pass api client --- App.tsx | 12 +++++ package-lock.json | 82 +++++++++++++++++++++++++++++++++- package.json | 6 ++- src/subsonic/api.ts | 106 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/subsonic/api.ts diff --git a/App.tsx b/App.tsx index 425cce7..7a8a592 100644 --- a/App.tsx +++ b/App.tsx @@ -11,3 +11,15 @@ const App = () => { } export default App; + +import { SubsonicApiClient } from './src/subsonic/api'; +import md5 from 'md5'; + +const password = 'test'; +const salt = 'salty'; +const token = md5(password + salt); + +const client = new SubsonicApiClient('http://navidrome.home', 'austin', token, salt); + +client.ping(); +client.getArtists(); diff --git a/package-lock.json b/package-lock.json index f08a670..ed070ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,21 @@ "name": "subsonify", "version": "0.0.1", "dependencies": { + "md5": "^2.3.0", "react": "17.0.1", "react-native": "0.64.1", - "recoil": "^0.3.1" + "recoil": "^0.3.1", + "xmldom": "^0.5.0" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/runtime": "^7.12.5", "@react-native-community/eslint-config": "^2.0.0", "@types/jest": "^26.0.23", + "@types/md5": "^2.3.0", "@types/react-native": "^0.64.5", "@types/react-test-renderer": "^16.9.2", + "@types/xmldom": "^0.1.30", "babel-jest": "^26.6.3", "eslint": "^7.14.0", "jest": "^26.6.3", @@ -3071,6 +3075,15 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "node_modules/@types/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-556YJ7ejzxIqSSxzyGGpctuZOarNZJt/zlEkhmmDc1f/slOEANHuwu2ZX7YaZ40rMiWoxt8GvAhoDpW1cmSy6A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "15.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", @@ -3146,6 +3159,12 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "node_modules/@types/xmldom": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.30.tgz", + "integrity": "sha512-edqgAFXMEtVvaBZ3YnhamvmrHjoYpuxETmnb0lbTZmf/dXpAsO9ZKotUO4K2rn2SIZBDFCMOuA7fOe0H6dRZcA==", + "dev": true + }, "node_modules/@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -4222,6 +4241,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "engines": { + "node": "*" + } + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -4625,6 +4652,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "engines": { + "node": "*" + } + }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -9108,6 +9143,16 @@ "node": ">=0.10.0" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -15576,6 +15621,15 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-556YJ7ejzxIqSSxzyGGpctuZOarNZJt/zlEkhmmDc1f/slOEANHuwu2ZX7YaZ40rMiWoxt8GvAhoDpW1cmSy6A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "15.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", @@ -15653,6 +15707,12 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/xmldom": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.30.tgz", + "integrity": "sha512-edqgAFXMEtVvaBZ3YnhamvmrHjoYpuxETmnb0lbTZmf/dXpAsO9ZKotUO4K2rn2SIZBDFCMOuA7fOe0H6dRZcA==", + "dev": true + }, "@types/yargs": { "version": "15.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", @@ -16436,6 +16496,11 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -16770,6 +16835,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", @@ -20135,6 +20205,16 @@ "object-visit": "^1.0.0" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 9e52bbd..7e05b6f 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,21 @@ "lint": "eslint . --ext .js,.jsx,.ts,.tsx" }, "dependencies": { + "md5": "^2.3.0", "react": "17.0.1", "react-native": "0.64.1", - "recoil": "^0.3.1" + "recoil": "^0.3.1", + "xmldom": "^0.5.0" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/runtime": "^7.12.5", "@react-native-community/eslint-config": "^2.0.0", "@types/jest": "^26.0.23", + "@types/md5": "^2.3.0", "@types/react-native": "^0.64.5", "@types/react-test-renderer": "^16.9.2", + "@types/xmldom": "^0.1.30", "babel-jest": "^26.6.3", "eslint": "^7.14.0", "jest": "^26.6.3", diff --git a/src/subsonic/api.ts b/src/subsonic/api.ts new file mode 100644 index 0000000..f408442 --- /dev/null +++ b/src/subsonic/api.ts @@ -0,0 +1,106 @@ +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, + }); + } + } +}