From ff94889644867bf848c0c2f42f3f2f4b9c1b5d55 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:44:22 +0900 Subject: [PATCH] albumlist apis implemented --- src/storage/music.ts | 8 +- src/subsonic/api.ts | 132 ++++++++-------- src/subsonic/element.ts | 46 ------ src/subsonic/elements.ts | 171 +++++++++++++++++++++ src/subsonic/params.ts | 47 ++++++ src/subsonic/{response.ts => responses.ts} | 51 ++++-- 6 files changed, 330 insertions(+), 125 deletions(-) delete mode 100644 src/subsonic/element.ts create mode 100644 src/subsonic/elements.ts create mode 100644 src/subsonic/params.ts rename src/subsonic/{response.ts => responses.ts} (71%) diff --git a/src/storage/music.ts b/src/storage/music.ts index b5f88e0..f69b88d 100644 --- a/src/storage/music.ts +++ b/src/storage/music.ts @@ -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]); } }); } diff --git a/src/subsonic/api.ts b/src/subsonic/api.ts index dc1481c..aa909ef 100644 --- a/src/subsonic/api.ts +++ b/src/subsonic/api.ts @@ -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 { + private async apiRequest(method: string, params?: {[key: string]: any}): Promise { 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> { const xml = await this.apiRequest('ping'); - const response = new SubsonicResponse(xml, null); - - return response; + return new SubsonicResponse(xml, null); } + // + // Browsing + // + async getArtists(): Promise> { const xml = await this.apiRequest('getArtists'); - const response = new SubsonicResponse(xml, new GetArtistsResponse(xml)); - - return response; + return new SubsonicResponse(xml, new GetArtistsResponse(xml)); } - async getIndexes(ifModifiedSince?: number): Promise> { - const params = new URLSearchParams(); - if (ifModifiedSince !== undefined) { - params.append('ifModifiedSince', ifModifiedSince.toString()); - } - + async getIndexes(params?: GetIndexesParams): Promise> { const xml = await this.apiRequest('getIndexes', params); - const response = new SubsonicResponse(xml, new GetIndexesResponse(xml)); - - console.log(response.status); - console.log(response.version); - console.log(response.data.lastModified); - - return response; + return new SubsonicResponse(xml, new GetIndexesResponse(xml)); } - async getArtistInfo(id: string, count?: number, includeNotPresent?: boolean): Promise> { - 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> { const xml = await this.apiRequest('getArtistInfo', params); - const response = new SubsonicResponse(xml, new GetArtistInfoResponse(xml)); - console.log(response.data); - - return response; + return new SubsonicResponse(xml, new GetArtistInfoResponse(xml)); } - async getArtistInfo2(id: string, count?: number, includeNotPresent?: boolean): Promise> { - 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> { const xml = await this.apiRequest('getArtistInfo2', params); - const response = new SubsonicResponse(xml, new GetArtistInfo2Response(xml)); - console.log(response.data); + return new SubsonicResponse(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> { + const xml = await this.apiRequest('getAlbumList', params); + return new SubsonicResponse(xml, new GetAlbumListResponse(xml)); + } + + async getAlbumList2(params: GetAlbumList2Params): Promise> { + const xml = await this.apiRequest('getAlbumList2', params); + return new SubsonicResponse(xml, new GetAlbumList2Response(xml)); } } diff --git a/src/subsonic/element.ts b/src/subsonic/element.ts deleted file mode 100644 index 4c7af58..0000000 --- a/src/subsonic/element.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/subsonic/elements.ts b/src/subsonic/elements.ts new file mode 100644 index 0000000..e900790 --- /dev/null +++ b/src/subsonic/elements.ts @@ -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'); + } +} diff --git a/src/subsonic/params.ts b/src/subsonic/params.ts new file mode 100644 index 0000000..70242d6 --- /dev/null +++ b/src/subsonic/params.ts @@ -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; diff --git a/src/subsonic/response.ts b/src/subsonic/responses.ts similarity index 71% rename from src/subsonic/response.ts rename to src/subsonic/responses.ts index c25434c..90e97f1 100644 --- a/src/subsonic/response.ts +++ b/src/subsonic/responses.ts @@ -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 { } } +// +// 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 { +class BaseGetArtistInfoResponse { similarArtists: T[] = []; biography?: string; musicBrainzId?: string; @@ -82,14 +86,41 @@ class BaseGetArtistInfoResponse { } } -export class GetArtistInfoResponse extends BaseGetArtistInfoResponse { +export class GetArtistInfoResponse extends BaseGetArtistInfoResponse { constructor(xml: Document) { - super(xml, Artist); + super(xml, ArtistElement); } } -export class GetArtistInfo2Response extends BaseGetArtistInfoResponse { +export class GetArtistInfo2Response extends BaseGetArtistInfoResponse { constructor(xml: Document) { - super(xml, ArtistID3); + super(xml, ArtistID3Element); + } +} + +// +// Album/song lists +// + +class BaseGetAlbumListResponse { + 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 { + constructor(xml: Document) { + super(xml, ChildElement); + } +} + +export class GetAlbumList2Response extends BaseGetAlbumListResponse { + constructor(xml: Document) { + super(xml, AlbumID3Element); } }