Ability to query artists from navidrome with paging

This commit is contained in:
simojenki
2021-03-01 22:31:37 +11:00
parent 7a28bc5288
commit 3aa1056aa5
12 changed files with 341 additions and 126 deletions

View File

@@ -39,6 +39,8 @@ item | default value | description
---- | ------------- | ----------- ---- | ------------- | -----------
BONOB_PORT | 4534 | Default http port for bonob to listen on BONOB_PORT | 4534 | Default http port for bonob to listen on
BONOB_WEB_ADDRESS | http://localhost:4534 | Web address for bonob BONOB_WEB_ADDRESS | http://localhost:4534 | Web address for bonob
BONOB_SECRET | bonob | secret used for encrypting credentials
BONOB_SONOS_SEED_HOST | undefined | sonos device seed host for auto-discovery, or 'disabled' to turn off device discovery entirely BONOB_SONOS_SEED_HOST | undefined | sonos device seed host for auto-discovery, or 'disabled' to turn off device discovery entirely
BONOB_SONOS_SERVICE_NAME | bonob | service name for sonos BONOB_SONOS_SERVICE_NAME | bonob | service name for sonos
BONOS_SONOS_SERVICE_ID | 246 | service id for sonos BONOB_SONOS_SERVICE_ID | 246 | service id for sonos
BONOB_NAVIDROME_URL | http://localhost:4533 | URL for navidrome

View File

