mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
albumlist apis implemented
This commit is contained in:
parent
71563985c0
commit
ff94889644
@ -12,7 +12,7 @@ export class MusicDb extends DbStorage {
|
||||
CREATE TABLE artists (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
starred INTEGER NOT NULL,
|
||||
starred TEXT,
|
||||
coverArt TEXT
|
||||
);
|
||||
`);
|
||||
@ -20,7 +20,7 @@ export class MusicDb extends DbStorage {
|
||||
CREATE TABLE albums (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
starred INTEGER NOT NULL
|
||||
starred TEXT
|
||||
);
|
||||
`);
|
||||
});
|
||||
@ -45,7 +45,7 @@ export class MusicDb extends DbStorage {
|
||||
tx.executeSql(`
|
||||
INSERT INTO artists (id, name, starred, coverArt)
|
||||
VALUES (?, ?, ?, ?);
|
||||
`, [a.id, a.name, false, a.coverArt || null]);
|
||||
`, [a.id, a.name, null, a.coverArt || null]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -68,7 +68,7 @@ export class MusicDb extends DbStorage {
|
||||
tx.executeSql(`
|
||||
INSERT INTO albums (id, name, starred)
|
||||
VALUES (?, ?, ?);
|
||||
`, [a.id, a.name, false]);
|
||||
`, [a.id, a.name, null]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,21 @@
|
||||
import { DOMParser } from 'xmldom';
|
||||
import { GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, SubsonicResponse } from './response';
|
||||
import { GetAlbumList2Params, GetAlbumListParams, GetArtistInfo2Params, GetArtistInfoParams, GetIndexesParams } from './params';
|
||||
import { GetAlbumList2Response, GetAlbumListResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, SubsonicResponse } from './responses';
|
||||
|
||||
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;
|
||||
@ -19,13 +35,17 @@ export class SubsonicApiClient {
|
||||
this.params.append('c', 'subsonify-cool-unique-app-string')
|
||||
}
|
||||
|
||||
private async apiRequest(method: string, params?: URLSearchParams): Promise<Document> {
|
||||
private async apiRequest(method: string, params?: {[key: string]: any}): Promise<Document> {
|
||||
let query = this.params.toString();
|
||||
if (params !== undefined && Array.from(params as any).length > 0) {
|
||||
query += '&' + params.toString();
|
||||
if (params) {
|
||||
const urlParams = this.obj2Params(params);
|
||||
if (urlParams) {
|
||||
query += '&' + urlParams.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const url = `${this.address}/rest/${method}?${query}`;
|
||||
|
||||
console.log(url);
|
||||
|
||||
const response = await fetch(url);
|
||||
@ -41,82 +61,64 @@ export class SubsonicApiClient {
|
||||
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) {
|
||||
params.append(key, String(obj[key]));
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
//
|
||||
// System
|
||||
//
|
||||
|
||||
async ping(): Promise<SubsonicResponse<null>> {
|
||||
const xml = await this.apiRequest('ping');
|
||||
const response = new SubsonicResponse<null>(xml, null);
|
||||
|
||||
return response;
|
||||
return new SubsonicResponse<null>(xml, null);
|
||||
}
|
||||
|
||||
//
|
||||
// Browsing
|
||||
//
|
||||
|
||||
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
|
||||
const xml = await this.apiRequest('getArtists');
|
||||
const response = new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml));
|
||||
|
||||
return response;
|
||||
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml));
|
||||
}
|
||||
|
||||
async getIndexes(ifModifiedSince?: number): Promise<SubsonicResponse<GetIndexesResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
if (ifModifiedSince !== undefined) {
|
||||
params.append('ifModifiedSince', ifModifiedSince.toString());
|
||||
}
|
||||
|
||||
async getIndexes(params?: GetIndexesParams): Promise<SubsonicResponse<GetIndexesResponse>> {
|
||||
const xml = await this.apiRequest('getIndexes', params);
|
||||
const response = new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
|
||||
|
||||
console.log(response.status);
|
||||
console.log(response.version);
|
||||
console.log(response.data.lastModified);
|
||||
|
||||
return response;
|
||||
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
|
||||
}
|
||||
|
||||
async getArtistInfo(id: string, count?: number, includeNotPresent?: boolean): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('id', id);
|
||||
if (count !== undefined) {
|
||||
params.append('count', count.toString());
|
||||
}
|
||||
if (includeNotPresent !== undefined) {
|
||||
params.append('includeNotPresent', includeNotPresent.toString());
|
||||
}
|
||||
|
||||
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
||||
const xml = await this.apiRequest('getArtistInfo', params);
|
||||
const response = new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
|
||||
console.log(response.data);
|
||||
|
||||
return response;
|
||||
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
|
||||
}
|
||||
|
||||
async getArtistInfo2(id: string, count?: number, includeNotPresent?: boolean): Promise<SubsonicResponse<GetArtistInfo2Response>> {
|
||||
const params = new URLSearchParams();
|
||||
params.append('id', id);
|
||||
if (count !== undefined) {
|
||||
params.append('count', count.toString());
|
||||
}
|
||||
if (includeNotPresent !== undefined) {
|
||||
params.append('includeNotPresent', includeNotPresent.toString());
|
||||
}
|
||||
|
||||
async getArtistInfo2(params: GetArtistInfo2Params): Promise<SubsonicResponse<GetArtistInfo2Response>> {
|
||||
const xml = await this.apiRequest('getArtistInfo2', params);
|
||||
const response = new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
|
||||
console.log(response.data);
|
||||
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
|
||||
}
|
||||
|
||||
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;
|
||||
//
|
||||
// Album/song lists
|
||||
//
|
||||
|
||||
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
|
||||
const xml = await this.apiRequest('getAlbumList', params);
|
||||
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml));
|
||||
}
|
||||
|
||||
async getAlbumList2(params: GetAlbumList2Params): Promise<SubsonicResponse<GetAlbumList2Response>> {
|
||||
const xml = await this.apiRequest('getAlbumList2', params);
|
||||
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/subsonic/elements.ts
Normal file
171
src/subsonic/elements.ts
Normal file
@ -0,0 +1,171 @@
|
||||
function requiredString(e: Element, name: string): string {
|
||||
return e.getAttribute(name) as string;
|
||||
}
|
||||
|
||||
function optionalString(e: Element, name: string): string | undefined {
|
||||
return e.hasAttribute(name) ? requiredString(e, name) : undefined;
|
||||
}
|
||||
|
||||
function requiredBoolean(e: Element, name: string): boolean {
|
||||
return (e.getAttribute(name) as string).toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function optionalBoolean(e: Element, name: string): boolean | undefined {
|
||||
return e.hasAttribute(name) ? requiredBoolean(e, name) : undefined;
|
||||
}
|
||||
|
||||
function requiredInt(e: Element, name: string): number {
|
||||
return parseInt(e.getAttribute(name) as string);
|
||||
}
|
||||
|
||||
function optionalInt(e: Element, name: string): number | undefined {
|
||||
return e.hasAttribute(name) ? requiredInt(e, name) : undefined;
|
||||
}
|
||||
|
||||
function requiredFloat(e: Element, name: string): number {
|
||||
return parseFloat(e.getAttribute(name) as string);
|
||||
}
|
||||
|
||||
function optionalFloat(e: Element, name: string): number | undefined {
|
||||
return e.hasAttribute(name) ? requiredFloat(e, name) : undefined;
|
||||
}
|
||||
|
||||
function requiredDate(e: Element, name: string): Date {
|
||||
return new Date(e.getAttribute(name) as string);
|
||||
}
|
||||
|
||||
function optionalDate(e: Element, name: string): Date | undefined {
|
||||
return e.hasAttribute(name) ? requiredDate(e, name) : undefined;
|
||||
}
|
||||
|
||||
export class BaseArtistElement {
|
||||
id: string;
|
||||
name: string;
|
||||
starred?: Date;
|
||||
|
||||
constructor(e: Element) {
|
||||
this.id = requiredString(e, 'id');
|
||||
this.name = requiredString(e, 'name');
|
||||
this.starred = optionalDate(e, 'starred');
|
||||
}
|
||||
}
|
||||
|
||||
export class ArtistID3Element extends BaseArtistElement {
|
||||
coverArt?: string;
|
||||
albumCount?: number;
|
||||
|
||||
constructor(e: Element) {
|
||||
super(e);
|
||||
this.coverArt = optionalString(e, 'coverArt');
|
||||
this.albumCount = optionalInt(e, 'albumCount');
|
||||
}
|
||||
}
|
||||
|
||||
export class ArtistElement extends BaseArtistElement {
|
||||
userRating?: number;
|
||||
averageRating?: number;
|
||||
|
||||
constructor(e: Element) {
|
||||
super(e);
|
||||
this.userRating = optionalInt(e, 'userRating');
|
||||
this.averageRating = optionalFloat(e, 'averageRating');
|
||||
}
|
||||
}
|
||||
|
||||
export class ChildElement {
|
||||
id: string;
|
||||
parent?: string;
|
||||
isDir: boolean;
|
||||
title: string;
|
||||
album?: string;
|
||||
artist?: string;
|
||||
track?: number;
|
||||
year?: number;
|
||||
genre?: string;
|
||||
coverArt?: string;
|
||||
size?: number;
|
||||
contentType?: string;
|
||||
suffix?: string;
|
||||
transcodedContentType?: string;
|
||||
transcodedSuffix?: string;
|
||||
duration?: number;
|
||||
bitRate?: number;
|
||||
path?: string;
|
||||
isVideo?: boolean;
|
||||
userRating?: number;
|
||||
averageRating?: number;
|
||||
playCount?: number;
|
||||
discNumber?: number;
|
||||
created?: Date;
|
||||
starred?: Date;
|
||||
albumId?: string;
|
||||
artistId?: string;
|
||||
type?: string;
|
||||
bookmarkPosition?: number;
|
||||
originalWidth?: number;
|
||||
originalHeight?: number;
|
||||
|
||||
constructor(e: Element) {
|
||||
this.id = requiredString(e, 'id');
|
||||
this.parent = optionalString(e, 'parent');
|
||||
this.isDir = requiredBoolean(e, 'isDir');
|
||||
this.title = requiredString(e, 'title');
|
||||
this.album = optionalString(e, 'album');
|
||||
this.artist = optionalString(e, 'artist');
|
||||
this.track = optionalInt(e, 'track');
|
||||
this.year = optionalInt(e, 'year');
|
||||
this.genre = optionalString(e, 'genre');
|
||||
this.coverArt = optionalString(e, 'coverArt');
|
||||
this.size = optionalInt(e, 'size');
|
||||
this.contentType = optionalString(e, 'contentType');
|
||||
this.suffix = optionalString(e, 'suffix');
|
||||
this.transcodedContentType = optionalString(e, 'transcodedContentType');
|
||||
this.transcodedSuffix = optionalString(e, 'transcodedSuffix');
|
||||
this.duration = optionalInt(e, 'duration');
|
||||
this.bitRate = optionalInt(e, 'bitRate');
|
||||
this.path = optionalString(e, 'path');
|
||||
this.isVideo = optionalBoolean(e, 'isVideo');
|
||||
this.userRating = optionalInt(e, 'userRating');
|
||||
this.averageRating = optionalFloat(e, 'averageRating');
|
||||
this.playCount = optionalInt(e, 'playCount');
|
||||
this.discNumber = optionalInt(e, 'discNumber');
|
||||
this.created = optionalDate(e, 'created');
|
||||
this.starred = optionalDate(e, 'starred');
|
||||
this.albumId = optionalString(e, 'albumId');
|
||||
this.artistId = optionalString(e, 'artistId');
|
||||
this.type = optionalString(e, 'type');
|
||||
this.bookmarkPosition = optionalInt(e, 'bookmarkPosition');
|
||||
this.originalWidth = optionalInt(e, 'originalWidth');
|
||||
this.originalHeight = optionalInt(e, 'originalHeight');
|
||||
}
|
||||
}
|
||||
|
||||
export class AlbumID3Element {
|
||||
id: string;
|
||||
name: string;
|
||||
artist?: string;
|
||||
artistId?: string;
|
||||
coverArt?: string;
|
||||
songCount: number;
|
||||
duration: number;
|
||||
playCount?: number;
|
||||
created: Date;
|
||||
starred?: Date;
|
||||
year?: number;
|
||||
genre?: string;
|
||||
|
||||
constructor(e: Element) {
|
||||
this.id = requiredString(e, 'id');
|
||||
this.name = requiredString(e, 'name');
|
||||
this.artist = optionalString(e, 'artist');
|
||||
this.artistId = optionalString(e, 'artistId');
|
||||
this.coverArt = optionalString(e, 'coverArt');
|
||||
this.songCount = requiredInt(e, 'songCount');
|
||||
this.duration = requiredInt(e, 'duration');
|
||||
this.playCount = optionalInt(e, 'playCount');
|
||||
this.created = requiredDate(e, 'created');
|
||||
this.starred = optionalDate(e, 'starred');
|
||||
this.year = optionalInt(e, 'year');
|
||||
this.genre = optionalString(e, 'genre');
|
||||
}
|
||||
}
|
||||
47
src/subsonic/params.ts
Normal file
47
src/subsonic/params.ts
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// Browsing
|
||||
//
|
||||
|
||||
export type GetIndexesParams = {
|
||||
musicFolderId?: string;
|
||||
ifModifiedSince?: number;
|
||||
}
|
||||
|
||||
export type GetArtistInfoParams = {
|
||||
id: string;
|
||||
count?: number;
|
||||
includeNotPresent?: boolean;
|
||||
}
|
||||
|
||||
export type GetArtistInfo2Params = GetArtistInfoParams;
|
||||
|
||||
|
||||
//
|
||||
// Album/song lists
|
||||
//
|
||||
|
||||
export type GetAlbumList2Type = 'random' | 'newest' | 'frequent' | 'recent' | 'starred' | 'alphabeticalByName' | 'alphabeticalByArtist';
|
||||
export type GetAlbumListType = GetAlbumList2Type | ' highest';
|
||||
|
||||
export type GetAlbumList2TypeByYear = {
|
||||
type: 'byYear';
|
||||
fromYear: string;
|
||||
toYear: string;
|
||||
}
|
||||
|
||||
export type GetAlbumList2TypeByGenre = {
|
||||
type: 'byGenre';
|
||||
genre: string;
|
||||
}
|
||||
|
||||
export type GetAlbumList2Params = {
|
||||
type: GetAlbumList2Type;
|
||||
size?: number;
|
||||
offset?: number;
|
||||
fromYear?: string;
|
||||
toYear?: string;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
} | GetAlbumList2TypeByYear | GetAlbumList2TypeByGenre;
|
||||
|
||||
export type GetAlbumListParams = GetAlbumList2Params;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Artist, ArtistID3, BaseArtist } from "./element";
|
||||
import { AlbumID3Element, ArtistElement, ArtistID3Element, BaseArtistElement, ChildElement } from "./elements";
|
||||
|
||||
export type ResponseStatus = 'ok' | 'failed';
|
||||
|
||||
@ -14,16 +14,20 @@ export class SubsonicResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Browsing
|
||||
//
|
||||
|
||||
export class GetArtistsResponse {
|
||||
ignoredArticles: string;
|
||||
artists: ArtistID3[] = [];
|
||||
artists: ArtistID3Element[] = [];
|
||||
|
||||
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]));
|
||||
this.artists.push(new ArtistID3Element(artistElements[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,7 +35,7 @@ export class GetArtistsResponse {
|
||||
export class GetIndexesResponse {
|
||||
ignoredArticles: string;
|
||||
lastModified: number;
|
||||
artists: Artist[] = [];
|
||||
artists: ArtistElement[] = [];
|
||||
|
||||
constructor(xml: Document) {
|
||||
const indexesElement = xml.getElementsByTagName('indexes')[0];
|
||||
@ -41,12 +45,12 @@ export class GetIndexesResponse {
|
||||
|
||||
const artistElements = xml.getElementsByTagName('artist');
|
||||
for (let i = 0; i < artistElements.length; i++) {
|
||||
this.artists.push(new Artist(artistElements[i]));
|
||||
this.artists.push(new ArtistElement(artistElements[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BaseGetArtistInfoResponse<T extends BaseArtist> {
|
||||
class BaseGetArtistInfoResponse<T extends BaseArtistElement> {
|
||||
similarArtists: T[] = [];
|
||||
biography?: string;
|
||||
musicBrainzId?: string;
|
||||
@ -82,14 +86,41 @@ class BaseGetArtistInfoResponse<T extends BaseArtist> {
|
||||
}
|
||||
}
|
||||
|
||||
export class GetArtistInfoResponse extends BaseGetArtistInfoResponse<Artist> {
|
||||
export class GetArtistInfoResponse extends BaseGetArtistInfoResponse<ArtistElement> {
|
||||
constructor(xml: Document) {
|
||||
super(xml, Artist);
|
||||
super(xml, ArtistElement);
|
||||
}
|
||||
}
|
||||
|
||||
export class GetArtistInfo2Response extends BaseGetArtistInfoResponse<ArtistID3> {
|
||||
export class GetArtistInfo2Response extends BaseGetArtistInfoResponse<ArtistID3Element> {
|
||||
constructor(xml: Document) {
|
||||
super(xml, ArtistID3);
|
||||
super(xml, ArtistID3Element);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Album/song lists
|
||||
//
|
||||
|
||||
class BaseGetAlbumListResponse<T> {
|
||||
albums: T[] = [];
|
||||
|
||||
constructor(xml: Document, albumType: new (e: Element) => T) {
|
||||
const albumElements = xml.getElementsByTagName('album');
|
||||
for (let i = 0; i < albumElements.length; i++) {
|
||||
this.albums.push(new albumType(albumElements[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class GetAlbumListResponse extends BaseGetAlbumListResponse<ChildElement> {
|
||||
constructor(xml: Document) {
|
||||
super(xml, ChildElement);
|
||||
}
|
||||
}
|
||||
|
||||
export class GetAlbumList2Response extends BaseGetAlbumListResponse<AlbumID3Element> {
|
||||
constructor(xml: Document) {
|
||||
super(xml, AlbumID3Element);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user