first pass api client

This commit is contained in:
austinried 2021-06-15 09:07:43 +09:00
parent 1b78acd5fa
commit b51d0eb685
4 changed files with 204 additions and 2 deletions

12
App.tsx
View File

@ -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();

82
package-lock.json generated
View File

@ -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",

View File

@ -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",

106
src/subsonic/api.ts Normal file
View File

@ -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<Document> {
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<SubsonicResponse<null>> {
const xml = await this.apiRequest('ping');
const response = new SubsonicResponse<null>(xml, null);
console.log(response.status);
console.log(response.version);
return response;
}
public async getArtists(): Promise<SubsonicResponse<ArtistID3Response>> {
const xml = await this.apiRequest('getArtists');
const data = new ArtistID3Response(xml);
const response = new SubsonicResponse<ArtistID3Response>(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<T> {
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,
});
}
}
}