@@ -18,7 +18,7 @@ const app = server(
sonos(process.env["BONOB_SONOS_SEED_HOST"]), sonos(process.env["BONOB_SONOS_SEED_HOST"]),
bonob, bonob,
WEB_ADDRESS, WEB_ADDRESS,
new Navidrome(process.env["BONOB_NAVIDROME_URL"] || "http://localhost:4533", encryption()) new Navidrome(process.env["BONOB_NAVIDROME_URL"] || "http://localhost:4533", encryption(process.env["BONOB_SECRET"] || "bonob"))
); );
app.listen(PORT, () => { app.listen(PORT, () => {

View File

@@ -1,8 +1,7 @@
import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
const ALGORITHM = "aes-256-cbc" const ALGORITHM = "aes-256-cbc"
const IV = randomBytes(16); const IV = randomBytes(16);
const KEY = randomBytes(32);
export type Hash = { export type Hash = {
iv: string, iv: string,
@@ -14,17 +13,18 @@ export type Encryption = {
decrypt: (hash: Hash) => string decrypt: (hash: Hash) => string
} }
const encryption = (): Encryption => { const encryption = (secret: string): Encryption => {
const key = createHash('sha256').update(String(secret)).digest('base64').substr(0, 32);
return { return {
encrypt: (value: string) => { encrypt: (value: string) => {
const cipher = createCipheriv(ALGORITHM, KEY, IV); const cipher = createCipheriv(ALGORITHM, key, IV);
return { return {
iv: IV.toString("hex"), iv: IV.toString("hex"),
encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex") encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex")
}; };
}, },
decrypt: (hash: Hash) => { decrypt: (hash: Hash) => {
const decipher = createDecipheriv(ALGORITHM, KEY, Buffer.from(hash.iv, 'hex')); const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(hash.iv, 'hex'));
return Buffer.concat([decipher.update(Buffer.from(hash.encryptedData, 'hex')), decipher.final()]).toString(); return Buffer.concat([decipher.update(Buffer.from(hash.encryptedData, 'hex')), decipher.final()]).toString();
} }
} }

View File

@@ -1,4 +1,3 @@
export type Credentials = { username: string; password: string }; export type Credentials = { username: string; password: string };
export function isSuccess( export function isSuccess(
@@ -38,8 +37,19 @@ export type Album = {
name: string; name: string;
}; };
export type Paging = {
_index?: number;
_count?: number;
};
export interface MusicLibrary { export interface MusicLibrary {
artists(): Artist[]; artists({ _index, _count }: Paging): Promise<[Artist[], number]>;
artist(id: string): Artist; artist(id: string): Artist;
albums({ artistId, _index, _count }: { artistId?: string, _index?: number, _count?: number }): Album[]; albums({
artistId,
_index,
_count,
}: {
artistId?: string;
} & Paging): Promise<[Album[], number]>;
} }

View File

@@ -1,5 +1,5 @@
import { Md5 } from "ts-md5/dist/md5"; import { Md5 } from "ts-md5/dist/md5";
import { Credentials, MusicService } from "./music_service"; import { Credentials, MusicService, Paging, Album, Artist } from "./music_service";
import X2JS from "x2js"; import X2JS from "x2js";
import axios from "axios"; import axios from "axios";
@@ -25,6 +25,15 @@ export type SubsonicResponse = {
_status: string; _status: string;
}; };
export type GetArtistsResponse = SubsonicResponse & {
artists: {
index: {
artist: { _id: string; _name: string; _albumCount: string }[];
_name: string;
}[];
};
};
export type SubsonicError = SubsonicResponse & { export type SubsonicError = SubsonicResponse & {
error: { error: {
_code: string; _code: string;
@@ -47,11 +56,11 @@ export class Navidrome implements MusicService {
this.encryption = encryption; this.encryption = encryption;
} }
get = async ( get = async <T>(
{ username, password }: Credentials, { username, password }: Credentials,
path: string, path: string,
q: {} = {} q: {} = {}
): Promise<SubsonicResponse> => ): Promise<T> =>
axios axios
.get(`${this.url}${path}`, { .get(`${this.url}${path}`, {
params: { params: {
@@ -66,11 +75,11 @@ export class Navidrome implements MusicService {
.then((json) => json["subsonic-response"]) .then((json) => json["subsonic-response"])
.then((json) => { .then((json) => {
if (isError(json)) throw json.error._message; if (isError(json)) throw json.error._message;
else return json; else return (json as unknown) as T;
}); });
generateToken = async (credentials: Credentials) => generateToken = async (credentials: Credentials) =>
this.get(credentials, "/rest/ping.view").then((_) => ({ this.get(credentials, "/rest/ping.view").then(() => ({
authToken: Buffer.from( authToken: Buffer.from(
JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials))) JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials)))
).toString("base64"), ).toString("base64"),
@@ -85,17 +94,33 @@ export class Navidrome implements MusicService {
) )
); );
async login(_: string) { async login(token: string) {
// const credentials: Credentials = this.parseToken(token); const navidrome = this;
const credentials: Credentials = this.parseToken(token);
return Promise.resolve({ return Promise.resolve({
artists: () => [], artists: ({ _index, _count }: Paging): Promise<[Artist[], number]> =>
navidrome
.get<GetArtistsResponse>(credentials, "/rest/getArtists")
.then((it) => it.artists.index.flatMap((it) => it.artist))
.then((artists) =>
artists.map((it) => ({ id: it._id, name: it._name }))
)
.then((artists) => {
const i0 = _index || 0;
const i1 = _count ? i0 + _count : undefined;
return [artists.slice(i0, i1), artists.length];
}),
artist: (id: string) => ({ artist: (id: string) => ({
id, id,
name: id, name: id,
}), }),
albums: ({ artistId }: { artistId?: string }) => { albums: ({
artistId,
}: {
artistId?: string;
} & Paging): Promise<[Album[], number]> => {
console.log(artistId); console.log(artistId);
return []; return Promise.resolve([[], 0]);
}, },
}); });
} }

View File

@@ -6,7 +6,7 @@ import path from "path";
import logger from "./logger"; import logger from "./logger";
import { LinkCodes } from "./link_codes"; import { LinkCodes } from "./link_codes";
import { MusicLibrary, MusicService } from "./music_service"; import { MusicLibrary, MusicService } from "./music_service";
export const LOGIN_ROUTE = "/login"; export const LOGIN_ROUTE = "/login";
export const SOAP_PATH = "/ws/sonos"; export const SOAP_PATH = "/ws/sonos";
@@ -64,14 +64,18 @@ export type GetMetadataResponse = {
export function getMetadataResult({ export function getMetadataResult({
mediaCollection, mediaCollection,
index,
total,
}: { }: {
mediaCollection: any[] | undefined; mediaCollection: any[] | undefined;
index: number;
total: number;
}): GetMetadataResponse { }): GetMetadataResponse {
return { return {
getMetadataResult: { getMetadataResult: {
count: mediaCollection?.length || 0, count: mediaCollection?.length || 0,
index: 0, index,
total: mediaCollection?.length || 0, total,
mediaCollection: mediaCollection || [], mediaCollection: mediaCollection || [],
}, },
}; };
@@ -181,8 +185,8 @@ function bindSmapiSoapServiceToExpress(
id, id,
index, index,
count, count,
// recursive, }: // recursive,
}: { id: string; index: number; count: number; recursive: boolean }, { id: string; index: number; count: number; recursive: boolean },
_, _,
headers?: SoapyHeaders headers?: SoapyHeaders
) => { ) => {
@@ -194,21 +198,22 @@ function bindSmapiSoapServiceToExpress(
}, },
}; };
} }
const login = await musicService.login( const login = await musicService
headers.credentials.loginToken.token .login(headers.credentials.loginToken.token)
).catch(_ => { .catch((_) => {
throw { throw {
Fault: { Fault: {
faultcode: "Client.LoginUnauthorized", faultcode: "Client.LoginUnauthorized",
faultstring: "Credentials not found...", faultstring: "Credentials not found...",
}, },
}; };
}); });
const musicLibrary = login as MusicLibrary; const musicLibrary = login as MusicLibrary;
// const [type, typeId] = id.split(":"); const [type, typeId] = id.split(":");
const type = id; const paging = { _index: index, _count: count };
logger.debug(`Fetching type=${type}, typeId=${typeId}`);
switch (type) { switch (type) {
case "root": case "root":
return getMetadataResult({ return getMetadataResult({
@@ -216,27 +221,39 @@ function bindSmapiSoapServiceToExpress(
container({ id: "artists", title: "Artists" }), container({ id: "artists", title: "Artists" }),
container({ id: "albums", title: "Albums" }), container({ id: "albums", title: "Albums" }),
], ],
index: 0,
total: 2,
}); });
case "artists": case "artists":
return getMetadataResult({ return await musicLibrary
mediaCollection: musicLibrary.artists().map((it) => .artists(paging)
container({ .then(([artists, total]) =>
id: `artist:${it.id}`, getMetadataResult({
title: it.name, mediaCollection: artists.map((it) =>
container({
id: `artist:${it.id}`,
title: it.name,
})
),
index: paging._index,
total,
}) })
), );
});
case "albums": case "albums":
return getMetadataResult({ return await musicLibrary
mediaCollection: musicLibrary .albums(paging)
.albums({ _index: index, _count: count }) .then(([albums, total]) =>
.map((it) => getMetadataResult({
container({ mediaCollection: albums.map((it) =>
id: `album:${it.id}`, container({
title: it.name, id: `album:${it.id}`,
}) title: it.name,
), })
}); ),
index: paging._index,
total,
})
);
default: default:
throw `Unsupported id:${id}`; throw `Unsupported id:${id}`;
} }

View File

@@ -61,11 +61,11 @@ export function someCredentials(token: string): Credentials {
return { return {
loginToken: { loginToken: {
token, token,
householdId: "hh1" householdId: "hh1",
}, },
deviceId: "d1", deviceId: "d1",
deviceProvider: "dp1" deviceProvider: "dp1",
} };
} }
export type ArtistWithAlbums = Artist & { export type ArtistWithAlbums = Artist & {
@@ -96,3 +96,25 @@ export const MADONNA: ArtistWithAlbums = {
name: "Madonna", name: "Madonna",
albums: [], albums: [],
}; };
export const METALLICA: ArtistWithAlbums = {
id: uuid(),
name: "Metallica",
albums: [
{
id: uuid(),
name: "Ride the Lightening",
},
{
id: uuid(),
name: "Master of Puppets",
},
],
};
export const ALL_ALBUMS = [
...BOB_MARLEY.albums,
...BLONDIE.albums,
...MADONNA.albums,
...METALLICA.albums,
];

View File

@@ -1,7 +1,7 @@
import encryption from '../src/encryption'; import encryption from '../src/encryption';
describe("encrypt", () => { describe("encrypt", () => {
const e = encryption(); const e = encryption("secret squirrel");
it("can encrypt and decrypt", () => { it("can encrypt and decrypt", () => {
const value = "bobs your uncle" const value = "bobs your uncle"

View File

@@ -1,7 +1,13 @@
import { InMemoryMusicService } from "./in_memory_music_service"; import { InMemoryMusicService } from "./in_memory_music_service";
import { AuthSuccess, MusicLibrary } from "../src/music_service"; import { AuthSuccess, MusicLibrary } from "../src/music_service";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { BOB_MARLEY, MADONNA, BLONDIE } from "./builders"; import {
BOB_MARLEY,
MADONNA,
BLONDIE,
METALLICA,
ALL_ALBUMS,
} from "./builders";
describe("InMemoryMusicService", () => { describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService(); const service = new InMemoryMusicService();
@@ -31,7 +37,9 @@ describe("InMemoryMusicService", () => {
service.clear(); service.clear();
return expect(service.login(token.authToken)).rejects.toEqual("Invalid auth token"); return expect(service.login(token.authToken)).rejects.toEqual(
"Invalid auth token"
);
}); });
}); });
@@ -42,7 +50,7 @@ describe("InMemoryMusicService", () => {
beforeEach(async () => { beforeEach(async () => {
service.clear(); service.clear();
service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE); service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE, METALLICA);
service.hasUser(user); service.hasUser(user);
const token = (await service.generateToken(user)) as AuthSuccess; const token = (await service.generateToken(user)) as AuthSuccess;
@@ -50,12 +58,42 @@ describe("InMemoryMusicService", () => {
}); });
describe("artists", () => { describe("artists", () => {
it("should provide an array of artists", () => { describe("fetching all", () => {
expect(musicLibrary.artists()).toEqual([ it("should provide an array of artists", async () => {
{ id: BOB_MARLEY.id, name: BOB_MARLEY.name }, const artists = [
{ id: MADONNA.id, name: MADONNA.name }, { id: BOB_MARLEY.id, name: BOB_MARLEY.name },
{ id: BLONDIE.id, name: BLONDIE.name }, { id: MADONNA.id, name: MADONNA.name },
]); { id: BLONDIE.id, name: BLONDIE.name },
{ id: METALLICA.id, name: METALLICA.name },
];
expect(await musicLibrary.artists({})).toEqual([artists, 4]);
});
});
describe("fetching the second page", () => {
it("should provide an array of artists", async () => {
const artists = [
{ id: BLONDIE.id, name: BLONDIE.name },
{ id: METALLICA.id, name: METALLICA.name },
];
expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual([
artists,
4,
]);
});
});
describe("fetching the more items than fit on the second page", () => {
it("should provide an array of artists", async () => {
const artists = [
{ id: MADONNA.id, name: MADONNA.name },
{ id: BLONDIE.id, name: BLONDIE.name },
{ id: METALLICA.id, name: METALLICA.name },
];
expect(
await musicLibrary.artists({ _index: 1, _count: 50 })
).toEqual([artists, 4]);
});
}); });
}); });
@@ -84,67 +122,85 @@ describe("InMemoryMusicService", () => {
describe("albums", () => { describe("albums", () => {
describe("fetching with no filtering", () => { describe("fetching with no filtering", () => {
it("should return all the albums for all the artists", () => { it("should return all the albums for all the artists", async () => {
expect(musicLibrary.albums({})).toEqual([ expect(await musicLibrary.albums({})).toEqual([
...BOB_MARLEY.albums, ALL_ALBUMS,
...BLONDIE.albums, ALL_ALBUMS.length,
...MADONNA.albums,
]); ]);
}); });
}); });
describe("fetching for a single artist", () => { describe("fetching for a single artist", () => {
it("should return them all if the artist has some", () => { it("should return them all if the artist has some", async () => {
expect(musicLibrary.albums({ artistId: BLONDIE.id })).toEqual( expect(await musicLibrary.albums({ artistId: BLONDIE.id })).toEqual([
BLONDIE.albums BLONDIE.albums,
); BLONDIE.albums.length,
]);
}); });
it("should return empty list of the artists does not have any", () => { it("should return empty list of the artists does not have any", async () => {
expect(musicLibrary.albums({ artistId: MADONNA.id })).toEqual([]); expect(await musicLibrary.albums({ artistId: MADONNA.id })).toEqual([
[],
0,
]);
}); });
it("should return empty list if the artist id is not valid", () => { it("should return empty list if the artist id is not valid", async () => {
expect(musicLibrary.albums({ artistId: uuid() })).toEqual([]); expect(await musicLibrary.albums({ artistId: uuid() })).toEqual([
[],
0,
]);
}); });
}); });
describe("fetching with just index", () => { describe("fetching with just index", () => {
it("should return everything after", () => { it("should return everything after", async () => {
expect(musicLibrary.albums({ _index: 2 })).toEqual([ const albums = [
BOB_MARLEY.albums[2], BOB_MARLEY.albums[2],
BLONDIE.albums[0], ...BLONDIE.albums,
BLONDIE.albums[1], ...MADONNA.albums,
...METALLICA.albums,
];
expect(await musicLibrary.albums({ _index: 2 })).toEqual([
albums,
ALL_ALBUMS.length,
]); ]);
}); });
}); });
describe("fetching with just count", () => { describe("fetching with just count", () => {
it("should return first n items", () => { it("should return first n items", async () => {
expect(musicLibrary.albums({ _count: 3 })).toEqual([ const albums = [
BOB_MARLEY.albums[0], BOB_MARLEY.albums[0],
BOB_MARLEY.albums[1], BOB_MARLEY.albums[1],
BOB_MARLEY.albums[2], BOB_MARLEY.albums[2],
];
expect(await musicLibrary.albums({ _count: 3 })).toEqual([
albums,
ALL_ALBUMS.length,
]); ]);
}); });
}); });
describe("fetching with index and count", () => { describe("fetching with index and count", () => {
it("should be able to return the first page", () => { it("should be able to return the first page", async () => {
expect(musicLibrary.albums({ _index: 0, _count: 2 })).toEqual([ const albums = [BOB_MARLEY.albums[0], BOB_MARLEY.albums[1]];
BOB_MARLEY.albums[0], expect(await musicLibrary.albums({ _index: 0, _count: 2 })).toEqual([
BOB_MARLEY.albums[1], albums,
ALL_ALBUMS.length,
]); ]);
}); });
it("should be able to return the second page", () => { it("should be able to return the second page", async () => {
expect(musicLibrary.albums({ _index: 2, _count: 2 })).toEqual([ const albums = [BOB_MARLEY.albums[2], BLONDIE.albums[0]];
BOB_MARLEY.albums[2], expect(await musicLibrary.albums({ _index: 2, _count: 2 })).toEqual([
BLONDIE.albums[0], albums,
ALL_ALBUMS.length,
]); ]);
}); });
it("should be able to return the last page", () => { it("should be able to return the last page", async () => {
expect(musicLibrary.albums({ _index: 4, _count: 2 })).toEqual([ expect(await musicLibrary.albums({ _index: 5, _count: 2 })).toEqual([
BLONDIE.albums[1], METALLICA.albums,
ALL_ALBUMS.length,
]); ]);
}); });
}); });

View File

@@ -9,9 +9,9 @@ import {
AuthFailure, AuthFailure,
Artist, Artist,
MusicLibrary, MusicLibrary,
Paging,
} from "../src/music_service"; } from "../src/music_service";
const artistWithAlbumsToArtist = (it: ArtistWithAlbums): Artist => ({ const artistWithAlbumsToArtist = (it: ArtistWithAlbums): Artist => ({
id: it.id, id: it.id,
name: it.name, name: it.name,
@@ -52,9 +52,15 @@ export class InMemoryMusicService implements MusicService {
login(token: string): Promise<MusicLibrary> { login(token: string): Promise<MusicLibrary> {
const credentials = JSON.parse(token) as Credentials; const credentials = JSON.parse(token) as Credentials;
if (this.users[credentials.username] != credentials.password) return Promise.reject("Invalid auth token") if (this.users[credentials.username] != credentials.password)
return Promise.reject("Invalid auth token");
return Promise.resolve({ return Promise.resolve({
artists: () => this.artists.map(artistWithAlbumsToArtist), artists: ({ _index, _count }: Paging) => {
const i0 = _index || 0;
const i1 = _count ? i0 + _count : undefined;
const artists = this.artists.map(artistWithAlbumsToArtist);
return Promise.resolve([artists.slice(i0, i1), artists.length]);
},
artist: (id: string) => artist: (id: string) =>
pipe( pipe(
this.artists.find((it) => it.id === id), this.artists.find((it) => it.id === id),
@@ -73,7 +79,7 @@ export class InMemoryMusicService implements MusicService {
}) => { }) => {
const i0 = _index || 0; const i0 = _index || 0;
const i1 = _count ? i0 + _count : undefined; const i1 = _count ? i0 + _count : undefined;
return this.artists const albums = this.artists
.filter( .filter(
pipe( pipe(
O.fromNullable(artistId), O.fromNullable(artistId),
@@ -81,8 +87,8 @@ export class InMemoryMusicService implements MusicService {
O.getOrElse(() => all) O.getOrElse(() => all)
) )
) )
.flatMap((it) => it.albums) .flatMap((it) => it.albums);
.slice(i0, i1); return Promise.resolve([albums.slice(i0, i1), albums.length]);
}, },
}); });
} }

View File

@@ -23,7 +23,7 @@ describe("navidrome", () => {
const password = "pass1"; const password = "pass1";
const salt = "saltysalty"; const salt = "saltysalty";
const navidrome = new Navidrome(url, encryption()); const navidrome = new Navidrome(url, encryption("secret"));
const mockedRandomString = (randomString as unknown) as jest.Mock; const mockedRandomString = (randomString as unknown) as jest.Mock;
@@ -35,6 +35,14 @@ describe("navidrome", () => {
mockedRandomString.mockReturnValue(salt); mockedRandomString.mockReturnValue(salt);
}); });
const authParams = {
u: username,
t: t(password, salt),
s: salt,
v: "1.16.1",
c: "bonob",
};
describe("generateToken", () => { describe("generateToken", () => {
describe("when the credentials are valid", () => { describe("when the credentials are valid", () => {
it("should be able to generate a token and then login using it", async () => { it("should be able to generate a token and then login using it", async () => {
@@ -50,18 +58,9 @@ describe("navidrome", () => {
expect(token.nickname).toEqual(username); expect(token.nickname).toEqual(username);
expect(token.userId).toEqual(username); expect(token.userId).toEqual(username);
expect(axios.get).toHaveBeenCalledWith( expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
`${url}/rest/ping.view`, params: authParams,
{ });
params: {
u: username,
t: t(password, salt),
s: salt,
v: "1.16.1",
c: "bonob",
},
}
);
}); });
}); });
@@ -80,4 +79,66 @@ describe("navidrome", () => {
}); });
}); });
}); });
describe("getArtists", () => {
beforeEach(() => {
(axios.get as jest.Mock).mockResolvedValue({
status: 200,
data: `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<artists lastModified="1614586749000" ignoredArticles="The El La Los Las Le Les Os As O A">
<index name="#">
<artist id="2911b2d67a6b11eb804dd360a6225680" name="10 Planets" albumCount="22"></artist>
<artist id="3c0b9d7a7a6b11eb9773f398e6236ad6" name="1200 Ounces" albumCount="9"></artist>
</index>
<index name="A">
<artist id="3c5113007a6b11eb87173bfb9b07f9b1" name="AAAB" albumCount="2"></artist>
</index>
<index name="B">
<artist id="3ca781c27a6b11eb897ebbb5773603ad" name="BAAB" albumCount="2"></artist>
</index>
</artists>
</subsonic-response>`,
});
});
describe("when no paging specified", () => {
it("should return all the artists", async () => {
const artists = await navidrome
.generateToken({ username, password })
.then((it) => navidrome.login(it.authToken))
.then((it) => it.artists({}));
const expectedArtists = [
{ id: "2911b2d67a6b11eb804dd360a6225680", name: "10 Planets" },
{ id: "3c0b9d7a7a6b11eb9773f398e6236ad6", name: "1200 Ounces" },
{ id: "3c5113007a6b11eb87173bfb9b07f9b1", name: "AAAB" },
{ id: "3ca781c27a6b11eb897ebbb5773603ad", name: "BAAB" },
];
expect(artists).toEqual([expectedArtists, 4]);
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: authParams,
});
});
});
describe("when paging specified", () => {
it("should return only the correct page of artists", async () => {
const artists = await navidrome
.generateToken({ username, password })
.then((it) => navidrome.login(it.authToken))
.then((it) => it.artists({ _index: 1, _count: 2 }));
const expectedArtists = [
{ id: "3c0b9d7a7a6b11eb9773f398e6236ad6", name: "1200 Ounces" },
{ id: "3c5113007a6b11eb87173bfb9b07f9b1", name: "AAAB" },
];
expect(artists).toEqual([expectedArtists, 4]);
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: authParams,
});
});
});
});
}); });

View File

@@ -42,7 +42,9 @@ describe("service config", () => {
arrayAccessFormPaths: ["stringtables", "stringtables.stringtable"], arrayAccessFormPaths: ["stringtables", "stringtables.stringtable"],
}).xml2js(res.text); }).xml2js(res.text);
expect(strings.stringtables.stringtable[0].string[0]._stringId).toEqual("AppLinkMessage") expect(strings.stringtables.stringtable[0].string[0]._stringId).toEqual(
"AppLinkMessage"
);
}); });
}); });
}); });
@@ -50,11 +52,15 @@ describe("service config", () => {
describe("getMetadataResult", () => { describe("getMetadataResult", () => {
describe("when there are a zero mediaCollections", () => { describe("when there are a zero mediaCollections", () => {
it("should have zero count", () => { it("should have zero count", () => {
const result = getMetadataResult({ mediaCollection: [] }); const result = getMetadataResult({
mediaCollection: [],
index: 33,
total: 99,
});
expect(result.getMetadataResult.count).toEqual(0); expect(result.getMetadataResult.count).toEqual(0);
expect(result.getMetadataResult.index).toEqual(0); expect(result.getMetadataResult.index).toEqual(33);
expect(result.getMetadataResult.total).toEqual(0); expect(result.getMetadataResult.total).toEqual(99);
expect(result.getMetadataResult.mediaCollection).toEqual([]); expect(result.getMetadataResult.mediaCollection).toEqual([]);
}); });
}); });
@@ -62,11 +68,15 @@ describe("getMetadataResult", () => {
describe("when there are a number of mediaCollections", () => { describe("when there are a number of mediaCollections", () => {
it("should add correct counts", () => { it("should add correct counts", () => {
const mediaCollection = [{}, {}]; const mediaCollection = [{}, {}];
const result = getMetadataResult({ mediaCollection }); const result = getMetadataResult({
mediaCollection,
index: 22,
total: 3,
});
expect(result.getMetadataResult.count).toEqual(2); expect(result.getMetadataResult.count).toEqual(2);
expect(result.getMetadataResult.index).toEqual(0); expect(result.getMetadataResult.index).toEqual(22);
expect(result.getMetadataResult.total).toEqual(2); expect(result.getMetadataResult.total).toEqual(3);
expect(result.getMetadataResult.mediaCollection).toEqual(mediaCollection); expect(result.getMetadataResult.mediaCollection).toEqual(mediaCollection);
}); });
}); });
@@ -349,6 +359,8 @@ describe("api", () => {
container({ id: "artists", title: "Artists" }), container({ id: "artists", title: "Artists" }),
container({ id: "albums", title: "Albums" }), container({ id: "albums", title: "Albums" }),
], ],
index: 0,
total: 2,
}) })
); );
}); });
@@ -365,9 +377,9 @@ describe("api", () => {
}); });
expect(artists[0]).toEqual( expect(artists[0]).toEqual(
getMetadataResult({ getMetadataResult({
mediaCollection: [BLONDIE, BOB_MARLEY].map((it) => mediaCollection: [BLONDIE, BOB_MARLEY].map((it) => container({ id: `artist:${it.id}`, title: it.name })),
container({ id: `artist:${it.id}`, title: it.name }) index: 0,
), total: 2
}) })
); );
}); });
@@ -390,6 +402,8 @@ describe("api", () => {
].map((it) => ].map((it) =>
container({ id: `album:${it.id}`, title: it.name }) container({ id: `album:${it.id}`, title: it.name })
), ),
index: 0,
total: BLONDIE.albums.length + BOB_MARLEY.albums.length
}) })
); );
}); });
@@ -419,6 +433,8 @@ describe("api", () => {
title: BOB_MARLEY.albums[1]!.name, title: BOB_MARLEY.albums[1]!.name,
}), }),
], ],
index: 2,
total: BLONDIE.albums.length + BOB_MARLEY.albums.length
}) })
); );
}); });