albumlist apis implemented

This commit is contained in:
austinried 2021-06-20 09:44:22 +09:00
parent 71563985c0
commit ff94889644
6 changed files with 330 additions and 125 deletions

View File

@ -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]);
}
});
}

View File

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

View File

@ -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
View 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
View 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;

View File

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