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 & { export type AlbumQuery = Paging & {
artistId?: string 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 { export interface MusicService {
generateToken(credentials: Credentials): Promise<AuthSuccess | AuthFailure>; generateToken(credentials: Credentials): Promise<AuthSuccess | AuthFailure>;
login(authToken: string): Promise<MusicLibrary>; login(authToken: string): Promise<MusicLibrary>;
@@ -84,5 +103,5 @@ export interface MusicLibrary {
artists(q: ArtistQuery): Promise<Result<ArtistSummary>>; artists(q: ArtistQuery): Promise<Result<ArtistSummary>>;
artist(id: string): Promise<Artist>; artist(id: string): Promise<Artist>;
albums(q: AlbumQuery): Promise<Result<AlbumSummary>>; 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 { Md5 } from "ts-md5/dist/md5";
import { import {
Credentials, Credentials,
@@ -19,6 +21,7 @@ import axios from "axios";
import { Encryption } from "./encryption"; import { Encryption } from "./encryption";
import randomString from "./random_string"; import randomString from "./random_string";
export const t = (password: string, s: string) => export const t = (password: string, s: string) =>
Md5.hashStr(`${password}${s}`); Md5.hashStr(`${password}${s}`);
@@ -66,6 +69,12 @@ export type GetArtistsResponse = SubsonicResponse & {
}; };
}; };
export type GetAlbumListResponse = SubsonicResponse & {
albumList: {
album: album[];
};
};
export type SubsonicError = SubsonicResponse & { export type SubsonicError = SubsonicResponse & {
error: { error: {
_code: string; _code: string;
@@ -105,6 +114,17 @@ export type IdName = {
name: string; 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 { export class Navidrome implements MusicService {
url: string; url: string;
encryption: Encryption; encryption: Encryption;
@@ -235,8 +255,36 @@ export class Navidrome implements MusicService {
image: artistInfo.image, image: artistInfo.image,
albums: artist.albums, albums: artist.albums,
})), })),
albums: (_: AlbumQuery): Promise<Result<AlbumSummary>> => { albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> => {
return Promise.resolve({ results: [], total: 0 }); 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 logger from "./logger";
import { LinkCodes } from "./link_codes"; 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 LOGIN_ROUTE = "/login";
export const SOAP_PATH = "/ws/sonos"; export const SOAP_PATH = "/ws/sonos";
@@ -250,14 +255,14 @@ function bindSmapiSoapServiceToExpress(
total: result.total, total: result.total,
}) })
); );
case "albums": case "albums":
return await musicLibrary.albums(paging).then((result) => return await musicLibrary.albums(paging).then((result) =>
getMetadataResult({ getMetadataResult({
mediaCollection: result.results.map(album), mediaCollection: result.results.map(album),
index: paging._index, index: paging._index,
total: result.total, total: result.total,
}) })
); );
case "artist": case "artist":
return await musicLibrary return await musicLibrary
.artist(typeId!) .artist(typeId!)

View File

@@ -5,7 +5,7 @@ import { Credentials } from "../src/smapi";
import { Service, Device } from "../src/sonos"; import { Service, Device } from "../src/sonos";
import { Album, Artist } from "../src/music_service"; 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)}`; const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`;
export const aService = (fields: Partial<Service> = {}): Service => ({ export const aService = (fields: Partial<Service> = {}): Service => ({
@@ -73,25 +73,26 @@ export function anArtist(fields: Partial<Artist> = {}): Artist {
return { return {
id, id,
name: `Artist ${id}`, name: `Artist ${id}`,
albums: [], albums: [anAlbum(), anAlbum(), anAlbum()],
image: { image: {
small: undefined, small: `/artist/art/${id}/small`,
medium: undefined, medium: `/artist/art/${id}/small`,
large: undefined large: `/artist/art/${id}/large`,
}, },
...fields ...fields,
} };
} }
export function anAlbum(fields: Partial<Album> = {}): Album { export function anAlbum(fields: Partial<Album> = {}): Album {
const genres = ["Metal", "Pop", "Rock", "Hip-Hop"];
const id = uuid(); const id = uuid();
return { return {
id, id,
name: `Album ${id}`, name: `Album ${id}`,
genre: "Metal", genre: genres[randomInt(genres.length)],
year: "1900", year: `19${randomInt(99)}`,
...fields ...fields,
} };
} }
export const BLONDIE: Artist = { 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 { import {
InMemoryMusicService, AuthSuccess,
MusicLibrary,
artistToArtistSummary, artistToArtistSummary,
} from "./in_memory_music_service"; albumToAlbumSummary,
import { AuthSuccess, MusicLibrary } from "../src/music_service"; } from "../src/music_service";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { import { anArtist, anAlbum } from "./builders";
BOB_MARLEY,
MADONNA,
BLONDIE,
METALLICA,
ALL_ALBUMS,
} from "./builders";
describe("InMemoryMusicService", () => { describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService(); const service = new InMemoryMusicService();
@@ -48,10 +44,19 @@ describe("InMemoryMusicService", () => {
describe("artistToArtistSummary", () => { describe("artistToArtistSummary", () => {
it("should map fields correctly", () => { it("should map fields correctly", () => {
expect(artistToArtistSummary(BOB_MARLEY)).toEqual({ const artist = anArtist({
id: BOB_MARLEY.id, id: uuid(),
name: BOB_MARLEY.name, name: "The Artist",
image: BOB_MARLEY.image, 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 () => { beforeEach(async () => {
service.clear(); service.clear();
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;
@@ -71,59 +75,67 @@ describe("InMemoryMusicService", () => {
}); });
describe("artists", () => { 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 () => { it("should provide an array of artists", async () => {
const artists = [
artistToArtistSummary(BOB_MARLEY),
artistToArtistSummary(MADONNA),
artistToArtistSummary(BLONDIE),
artistToArtistSummary(METALLICA),
];
expect( expect(
await musicLibrary.artists({ _index: 0, _count: 100 }) await musicLibrary.artists({ _index: 0, _count: 100 })
).toEqual({ ).toEqual({
results: artists, results: [
total: 4, artistToArtistSummary(artist1),
artistToArtistSummary(artist2),
artistToArtistSummary(artist3),
artistToArtistSummary(artist4),
artistToArtistSummary(artist5),
],
total: 5,
}); });
}); });
}); });
describe("fetching the second page", () => { describe("fetching the second page", () => {
it("should provide an array of artists", async () => { it("should provide an array of artists", async () => {
const artists = [
artistToArtistSummary(BLONDIE),
artistToArtistSummary(METALLICA),
];
expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual({ expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual({
results: artists, results: [
total: 4, 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 () => { it("should provide an array of artists", async () => {
const artists = [ expect(await musicLibrary.artists({ _index: 4, _count: 2 })).toEqual({
artistToArtistSummary(MADONNA), results: [artistToArtistSummary(artist5)],
artistToArtistSummary(BLONDIE), total: 5,
artistToArtistSummary(METALLICA), });
];
expect(
await musicLibrary.artists({ _index: 1, _count: 50 })
).toEqual({ results: artists, total: 4 });
}); });
}); });
}); });
describe("artist", () => { 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", () => { describe("when it exists", () => {
it("should provide an artist", async () => { it("should provide an artist", async () => {
expect(await musicLibrary.artist(MADONNA.id)).toEqual( expect(await musicLibrary.artist(artist1.id)).toEqual(artist1);
MADONNA expect(await musicLibrary.artist(artist2.id)).toEqual(artist2);
);
expect(await musicLibrary.artist(BLONDIE.id)).toEqual(
BLONDIE
);
}); });
}); });
@@ -136,36 +148,177 @@ describe("InMemoryMusicService", () => {
}); });
}); });
describe("albums", () => { describe("album", () => {
describe("fetching with no filtering", () => { describe("when it exists", () => {
it("should return all the albums for all the artists", async () => { const albumToLookFor = anAlbum({ id: "albumToLookFor" });
expect(await musicLibrary.albums({ _index: 0, _count: 100 })).toEqual( const artist1 = anArtist({ albums: [anAlbum(), anAlbum(), anAlbum()] });
{ const artist2 = anArtist({
results: ALL_ALBUMS, albums: [anAlbum(), albumToLookFor, anAlbum()],
total: ALL_ALBUMS.length, });
}
beforeEach(() => {
service.hasArtists(artist1, artist2);
});
it("should provide an artist", async () => {
expect(await musicLibrary.album(albumToLookFor.id)).toEqual(
albumToLookFor
); );
}); });
}); });
describe("fetching for a single artist", () => { describe("when it doesnt exist", () => {
it("should return them all if the artist has some", async () => { it("should blow up", async () => {
expect( return expect(musicLibrary.album("-1")).rejects.toEqual(
await musicLibrary.albums({ "No album with id '-1'"
artistId: BLONDIE.id, );
_index: 0, });
_count: 100, });
}) });
).toEqual({
results: BLONDIE.albums, describe("albums", () => {
total: BLONDIE.albums.length, 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({
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,
});
}); });
}); });
it("should return empty list of the artists does not have any", async () => { describe("fetching a page", () => {
it("should return only that page", async () => {
expect(await musicLibrary.albums({ _index: 4, _count: 3 })).toEqual(
{
results: [
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
],
total: totalAlbumCount,
}
);
});
});
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: artist3.id,
_index: 0,
_count: 100,
})
).toEqual({
results: [
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: artist3.albums.length,
});
});
});
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: 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( expect(
await musicLibrary.albums({ await musicLibrary.albums({
artistId: MADONNA.id, artistId: artistWithNoAlbums.id,
_index: 0, _index: 0,
_count: 100, _count: 100,
}) })
@@ -189,25 +342,71 @@ describe("InMemoryMusicService", () => {
}); });
}); });
describe("fetching with index and count", () => { describe("filtering by genre", () => {
it("should be able to return the first page", async () => { describe("fetching all on one page", () => {
const albums = [BOB_MARLEY.albums[0], BOB_MARLEY.albums[1]]; it.only("should return all the albums of that genre for all the artists", async () => {
expect(await musicLibrary.albums({ _index: 0, _count: 2 })).toEqual({ expect(
results: albums, await musicLibrary.albums({ _index: 0, _count: 100, genre: "Pop" })
total: ALL_ALBUMS.length, ).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]]; describe("when the genre has more albums than a single page", () => {
expect(await musicLibrary.albums({ _index: 2, _count: 2 })).toEqual({ describe("can fetch a single page", () => {
results: albums, it("should return only the albums for that page", async () => {
total: ALL_ALBUMS.length, 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,
});
});
}); });
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 be able to return the last page", async () => {
expect(await musicLibrary.albums({ _index: 5, _count: 2 })).toEqual({ it("should return empty list if there are no albums for the genre", async () => {
results: METALLICA.albums, expect(
total: ALL_ALBUMS.length, 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 { option as O } from "fp-ts";
import { pipe } from "fp-ts/lib/function"; import { pipe } from "fp-ts/lib/function";
import { import {
MusicService, MusicService,
Credentials, Credentials,
@@ -13,33 +12,20 @@ import {
AlbumQuery, AlbumQuery,
slice2, slice2,
asResult, asResult,
ArtistSummary, artistToArtistSummary,
albumToAlbumSummary,
Album, Album,
AlbumSummary
} from "../src/music_service"; } 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; type P<T> = (t: T) => boolean;
const all: P<any> = (_: any) => true; 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; artist.id === id;
const albumWithGenre = (genre: string): P<[Artist, Album]> => ([_, album]) =>
album.genre === genre;
export class InMemoryMusicService implements MusicService { export class InMemoryMusicService implements MusicService {
users: Record<string, string> = {}; users: Record<string, string> = {};
artists: Artist[] = []; artists: Artist[] = [];
@@ -76,24 +62,40 @@ export class InMemoryMusicService implements MusicService {
pipe( pipe(
this.artists.find((it) => it.id === id), this.artists.find((it) => it.id === id),
O.fromNullable, O.fromNullable,
O.map(it => Promise.resolve(it)), O.map((it) => Promise.resolve(it)),
O.getOrElse(() => Promise.reject(`No artist with id '${id}'`)) O.getOrElse(() => Promise.reject(`No artist with id '${id}'`))
), ),
albums: (q: AlbumQuery) => albums: (q: AlbumQuery) =>
Promise.resolve( Promise.resolve(
this.artists.filter( this.artists
pipe( .flatMap((artist) => artist.albums.map((album) => [artist, album]))
O.fromNullable(q.artistId), .filter(
O.map(artistWithId), pipe(
O.getOrElse(() => all) pipe(
O.fromNullable(q.artistId),
O.map(albumByArtist)
),
O.alt(() =>
pipe(
O.fromNullable(q.genre),
O.map(albumWithGenre)
)
),
O.getOrElse(() => all)
)
) )
)
) )
.then((artists) => artists.flatMap((it) => it.albums)) .then((matches) => matches.map(([_, album]) => album as Album))
.then(it => it.map(albumToAlbumSummary)) .then((it) => it.map(albumToAlbumSummary))
.then(slice2(q)) .then(slice2(q))
.then(asResult), .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"); jest.mock("axios");
import randomString from "../src/random_string"; 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"); jest.mock("../src/random_string");
describe("t", () => { describe("t", () => {
@@ -71,6 +79,16 @@ const albumXml = (artist: Artist, album: Album) => `<album id="${album.id}"
songCount="19" songCount="19"
isVideo="false"></album>`; 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 = ( const artistXml = (
artist: Artist artist: Artist
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"> ) => `<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", () => { describe("getting an artist", () => {
const album1: Album = { const album1: Album = anAlbum();
id: "album1",
name: "super album",
year: "2001",
genre: "Pop",
};
const album2: Album = { const album2: Album = anAlbum();
id: "album2",
name: "bad album",
year: "2002",
genre: "Rock",
};
const artist: Artist = { const artist: Artist = anArtist({
id: "someUUID_123",
name: "BananaMan",
image: {
small: "sml1",
medium: "med1",
large: "lge1",
},
albums: [album1, album2], albums: [album1, album2],
}; });
beforeEach(() => { beforeEach(() => {
mockGET mockGET
@@ -180,37 +181,39 @@ describe("Navidrome", () => {
); );
}); });
it.only("should do it", async () => { describe("when the artist exists", () => {
const result: Artist = await navidrome it("should return it", async () => {
.generateToken({ username, password }) const result: Artist = await navidrome
.then((it) => it as AuthSuccess) .generateToken({ username, password })
.then((it) => navidrome.login(it.authToken)) .then((it) => it as AuthSuccess)
.then((it) => it.artist(artist.id)); .then((it) => navidrome.login(it.authToken))
.then((it) => it.artist(artist.id));
expect(result).toEqual({ expect(result).toEqual({
id: artist.id,
name: artist.name,
image: { small: "sml1", medium: "med1", large: "lge1" },
albums: [album1, album2]
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
params: {
id: artist.id, id: artist.id,
...authParams, name: artist.name,
}, image: artist.image,
}); albums: artist.albums,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
params: { params: {
id: artist.id, id: artist.id,
...authParams, ...authParams,
}, },
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
params: {
id: artist.id,
...authParams,
},
});
}); });
}); });
}); });
describe("getArtists", () => { describe("getting artists", () => {
describe("when there are no results", () => { describe("when there are no results", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
@@ -246,30 +249,10 @@ describe("Navidrome", () => {
}); });
describe("when there are artists", () => { describe("when there are artists", () => {
const artist1: Artist = { const artist1 = anArtist();
id: "artist1.id", const artist2 = anArtist();
name: "artist1.name", const artist3 = anArtist();
image: { small: "s1", medium: "m1", large: "l1" }, const artist4 = anArtist();
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 getArtistsXml = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"> 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"> <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,
},
});
});
});
});
});
}); });