From 3aa1056aa52aa6e37bdddad3061d6e5855ef2b31 Mon Sep 17 00:00:00 2001 From: simojenki Date: Mon, 1 Mar 2021 22:31:37 +1100 Subject: [PATCH] Ability to query artists from navidrome with paging --- README.md | 4 +- src/app.ts | 2 +- src/encryption.ts | 10 +- src/music_service.ts | 16 ++- src/navidrome.ts | 45 +++++++-- src/smapi.ts | 85 +++++++++------- tests/builders.ts | 28 +++++- tests/encryption.test.ts | 2 +- tests/in_memory_music_service.test.ts | 134 ++++++++++++++++++-------- tests/in_memory_music_service.ts | 18 ++-- tests/navidrome.test.ts | 87 ++++++++++++++--- tests/smapi.test.ts | 36 +++++-- 12 files changed, 341 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index db1ab02..9c193a5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ item | default value | description ---- | ------------- | ----------- BONOB_PORT | 4534 | Default http port for bonob to listen on 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_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 diff --git a/src/app.ts b/src/app.ts index 1959c39..e103e9d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,7 +18,7 @@ const app = server( sonos(process.env["BONOB_SONOS_SEED_HOST"]), bonob, 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, () => { diff --git a/src/encryption.ts b/src/encryption.ts index c922ca6..7c2ebfb 100644 --- a/src/encryption.ts +++ b/src/encryption.ts @@ -1,8 +1,7 @@ -import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; +import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto"; const ALGORITHM = "aes-256-cbc" const IV = randomBytes(16); -const KEY = randomBytes(32); export type Hash = { iv: string, @@ -14,17 +13,18 @@ export type Encryption = { 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 { encrypt: (value: string) => { - const cipher = createCipheriv(ALGORITHM, KEY, IV); + const cipher = createCipheriv(ALGORITHM, key, IV); return { iv: IV.toString("hex"), encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex") }; }, 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(); } } diff --git a/src/music_service.ts b/src/music_service.ts index e57db4d..11b550c 100644 --- a/src/music_service.ts +++ b/src/music_service.ts @@ -1,4 +1,3 @@ - export type Credentials = { username: string; password: string }; export function isSuccess( @@ -38,8 +37,19 @@ export type Album = { name: string; }; +export type Paging = { + _index?: number; + _count?: number; +}; + export interface MusicLibrary { - artists(): Artist[]; + artists({ _index, _count }: Paging): Promise<[Artist[], number]>; 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]>; } diff --git a/src/navidrome.ts b/src/navidrome.ts index 2ddc918..48ca91d 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -1,5 +1,5 @@ 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 axios from "axios"; @@ -25,6 +25,15 @@ export type SubsonicResponse = { _status: string; }; +export type GetArtistsResponse = SubsonicResponse & { + artists: { + index: { + artist: { _id: string; _name: string; _albumCount: string }[]; + _name: string; + }[]; + }; +}; + export type SubsonicError = SubsonicResponse & { error: { _code: string; @@ -47,11 +56,11 @@ export class Navidrome implements MusicService { this.encryption = encryption; } - get = async ( + get = async ( { username, password }: Credentials, path: string, q: {} = {} - ): Promise => + ): Promise => axios .get(`${this.url}${path}`, { params: { @@ -66,11 +75,11 @@ export class Navidrome implements MusicService { .then((json) => json["subsonic-response"]) .then((json) => { if (isError(json)) throw json.error._message; - else return json; + else return (json as unknown) as T; }); generateToken = async (credentials: Credentials) => - this.get(credentials, "/rest/ping.view").then((_) => ({ + this.get(credentials, "/rest/ping.view").then(() => ({ authToken: Buffer.from( JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials))) ).toString("base64"), @@ -85,17 +94,33 @@ export class Navidrome implements MusicService { ) ); - async login(_: string) { - // const credentials: Credentials = this.parseToken(token); + async login(token: string) { + const navidrome = this; + const credentials: Credentials = this.parseToken(token); return Promise.resolve({ - artists: () => [], + artists: ({ _index, _count }: Paging): Promise<[Artist[], number]> => + navidrome + .get(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) => ({ id, name: id, }), - albums: ({ artistId }: { artistId?: string }) => { + albums: ({ + artistId, + }: { + artistId?: string; + } & Paging): Promise<[Album[], number]> => { console.log(artistId); - return []; + return Promise.resolve([[], 0]); }, }); } diff --git a/src/smapi.ts b/src/smapi.ts index aed9027..fc8a001 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -6,7 +6,7 @@ import path from "path"; import logger from "./logger"; import { LinkCodes } from "./link_codes"; -import { MusicLibrary, MusicService } from "./music_service"; +import { MusicLibrary, MusicService } from "./music_service"; export const LOGIN_ROUTE = "/login"; export const SOAP_PATH = "/ws/sonos"; @@ -64,14 +64,18 @@ export type GetMetadataResponse = { export function getMetadataResult({ mediaCollection, + index, + total, }: { mediaCollection: any[] | undefined; + index: number; + total: number; }): GetMetadataResponse { return { getMetadataResult: { count: mediaCollection?.length || 0, - index: 0, - total: mediaCollection?.length || 0, + index, + total, mediaCollection: mediaCollection || [], }, }; @@ -181,8 +185,8 @@ function bindSmapiSoapServiceToExpress( id, index, count, - // recursive, - }: { id: string; index: number; count: number; recursive: boolean }, + }: // recursive, + { id: string; index: number; count: number; recursive: boolean }, _, headers?: SoapyHeaders ) => { @@ -194,21 +198,22 @@ function bindSmapiSoapServiceToExpress( }, }; } - const login = await musicService.login( - headers.credentials.loginToken.token - ).catch(_ => { - throw { - Fault: { - faultcode: "Client.LoginUnauthorized", - faultstring: "Credentials not found...", - }, - }; - }); + const login = await musicService + .login(headers.credentials.loginToken.token) + .catch((_) => { + throw { + Fault: { + faultcode: "Client.LoginUnauthorized", + faultstring: "Credentials not found...", + }, + }; + }); const musicLibrary = login as MusicLibrary; - // const [type, typeId] = id.split(":"); - const type = id; + const [type, typeId] = id.split(":"); + const paging = { _index: index, _count: count }; + logger.debug(`Fetching type=${type}, typeId=${typeId}`); switch (type) { case "root": return getMetadataResult({ @@ -216,27 +221,39 @@ function bindSmapiSoapServiceToExpress( container({ id: "artists", title: "Artists" }), container({ id: "albums", title: "Albums" }), ], + index: 0, + total: 2, }); case "artists": - return getMetadataResult({ - mediaCollection: musicLibrary.artists().map((it) => - container({ - id: `artist:${it.id}`, - title: it.name, + return await musicLibrary + .artists(paging) + .then(([artists, total]) => + getMetadataResult({ + mediaCollection: artists.map((it) => + container({ + id: `artist:${it.id}`, + title: it.name, + }) + ), + index: paging._index, + total, }) - ), - }); + ); case "albums": - return getMetadataResult({ - mediaCollection: musicLibrary - .albums({ _index: index, _count: count }) - .map((it) => - container({ - id: `album:${it.id}`, - title: it.name, - }) - ), - }); + return await musicLibrary + .albums(paging) + .then(([albums, total]) => + getMetadataResult({ + mediaCollection: albums.map((it) => + container({ + id: `album:${it.id}`, + title: it.name, + }) + ), + index: paging._index, + total, + }) + ); default: throw `Unsupported id:${id}`; } diff --git a/tests/builders.ts b/tests/builders.ts index 5dba7b3..1716768 100644 --- a/tests/builders.ts +++ b/tests/builders.ts @@ -61,11 +61,11 @@ export function someCredentials(token: string): Credentials { return { loginToken: { token, - householdId: "hh1" + householdId: "hh1", }, deviceId: "d1", - deviceProvider: "dp1" - } + deviceProvider: "dp1", + }; } export type ArtistWithAlbums = Artist & { @@ -96,3 +96,25 @@ export const MADONNA: ArtistWithAlbums = { name: "Madonna", 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, +]; \ No newline at end of file diff --git a/tests/encryption.test.ts b/tests/encryption.test.ts index 46b3be8..4dfb142 100644 --- a/tests/encryption.test.ts +++ b/tests/encryption.test.ts @@ -1,7 +1,7 @@ import encryption from '../src/encryption'; describe("encrypt", () => { - const e = encryption(); + const e = encryption("secret squirrel"); it("can encrypt and decrypt", () => { const value = "bobs your uncle" diff --git a/tests/in_memory_music_service.test.ts b/tests/in_memory_music_service.test.ts index a42f013..bb4911f 100644 --- a/tests/in_memory_music_service.test.ts +++ b/tests/in_memory_music_service.test.ts @@ -1,7 +1,13 @@ import { InMemoryMusicService } from "./in_memory_music_service"; import { AuthSuccess, MusicLibrary } from "../src/music_service"; 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", () => { const service = new InMemoryMusicService(); @@ -31,7 +37,9 @@ describe("InMemoryMusicService", () => { 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 () => { service.clear(); - service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE); + service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE, METALLICA); service.hasUser(user); const token = (await service.generateToken(user)) as AuthSuccess; @@ -50,12 +58,42 @@ describe("InMemoryMusicService", () => { }); describe("artists", () => { - it("should provide an array of artists", () => { - expect(musicLibrary.artists()).toEqual([ - { id: BOB_MARLEY.id, name: BOB_MARLEY.name }, - { id: MADONNA.id, name: MADONNA.name }, - { id: BLONDIE.id, name: BLONDIE.name }, - ]); + describe("fetching all", () => { + it("should provide an array of artists", async () => { + const artists = [ + { id: BOB_MARLEY.id, name: BOB_MARLEY.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("fetching with no filtering", () => { - it("should return all the albums for all the artists", () => { - expect(musicLibrary.albums({})).toEqual([ - ...BOB_MARLEY.albums, - ...BLONDIE.albums, - ...MADONNA.albums, + it("should return all the albums for all the artists", async () => { + expect(await musicLibrary.albums({})).toEqual([ + ALL_ALBUMS, + ALL_ALBUMS.length, ]); }); }); describe("fetching for a single artist", () => { - it("should return them all if the artist has some", () => { - expect(musicLibrary.albums({ artistId: BLONDIE.id })).toEqual( - BLONDIE.albums - ); + it("should return them all if the artist has some", async () => { + expect(await musicLibrary.albums({ artistId: BLONDIE.id })).toEqual([ + BLONDIE.albums, + BLONDIE.albums.length, + ]); }); - it("should return empty list of the artists does not have any", () => { - expect(musicLibrary.albums({ artistId: MADONNA.id })).toEqual([]); + it("should return empty list of the artists does not have any", async () => { + expect(await musicLibrary.albums({ artistId: MADONNA.id })).toEqual([ + [], + 0, + ]); }); - it("should return empty list if the artist id is not valid", () => { - expect(musicLibrary.albums({ artistId: uuid() })).toEqual([]); + it("should return empty list if the artist id is not valid", async () => { + expect(await musicLibrary.albums({ artistId: uuid() })).toEqual([ + [], + 0, + ]); }); }); describe("fetching with just index", () => { - it("should return everything after", () => { - expect(musicLibrary.albums({ _index: 2 })).toEqual([ + it("should return everything after", async () => { + const albums = [ BOB_MARLEY.albums[2], - BLONDIE.albums[0], - BLONDIE.albums[1], + ...BLONDIE.albums, + ...MADONNA.albums, + ...METALLICA.albums, + ]; + expect(await musicLibrary.albums({ _index: 2 })).toEqual([ + albums, + ALL_ALBUMS.length, ]); }); }); describe("fetching with just count", () => { - it("should return first n items", () => { - expect(musicLibrary.albums({ _count: 3 })).toEqual([ + it("should return first n items", async () => { + const albums = [ BOB_MARLEY.albums[0], BOB_MARLEY.albums[1], BOB_MARLEY.albums[2], + ]; + expect(await musicLibrary.albums({ _count: 3 })).toEqual([ + albums, + ALL_ALBUMS.length, ]); }); }); describe("fetching with index and count", () => { - it("should be able to return the first page", () => { - expect(musicLibrary.albums({ _index: 0, _count: 2 })).toEqual([ - BOB_MARLEY.albums[0], - BOB_MARLEY.albums[1], + it("should be able to return the first page", async () => { + const albums = [BOB_MARLEY.albums[0], BOB_MARLEY.albums[1]]; + expect(await musicLibrary.albums({ _index: 0, _count: 2 })).toEqual([ + albums, + ALL_ALBUMS.length, ]); }); - it("should be able to return the second page", () => { - expect(musicLibrary.albums({ _index: 2, _count: 2 })).toEqual([ - BOB_MARLEY.albums[2], - BLONDIE.albums[0], + it("should be able to return the second page", async () => { + const albums = [BOB_MARLEY.albums[2], BLONDIE.albums[0]]; + expect(await musicLibrary.albums({ _index: 2, _count: 2 })).toEqual([ + albums, + ALL_ALBUMS.length, ]); }); - it("should be able to return the last page", () => { - expect(musicLibrary.albums({ _index: 4, _count: 2 })).toEqual([ - BLONDIE.albums[1], + it("should be able to return the last page", async () => { + expect(await musicLibrary.albums({ _index: 5, _count: 2 })).toEqual([ + METALLICA.albums, + ALL_ALBUMS.length, ]); }); }); diff --git a/tests/in_memory_music_service.ts b/tests/in_memory_music_service.ts index 7cf961f..dc39046 100644 --- a/tests/in_memory_music_service.ts +++ b/tests/in_memory_music_service.ts @@ -9,9 +9,9 @@ import { AuthFailure, Artist, MusicLibrary, + Paging, } from "../src/music_service"; - const artistWithAlbumsToArtist = (it: ArtistWithAlbums): Artist => ({ id: it.id, name: it.name, @@ -52,9 +52,15 @@ export class InMemoryMusicService implements MusicService { login(token: string): Promise { 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({ - 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) => pipe( this.artists.find((it) => it.id === id), @@ -73,7 +79,7 @@ export class InMemoryMusicService implements MusicService { }) => { const i0 = _index || 0; const i1 = _count ? i0 + _count : undefined; - return this.artists + const albums = this.artists .filter( pipe( O.fromNullable(artistId), @@ -81,8 +87,8 @@ export class InMemoryMusicService implements MusicService { O.getOrElse(() => all) ) ) - .flatMap((it) => it.albums) - .slice(i0, i1); + .flatMap((it) => it.albums); + return Promise.resolve([albums.slice(i0, i1), albums.length]); }, }); } diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index fb38d79..8bb3a4c 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -23,7 +23,7 @@ describe("navidrome", () => { const password = "pass1"; const salt = "saltysalty"; - const navidrome = new Navidrome(url, encryption()); + const navidrome = new Navidrome(url, encryption("secret")); const mockedRandomString = (randomString as unknown) as jest.Mock; @@ -35,6 +35,14 @@ describe("navidrome", () => { mockedRandomString.mockReturnValue(salt); }); + const authParams = { + u: username, + t: t(password, salt), + s: salt, + v: "1.16.1", + c: "bonob", + }; + describe("generateToken", () => { describe("when the credentials are valid", () => { 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.userId).toEqual(username); - expect(axios.get).toHaveBeenCalledWith( - `${url}/rest/ping.view`, - { - params: { - u: username, - t: t(password, salt), - s: salt, - v: "1.16.1", - c: "bonob", - }, - } - ); + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: authParams, + }); }); }); @@ -80,4 +79,66 @@ describe("navidrome", () => { }); }); }); + + describe("getArtists", () => { + beforeEach(() => { + (axios.get as jest.Mock).mockResolvedValue({ + status: 200, + data: ` + + + + + + + + + + + + + `, + }); + }); + + 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, + }); + }); + }); + }); }); diff --git a/tests/smapi.test.ts b/tests/smapi.test.ts index e7b738d..739aeac 100644 --- a/tests/smapi.test.ts +++ b/tests/smapi.test.ts @@ -42,7 +42,9 @@ describe("service config", () => { arrayAccessFormPaths: ["stringtables", "stringtables.stringtable"], }).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("when there are a zero mediaCollections", () => { 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.index).toEqual(0); - expect(result.getMetadataResult.total).toEqual(0); + expect(result.getMetadataResult.index).toEqual(33); + expect(result.getMetadataResult.total).toEqual(99); expect(result.getMetadataResult.mediaCollection).toEqual([]); }); }); @@ -62,11 +68,15 @@ describe("getMetadataResult", () => { describe("when there are a number of mediaCollections", () => { it("should add correct counts", () => { const mediaCollection = [{}, {}]; - const result = getMetadataResult({ mediaCollection }); + const result = getMetadataResult({ + mediaCollection, + index: 22, + total: 3, + }); expect(result.getMetadataResult.count).toEqual(2); - expect(result.getMetadataResult.index).toEqual(0); - expect(result.getMetadataResult.total).toEqual(2); + expect(result.getMetadataResult.index).toEqual(22); + expect(result.getMetadataResult.total).toEqual(3); expect(result.getMetadataResult.mediaCollection).toEqual(mediaCollection); }); }); @@ -349,6 +359,8 @@ describe("api", () => { container({ id: "artists", title: "Artists" }), container({ id: "albums", title: "Albums" }), ], + index: 0, + total: 2, }) ); }); @@ -365,9 +377,9 @@ describe("api", () => { }); expect(artists[0]).toEqual( getMetadataResult({ - mediaCollection: [BLONDIE, BOB_MARLEY].map((it) => - container({ id: `artist:${it.id}`, title: it.name }) - ), + mediaCollection: [BLONDIE, BOB_MARLEY].map((it) => container({ id: `artist:${it.id}`, title: it.name })), + index: 0, + total: 2 }) ); }); @@ -390,6 +402,8 @@ describe("api", () => { ].map((it) => 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, }), ], + index: 2, + total: BLONDIE.albums.length + BOB_MARLEY.albums.length }) ); });