Query albums by genre

This commit is contained in:
simojenki
2021-03-06 20:06:08 +11:00
parent 5f9c240cdf
commit 1e5d020a75
7 changed files with 639 additions and 208 deletions

View File

@@ -74,7 +74,26 @@ export type ArtistQuery = Paging
export type AlbumQuery = Paging & {
artistId?: string
genre?: string
}
export const artistToArtistSummary = (
it: Artist
): ArtistSummary => ({
id: it.id,
name: it.name,
image: it.image,
});
export const albumToAlbumSummary = (
it: Album
): AlbumSummary => ({
id: it.id,
name: it.name,
year: it.year,
genre: it.genre,
});
export interface MusicService {
generateToken(credentials: Credentials): Promise<AuthSuccess | AuthFailure>;
login(authToken: string): Promise<MusicLibrary>;
@@ -84,5 +103,5 @@ export interface MusicLibrary {
artists(q: ArtistQuery): Promise<Result<ArtistSummary>>;
artist(id: string): Promise<Artist>;
albums(q: AlbumQuery): Promise<Result<AlbumSummary>>;
// album(id: string): Promise<Album>;
album(id: string): Promise<Album>;
}

View File

@@ -1,3 +1,5 @@
import { option as O } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import { Md5 } from "ts-md5/dist/md5";
import {
Credentials,
@@ -19,6 +21,7 @@ import axios from "axios";
import { Encryption } from "./encryption";
import randomString from "./random_string";
export const t = (password: string, s: string) =>
Md5.hashStr(`${password}${s}`);
@@ -66,6 +69,12 @@ export type GetArtistsResponse = SubsonicResponse & {
};
};
export type GetAlbumListResponse = SubsonicResponse & {
albumList: {
album: album[];
};
};
export type SubsonicError = SubsonicResponse & {
error: {
_code: string;
@@ -105,6 +114,17 @@ export type IdName = {
name: string;
};
export type getAlbumListParams = {
type: string,
size?: number;
offet?: number;
fromYear?: string,
toYear?: string,
genre?: string
}
const MAX_ALBUM_LIST = 500;
export class Navidrome implements MusicService {
url: string;
encryption: Encryption;
@@ -235,8 +255,36 @@ export class Navidrome implements MusicService {
image: artistInfo.image,
albums: artist.albums,
})),
albums: (_: AlbumQuery): Promise<Result<AlbumSummary>> => {
return Promise.resolve({ results: [], total: 0 });
albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> => {
const p = pipe(
O.fromNullable(q.genre),
O.map<string, getAlbumListParams>(genre => ({ type: "byGenre", genre })),
O.getOrElse<getAlbumListParams>(() => ({ type: "alphabeticalByArtist" })),
)
return navidrome
.get<GetAlbumListResponse>(credentials, "/rest/getAlbumList", {
...p,
size: MAX_ALBUM_LIST,
offset: 0,
})
.then((response) => response.albumList.album)
.then((albumList) =>
albumList.map((album) => ({
id: album._id,
name: album._name,
year: album._year,
genre: album._genre,
}))
)
.then(slice2(q))
.then(([page, total]) => ({
results: page,
total: Math.min(MAX_ALBUM_LIST, total),
}));
},
album: (_: string): Promise<Album> => {
return Promise.reject("not implemented");
},
};

View File

@@ -6,7 +6,12 @@ import path from "path";
import logger from "./logger";
import { LinkCodes } from "./link_codes";
import { AlbumSummary, MusicLibrary, MusicService, slice2 } from "./music_service";
import {
AlbumSummary,
MusicLibrary,
MusicService,
slice2,
} from "./music_service";
export const LOGIN_ROUTE = "/login";
export const SOAP_PATH = "/ws/sonos";

View File

@@ -5,7 +5,7 @@ import { Credentials } from "../src/smapi";
import { Service, Device } from "../src/sonos";
import { Album, Artist } from "../src/music_service";
const randomInt = (max: number) => Math.floor(Math.random() * max);
const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max));
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`;
export const aService = (fields: Partial<Service> = {}): Service => ({
@@ -73,25 +73,26 @@ export function anArtist(fields: Partial<Artist> = {}): Artist {
return {
id,
name: `Artist ${id}`,
albums: [],
albums: [anAlbum(), anAlbum(), anAlbum()],
image: {
small: undefined,
medium: undefined,
large: undefined
small: `/artist/art/${id}/small`,
medium: `/artist/art/${id}/small`,
large: `/artist/art/${id}/large`,
},
...fields
}
...fields,
};
}
export function anAlbum(fields: Partial<Album> = {}): Album {
const genres = ["Metal", "Pop", "Rock", "Hip-Hop"];
const id = uuid();
return {
id,
name: `Album ${id}`,
genre: "Metal",
year: "1900",
...fields
}
genre: genres[randomInt(genres.length)],
year: `19${randomInt(99)}`,
...fields,
};
}
export const BLONDIE: Artist = {
@@ -168,6 +169,6 @@ export const METALLICA: Artist = {
},
};
export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA]
export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA];
export const ALL_ALBUMS = ALL_ARTISTS.flatMap(it => it.albums || []);
export const ALL_ALBUMS = ALL_ARTISTS.flatMap((it) => it.albums || []);

View File

@@ -1,16 +1,12 @@
import { InMemoryMusicService } from "./in_memory_music_service";
import {
InMemoryMusicService,
AuthSuccess,
MusicLibrary,
artistToArtistSummary,
} from "./in_memory_music_service";
import { AuthSuccess, MusicLibrary } from "../src/music_service";
albumToAlbumSummary,
} from "../src/music_service";
import { v4 as uuid } from "uuid";
import {
BOB_MARLEY,
MADONNA,
BLONDIE,
METALLICA,
ALL_ALBUMS,
} from "./builders";
import { anArtist, anAlbum } from "./builders";
describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService();
@@ -48,10 +44,19 @@ describe("InMemoryMusicService", () => {
describe("artistToArtistSummary", () => {
it("should map fields correctly", () => {
expect(artistToArtistSummary(BOB_MARLEY)).toEqual({
id: BOB_MARLEY.id,
name: BOB_MARLEY.name,
image: BOB_MARLEY.image,
const artist = anArtist({
id: uuid(),
name: "The Artist",
image: {
small: "/path/to/small/jpg",
medium: "/path/to/medium/jpg",
large: "/path/to/large/jpg",
},
});
expect(artistToArtistSummary(artist)).toEqual({
id: artist.id,
name: artist.name,
image: artist.image,
});
});
});
@@ -63,7 +68,6 @@ describe("InMemoryMusicService", () => {
beforeEach(async () => {
service.clear();
service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE, METALLICA);
service.hasUser(user);
const token = (await service.generateToken(user)) as AuthSuccess;
@@ -71,59 +75,67 @@ describe("InMemoryMusicService", () => {
});
describe("artists", () => {
describe("fetching all", () => {
const artist1 = anArtist();
const artist2 = anArtist();
const artist3 = anArtist();
const artist4 = anArtist();
const artist5 = anArtist();
beforeEach(() => {
service.hasArtists(artist1, artist2, artist3, artist4, artist5);
});
describe("fetching all in one page", () => {
it("should provide an array of artists", async () => {
const artists = [
artistToArtistSummary(BOB_MARLEY),
artistToArtistSummary(MADONNA),
artistToArtistSummary(BLONDIE),
artistToArtistSummary(METALLICA),
];
expect(
await musicLibrary.artists({ _index: 0, _count: 100 })
).toEqual({
results: artists,
total: 4,
results: [
artistToArtistSummary(artist1),
artistToArtistSummary(artist2),
artistToArtistSummary(artist3),
artistToArtistSummary(artist4),
artistToArtistSummary(artist5),
],
total: 5,
});
});
});
describe("fetching the second page", () => {
it("should provide an array of artists", async () => {
const artists = [
artistToArtistSummary(BLONDIE),
artistToArtistSummary(METALLICA),
];
expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual({
results: artists,
total: 4,
results: [
artistToArtistSummary(artist3),
artistToArtistSummary(artist4),
],
total: 5,
});
});
});
describe("fetching the more items than fit on the second page", () => {
describe("fetching the last page", () => {
it("should provide an array of artists", async () => {
const artists = [
artistToArtistSummary(MADONNA),
artistToArtistSummary(BLONDIE),
artistToArtistSummary(METALLICA),
];
expect(
await musicLibrary.artists({ _index: 1, _count: 50 })
).toEqual({ results: artists, total: 4 });
expect(await musicLibrary.artists({ _index: 4, _count: 2 })).toEqual({
results: [artistToArtistSummary(artist5)],
total: 5,
});
});
});
});
describe("artist", () => {
const artist1 = anArtist({ id: uuid(), name: "Artist 1" });
const artist2 = anArtist({ id: uuid(), name: "Artist 2" });
beforeEach(() => {
service.hasArtists(artist1, artist2);
});
describe("when it exists", () => {
it("should provide an artist", async () => {
expect(await musicLibrary.artist(MADONNA.id)).toEqual(
MADONNA
);
expect(await musicLibrary.artist(BLONDIE.id)).toEqual(
BLONDIE
);
expect(await musicLibrary.artist(artist1.id)).toEqual(artist1);
expect(await musicLibrary.artist(artist2.id)).toEqual(artist2);
});
});
@@ -136,36 +148,177 @@ describe("InMemoryMusicService", () => {
});
});
describe("album", () => {
describe("when it exists", () => {
const albumToLookFor = anAlbum({ id: "albumToLookFor" });
const artist1 = anArtist({ albums: [anAlbum(), anAlbum(), anAlbum()] });
const artist2 = anArtist({
albums: [anAlbum(), albumToLookFor, anAlbum()],
});
beforeEach(() => {
service.hasArtists(artist1, artist2);
});
it("should provide an artist", async () => {
expect(await musicLibrary.album(albumToLookFor.id)).toEqual(
albumToLookFor
);
});
});
describe("when it doesnt exist", () => {
it("should blow up", async () => {
return expect(musicLibrary.album("-1")).rejects.toEqual(
"No album with id '-1'"
);
});
});
});
describe("albums", () => {
describe("fetching with no filtering", () => {
const artist1_album1 = anAlbum({ genre: "Pop" });
const artist1_album2 = anAlbum({ genre: "Rock" });
const artist1_album3 = anAlbum({ genre: "Metal" });
const artist1_album4 = anAlbum({ genre: "Pop" });
const artist1_album5 = anAlbum({ genre: "Pop" });
const artist2_album1 = anAlbum({ genre: "Metal" });
const artist3_album1 = anAlbum({ genre: "Hip-Hop" });
const artist3_album2 = anAlbum({ genre: "Pop" });
const totalAlbumCount = 8;
const artist1 = anArtist({
albums: [
artist1_album1,
artist1_album2,
artist1_album3,
artist1_album4,
artist1_album5,
],
});
const artist2 = anArtist({ albums: [artist2_album1] });
const artist3 = anArtist({ albums: [artist3_album1, artist3_album2] });
const artistWithNoAlbums = anArtist({ albums: [] });
beforeEach(() => {
service.hasArtists(artist1, artist2, artist3, artistWithNoAlbums);
});
describe("with no filtering", () => {
describe("fetching all on one page", () => {
it("should return all the albums for all the artists", async () => {
expect(await musicLibrary.albums({ _index: 0, _count: 100 })).toEqual(
expect(
await musicLibrary.albums({ _index: 0, _count: 100 })
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: totalAlbumCount,
});
});
});
describe("fetching a page", () => {
it("should return only that page", async () => {
expect(await musicLibrary.albums({ _index: 4, _count: 3 })).toEqual(
{
results: ALL_ALBUMS,
total: ALL_ALBUMS.length,
results: [
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
],
total: totalAlbumCount,
}
);
});
});
describe("fetching for a single artist", () => {
it("should return them all if the artist has some", async () => {
describe("fetching the last page", () => {
it("should return only that page", async () => {
expect(
await musicLibrary.albums({ _index: 6, _count: 100 })
).toEqual({
results: [
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: totalAlbumCount,
});
});
});
});
describe("filtering by artist", () => {
describe("fetching all", () => {
it("should return all artist albums", async () => {
expect(
await musicLibrary.albums({
artistId: BLONDIE.id,
artistId: artist3.id,
_index: 0,
_count: 100,
})
).toEqual({
results: BLONDIE.albums,
total: BLONDIE.albums.length,
results: [
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: artist3.albums.length,
});
});
});
it("should return empty list of the artists does not have any", async () => {
describe("when the artist has more albums than a single page", () => {
describe("can fetch a single page", () => {
it("should return only the albums for that page", async () => {
expect(
await musicLibrary.albums({
artistId: MADONNA.id,
artistId: artist1.id,
_index: 1,
_count: 3,
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
],
total: artist1.albums.length,
});
});
});
describe("can fetch the last page", () => {
it("should return only the albums for the last page", async () => {
expect(
await musicLibrary.albums({
artistId: artist1.id,
_index: 4,
_count: 100,
})
).toEqual({
results: [albumToAlbumSummary(artist1_album5)],
total: artist1.albums.length,
});
});
});
});
it("should return empty list if the artists does not have any", async () => {
expect(
await musicLibrary.albums({
artistId: artistWithNoAlbums.id,
_index: 0,
_count: 100,
})
@@ -189,25 +342,71 @@ describe("InMemoryMusicService", () => {
});
});
describe("fetching with index and count", () => {
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({
results: albums,
total: ALL_ALBUMS.length,
describe("filtering by genre", () => {
describe("fetching all on one page", () => {
it.only("should return all the albums of that genre for all the artists", async () => {
expect(
await musicLibrary.albums({ _index: 0, _count: 100, genre: "Pop" })
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist3_album2),
],
total: 4,
});
});
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({
results: albums,
total: ALL_ALBUMS.length,
});
describe("when the genre has more albums than a single page", () => {
describe("can fetch a single page", () => {
it("should return only the albums for that page", async () => {
expect(
await musicLibrary.albums({
genre: "Pop",
_index: 1,
_count: 3,
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist3_album2),
],
total: 4,
});
});
it("should be able to return the last page", async () => {
expect(await musicLibrary.albums({ _index: 5, _count: 2 })).toEqual({
results: METALLICA.albums,
total: ALL_ALBUMS.length,
});
describe("can fetch the last page", () => {
it("should return only the albums for the last page", async () => {
expect(
await musicLibrary.albums({
artistId: artist1.id,
_index: 4,
_count: 100,
})
).toEqual({
results: [albumToAlbumSummary(artist1_album5)],
total: artist1.albums.length,
});
});
});
});
it("should return empty list if there are no albums for the genre", async () => {
expect(
await musicLibrary.albums({
genre: "genre with no albums",
_index: 0,
_count: 100,
})
).toEqual({
results: [],
total: 0,
});
});
});

View File

@@ -1,7 +1,6 @@
import { option as O } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import {
MusicService,
Credentials,
@@ -13,33 +12,20 @@ import {
AlbumQuery,
slice2,
asResult,
ArtistSummary,
artistToArtistSummary,
albumToAlbumSummary,
Album,
AlbumSummary
} from "../src/music_service";
export const artistToArtistSummary = (
it: Artist
): ArtistSummary => ({
id: it.id,
name: it.name,
image: it.image,
});
export const albumToAlbumSummary = (
it: Album
): AlbumSummary => ({
id: it.id,
name: it.name,
year: it.year,
genre: it.genre,
});
type P<T> = (t: T) => boolean;
const all: P<any> = (_: any) => true;
const artistWithId = (id: string): P<Artist> => (artist: Artist) =>
const albumByArtist = (id: string): P<[Artist, Album]> => ([artist, _]) =>
artist.id === id;
const albumWithGenre = (genre: string): P<[Artist, Album]> => ([_, album]) =>
album.genre === genre;
export class InMemoryMusicService implements MusicService {
users: Record<string, string> = {};
artists: Artist[] = [];
@@ -76,24 +62,40 @@ export class InMemoryMusicService implements MusicService {
pipe(
this.artists.find((it) => it.id === id),
O.fromNullable,
O.map(it => Promise.resolve(it)),
O.map((it) => Promise.resolve(it)),
O.getOrElse(() => Promise.reject(`No artist with id '${id}'`))
),
albums: (q: AlbumQuery) =>
Promise.resolve(
this.artists.filter(
this.artists
.flatMap((artist) => artist.albums.map((album) => [artist, album]))
.filter(
pipe(
pipe(
O.fromNullable(q.artistId),
O.map(artistWithId),
O.map(albumByArtist)
),
O.alt(() =>
pipe(
O.fromNullable(q.genre),
O.map(albumWithGenre)
)
),
O.getOrElse(() => all)
)
)
)
.then((artists) => artists.flatMap((it) => it.albums))
.then(it => it.map(albumToAlbumSummary))
.then((matches) => matches.map(([_, album]) => album as Album))
.then((it) => it.map(albumToAlbumSummary))
.then(slice2(q))
.then(asResult),
// album: (id: albumId) => Promise.resolve(this.artists.flatMap(it => it.albums).find(it => it.id === id))
album: (id: string) =>
pipe(
this.artists.flatMap((it) => it.albums).find((it) => it.id === id),
O.fromNullable,
O.map((it) => Promise.resolve(it)),
O.getOrElse(() => Promise.reject(`No album with id '${id}'`))
),
});
}

View File

@@ -7,7 +7,15 @@ import axios from "axios";
jest.mock("axios");
import randomString from "../src/random_string";
import { Album, Artist, AuthSuccess, Images } from "../src/music_service";
import {
Album,
Artist,
AuthSuccess,
Images,
albumToAlbumSummary,
} from "../src/music_service";
import { anAlbum, anArtist } from "./builders";
jest.mock("../src/random_string");
describe("t", () => {
@@ -71,6 +79,16 @@ const albumXml = (artist: Artist, album: Album) => `<album id="${album.id}"
songCount="19"
isVideo="false"></album>`;
const albumListXml = (
albums: [Artist, Album][]
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<albumList>
${albums.map(([artist, album]) =>
albumXml(artist, album)
)}
</albumList>
</subsonic-response>`;
const artistXml = (
artist: Artist
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
@@ -145,31 +163,14 @@ describe("Navidrome", () => {
});
});
describe("getArtist", () => {
const album1: Album = {
id: "album1",
name: "super album",
year: "2001",
genre: "Pop",
};
describe("getting an artist", () => {
const album1: Album = anAlbum();
const album2: Album = {
id: "album2",
name: "bad album",
year: "2002",
genre: "Rock",
};
const album2: Album = anAlbum();
const artist: Artist = {
id: "someUUID_123",
name: "BananaMan",
image: {
small: "sml1",
medium: "med1",
large: "lge1",
},
const artist: Artist = anArtist({
albums: [album1, album2],
};
});
beforeEach(() => {
mockGET
@@ -180,7 +181,8 @@ describe("Navidrome", () => {
);
});
it.only("should do it", async () => {
describe("when the artist exists", () => {
it("should return it", async () => {
const result: Artist = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
@@ -190,8 +192,8 @@ describe("Navidrome", () => {
expect(result).toEqual({
id: artist.id,
name: artist.name,
image: { small: "sml1", medium: "med1", large: "lge1" },
albums: [album1, album2]
image: artist.image,
albums: artist.albums,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
@@ -209,8 +211,9 @@ describe("Navidrome", () => {
});
});
});
});
describe("getArtists", () => {
describe("getting artists", () => {
describe("when there are no results", () => {
beforeEach(() => {
mockGET
@@ -246,30 +249,10 @@ describe("Navidrome", () => {
});
describe("when there are artists", () => {
const artist1: Artist = {
id: "artist1.id",
name: "artist1.name",
image: { small: "s1", medium: "m1", large: "l1" },
albums: [],
};
const artist2: Artist = {
id: "artist2.id",
name: "artist2.name",
image: { small: "s2", medium: "m2", large: "l2" },
albums: [],
};
const artist3: Artist = {
id: "artist3.id",
name: "artist3.name",
image: { small: "s3", medium: "m3", large: "l3" },
albums: [],
};
const artist4: Artist = {
id: "artist4.id",
name: "artist4.name",
image: { small: "s4", medium: "m4", large: "l4" },
albums: [],
};
const artist1 = anArtist();
const artist2 = anArtist();
const artist3 = anArtist();
const artist4 = anArtist();
const getArtistsXml = `<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">
@@ -402,4 +385,178 @@ describe("Navidrome", () => {
});
});
});
const range = (size: number) => [...Array(size).keys()];
const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] =>
artists.flatMap((artist) =>
artist.albums.map((album) => [artist, album] as [Artist, Album])
);
describe("getting albums", () => {
describe("filtering", () => {
const album1 = anAlbum({ genre: "Pop" });
const album2 = anAlbum({ genre: "Rock" });
const album3 = anAlbum({ genre: "Pop" });
const artist = anArtist({ albums: [album1, album2, album3]});
describe("by genre", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml([[artist, album1], [artist, album3]])))
);
});
it("should pass the filter to navidrome", async () => {
const q = { _index: 0, _count: 500, genre: "Pop" };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.albums(q));
expect(result).toEqual({
results: [album1, album3].map(albumToAlbumSummary),
total: 2,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: {
type: "byGenre",
genre: "Pop",
size: 500,
offset: 0,
...authParams,
},
});
});
});
});
describe("when there are less than 500 albums", () => {
const artist1 = anArtist({
name: "abba",
albums: [anAlbum(), anAlbum(), anAlbum()],
});
const artist2 = anArtist({
name: "babba",
albums: [anAlbum(), anAlbum(), anAlbum()],
});
const artists = [artist1, artist2];
const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
);
});
describe("querying for all of them", () => {
it("should return all of them with corrent paging information", async () => {
const paging = { _index: 0, _count: 500 };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.albums(paging));
expect(result).toEqual({
results: albums.map(albumToAlbumSummary),
total: 6,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: {
type: "alphabeticalByArtist",
size: 500,
offset: 0,
...authParams,
},
});
});
});
describe("querying for a page of them", () => {
it("should return the page with the corrent paging information", async () => {
const paging = { _index: 2, _count: 2 };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.albums(paging));
expect(result).toEqual({
results: [albums[2], albums[3]],
total: 6,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: {
type: "alphabeticalByArtist",
size: 500,
offset: 0,
...authParams,
},
});
});
});
});
describe("when there are more than 500 albums", () => {
const first500Albums = range(500).map((i) =>
anAlbum({ name: `album ${i}` })
);
const artist = anArtist({
name: "> 500 albums",
albums: [...first500Albums, anAlbum(), anAlbum(), anAlbum()],
});
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml(
first500Albums.map(
(album) => [artist, album] as [Artist, Album]
)
)
)
)
);
});
describe("querying for all of them", () => {
it("will return only the first 500 with the correct paging information", async () => {
const paging = { _index: 0, _count: 1000 };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.albums(paging));
expect(result).toEqual({
results: first500Albums.map(albumToAlbumSummary),
total: 500,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: {
type: "alphabeticalByArtist",
size: 500,
offset: 0,
...authParams,
},
});
});
});
});
});
});