mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to query artists from navidrome with paging
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]>;
|
||||
}
|
||||
|
||||
@@ -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 <T>(
|
||||
{ username, password }: Credentials,
|
||||
path: string,
|
||||
q: {} = {}
|
||||
): Promise<SubsonicResponse> =>
|
||||
): Promise<T> =>
|
||||
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<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) => ({
|
||||
id,
|
||||
name: id,
|
||||
}),
|
||||
albums: ({ artistId }: { artistId?: string }) => {
|
||||
albums: ({
|
||||
artistId,
|
||||
}: {
|
||||
artistId?: string;
|
||||
} & Paging): Promise<[Album[], number]> => {
|
||||
console.log(artistId);
|
||||
return [];
|
||||
return Promise.resolve([[], 0]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
51
src/smapi.ts
51
src/smapi.ts
@@ -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,9 +198,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
},
|
||||
};
|
||||
}
|
||||
const login = await musicService.login(
|
||||
headers.credentials.loginToken.token
|
||||
).catch(_ => {
|
||||
const login = await musicService
|
||||
.login(headers.credentials.loginToken.token)
|
||||
.catch((_) => {
|
||||
throw {
|
||||
Fault: {
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
@@ -207,8 +211,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
|
||||
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) =>
|
||||
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) =>
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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"
|
||||
|
||||
@@ -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,15 +58,45 @@ describe("InMemoryMusicService", () => {
|
||||
});
|
||||
|
||||
describe("artists", () => {
|
||||
it("should provide an array of artists", () => {
|
||||
expect(musicLibrary.artists()).toEqual([
|
||||
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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("artist", () => {
|
||||
describe("when it exists", () => {
|
||||
it("should provide an artist", () => {
|
||||
@@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<MusicLibrary> {
|
||||
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]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: `<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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user