scoll indices based on ND sort name for artists

This commit is contained in:
simojenki
2021-12-24 17:22:10 +11:00
parent c7352aefa3
commit 00944a7a25
8 changed files with 607 additions and 203 deletions

View File

@@ -65,8 +65,8 @@ export type Track = {
}; };
export type Paging = { export type Paging = {
_index: number; _index: number | undefined;
_count: number; _count: number | undefined;
}; };
export type Result<T> = { export type Result<T> = {
@@ -74,9 +74,10 @@ export type Result<T> = {
total: number; total: number;
}; };
export function slice2<T>({ _index, _count }: Paging) { export function slice2<T>({ _index, _count }: Partial<Paging> = {}) {
const i = _index || 0;
return (things: T[]): [T[], number] => [ return (things: T[]): [T[], number] => [
things.slice(_index, _index + _count), _count ? things.slice(i, i + _count) : things.slice(i),
things.length, things.length,
]; ];
} }
@@ -138,6 +139,10 @@ export type Playlist = PlaylistSummary & {
entries: Track[] entries: Track[]
} }
export type Sortable = {
sortName: string
}
export const range = (size: number) => [...Array(size).keys()]; export const range = (size: number) => [...Array(size).keys()];
export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] => export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] =>
@@ -152,7 +157,7 @@ export interface MusicService {
} }
export interface MusicLibrary { export interface MusicLibrary {
artists(q: ArtistQuery): Promise<Result<ArtistSummary>>; artists(q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>>;
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

@@ -19,6 +19,7 @@ import {
Playlist, Playlist,
Rating, Rating,
slice2, slice2,
Sortable,
Track, Track,
} from "./music_service"; } from "./music_service";
import { APITokens } from "./api_tokens"; import { APITokens } from "./api_tokens";
@@ -366,7 +367,7 @@ export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({
albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(), albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(),
}); });
export const scrollIndicesFrom = (artists: ArtistSummary[]) => { export const scrollIndicesFrom = (things: Sortable[]) => {
const indicies: Record<string, number | undefined> = { const indicies: Record<string, number | undefined> = {
"A":undefined, "A":undefined,
"B":undefined, "B":undefined,
@@ -395,9 +396,9 @@ export const scrollIndicesFrom = (artists: ArtistSummary[]) => {
"Y":undefined, "Y":undefined,
"Z":undefined, "Z":undefined,
} }
const sortedNames = artists.map(artist => artist.name.toUpperCase()).sort(); const upperNames = things.map(thing => thing.sortName.toUpperCase());
for(var i = 0; i < sortedNames.length; i++) { for(var i = 0; i < upperNames.length; i++) {
const char = sortedNames[i]![0]!; const char = upperNames[i]![0]!;
if(Object.keys(indicies).includes(char) && indicies[char] == undefined) { if(Object.keys(indicies).includes(char) && indicies[char] == undefined) {
indicies[char] = i; indicies[char] = i;
} }
@@ -1002,7 +1003,7 @@ function bindSmapiSoapServiceToExpress(
switch(id) { switch(id) {
case "artists": { case "artists": {
return login(soapyHeaders?.credentials) return login(soapyHeaders?.credentials)
.then(({ musicLibrary }) => musicLibrary.artists({ _index: 0, _count: 999999999 })) .then(({ musicLibrary }) => musicLibrary.artists({ _index: 0, _count: undefined }))
.then((artists) => ({ .then((artists) => ({
getScrollIndicesResult: scrollIndicesFrom(artists.results) getScrollIndicesResult: scrollIndicesFrom(artists.results)
})) }))

View File

@@ -20,6 +20,7 @@ import {
AlbumQueryType, AlbumQueryType,
Artist, Artist,
AuthFailure, AuthFailure,
Sortable,
} from "./music_service"; } from "./music_service";
import sharp from "sharp"; import sharp from "sharp";
import _ from "underscore"; import _ from "underscore";
@@ -230,6 +231,13 @@ export function isError(
return (subsonicResponse as SubsonicError).error !== undefined; return (subsonicResponse as SubsonicError).error !== undefined;
} }
export type NDArtist = {
id: string;
name: string;
orderArtistName: string | undefined;
largeImageUrl: string | undefined;
};
type IdName = { type IdName = {
id: string; id: string;
name: string; name: string;
@@ -243,6 +251,18 @@ const coverArtURN = (coverArt: string | undefined): BUrn | undefined =>
O.getOrElseW(() => undefined) O.getOrElseW(() => undefined)
); );
export const artistSummaryFromNDArtist = (
artist: NDArtist
): ArtistSummary & Sortable => ({
id: artist.id,
name: artist.name,
sortName: artist.orderArtistName || artist.name,
image: artistImageURN({
artistId: artist.id,
artistImageURL: artist.largeImageUrl,
}),
});
export const artistImageURN = ( export const artistImageURN = (
spec: Partial<{ spec: Partial<{
artistId: string | undefined; artistId: string | undefined;
@@ -394,7 +414,7 @@ const AlbumQueryTypeToSubsonicType: Record<AlbumQueryType, string> = {
const artistIsInLibrary = (artistId: string | undefined) => const artistIsInLibrary = (artistId: string | undefined) =>
artistId != undefined && artistId != "-1"; artistId != undefined && artistId != "-1";
type SubsonicCredentials = Credentials & { export type SubsonicCredentials = Credentials & {
type: string; type: string;
bearer: string | undefined; bearer: string | undefined;
}; };
@@ -481,7 +501,7 @@ export class Subsonic implements MusicService {
TE.chain(({ type }) => TE.chain(({ type }) =>
pipe( pipe(
TE.tryCatch( TE.tryCatch(
() => this.libraryFor({ ...credentials, type }), () => this.libraryFor({ ...credentials, type, bearer: undefined }),
() => new AuthFailure("Failed to get library") () => new AuthFailure("Failed to get library")
), ),
TE.map((library) => ({ type, library })) TE.map((library) => ({ type, library }))
@@ -664,7 +684,7 @@ export class Subsonic implements MusicService {
.then(this.toAlbumSummary), .then(this.toAlbumSummary),
]).then(([total, albums]) => ({ ]).then(([total, albums]) => ({
results: albums.slice(0, q._count), results: albums.slice(0, q._count),
total: albums.length == 500 ? total : q._index + albums.length, total: albums.length == 500 ? total : (q._index || 0) + albums.length,
})); }));
// getStarred2 = (credentials: Credentials): Promise<{ albums: Album[] }> => // getStarred2 = (credentials: Credentials): Promise<{ albums: Album[] }> =>
@@ -677,14 +697,14 @@ export class Subsonic implements MusicService {
login = async (token: string) => this.libraryFor(parseToken(token)); login = async (token: string) => this.libraryFor(parseToken(token));
private libraryFor = ( private libraryFor = (
credentials: Credentials & { type: string } credentials: SubsonicCredentials
): Promise<SubsonicMusicLibrary> => { ): Promise<SubsonicMusicLibrary> => {
const subsonic = this; const subsonic = this;
const genericSubsonic: SubsonicMusicLibrary = { const genericSubsonic: SubsonicMusicLibrary = {
flavour: () => "subsonic", flavour: () => "subsonic",
bearerToken: (_: Credentials) => TE.right(undefined), bearerToken: (_: Credentials) => TE.right(undefined),
artists: (q: ArtistQuery): Promise<Result<ArtistSummary>> => artists: (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
subsonic subsonic
.getArtists(credentials) .getArtists(credentials)
.then(slice2(q)) .then(slice2(q))
@@ -693,6 +713,7 @@ export class Subsonic implements MusicService {
results: page.map((it) => ({ results: page.map((it) => ({
id: it.id, id: it.id,
name: it.name, name: it.name,
sortName: it.name,
image: it.image, image: it.image,
})), })),
})), })),
@@ -970,6 +991,43 @@ export class Subsonic implements MusicService {
), ),
TE.map((it) => it.data.token as string | undefined) TE.map((it) => it.data.token as string | undefined)
), ),
artists: async (q: ArtistQuery): Promise<Result<ArtistSummary & Sortable>> =>
{
let params: any = {
_sort: "name",
_order: "ASC",
_start: q._index || "0",
};
if(q._count) {
params = {
...params,
_end: (q._index || 0) + q._count
}
}
return axios
.get(`${this.url}/api/artist`, {
params: asURLSearchParams(params),
headers: {
"User-Agent": USER_AGENT,
"x-nd-authorization": `Bearer ${credentials.bearer}`,
},
})
.catch((e) => {
throw `Navidrome failed with: ${e}`;
})
.then((response) => {
if (response.status != 200 && response.status != 206) {
throw `Navidrome failed with a ${
response.status || "no!"
} status`;
} else return response;
})
.then((it) => ({
results: (it.data as NDArtist[]).map(artistSummaryFromNDArtist),
total: Number.parseInt(it.headers["x-total-count"] || "0")
}))
}
}); });
} else { } else {
return Promise.resolve(genericSubsonic); return Promise.resolve(genericSubsonic);

View File

@@ -6,6 +6,7 @@ import {
MusicLibrary, MusicLibrary,
artistToArtistSummary, artistToArtistSummary,
albumToAlbumSummary, albumToAlbumSummary,
Artist,
} from "../src/music_service"; } from "../src/music_service";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { import {
@@ -78,6 +79,11 @@ describe("InMemoryMusicService", () => {
musicLibrary = (await service.login(token.serviceToken)) as MusicLibrary; musicLibrary = (await service.login(token.serviceToken)) as MusicLibrary;
}); });
const artistToArtistSummaryWithSortName = (artist: Artist) => ({
...artistToArtistSummary(artist),
sortName: artist.name
})
describe("artists", () => { describe("artists", () => {
const artist1 = anArtist(); const artist1 = anArtist();
const artist2 = anArtist(); const artist2 = anArtist();
@@ -95,11 +101,11 @@ describe("InMemoryMusicService", () => {
await musicLibrary.artists({ _index: 0, _count: 100 }) await musicLibrary.artists({ _index: 0, _count: 100 })
).toEqual({ ).toEqual({
results: [ results: [
artistToArtistSummary(artist1), artistToArtistSummaryWithSortName(artist1),
artistToArtistSummary(artist2), artistToArtistSummaryWithSortName(artist2),
artistToArtistSummary(artist3), artistToArtistSummaryWithSortName(artist3),
artistToArtistSummary(artist4), artistToArtistSummaryWithSortName(artist4),
artistToArtistSummary(artist5), artistToArtistSummaryWithSortName(artist5),
], ],
total: 5, total: 5,
}); });
@@ -110,8 +116,8 @@ describe("InMemoryMusicService", () => {
it("should provide an array of artists", async () => { it("should provide an array of artists", async () => {
expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual({ expect(await musicLibrary.artists({ _index: 2, _count: 2 })).toEqual({
results: [ results: [
artistToArtistSummary(artist3), artistToArtistSummaryWithSortName(artist3),
artistToArtistSummary(artist4), artistToArtistSummaryWithSortName(artist4),
], ],
total: 5, total: 5,
}); });
@@ -121,7 +127,7 @@ describe("InMemoryMusicService", () => {
describe("fetching the last page", () => { describe("fetching the last page", () => {
it("should provide an array of artists", async () => { it("should provide an array of artists", async () => {
expect(await musicLibrary.artists({ _index: 4, _count: 2 })).toEqual({ expect(await musicLibrary.artists({ _index: 4, _count: 2 })).toEqual({
results: [artistToArtistSummary(artist5)], results: [artistToArtistSummaryWithSortName(artist5)],
total: 5, total: 5,
}); });
}); });

View File

@@ -62,7 +62,7 @@ export class InMemoryMusicService implements MusicService {
return Promise.resolve({ return Promise.resolve({
artists: (q: ArtistQuery) => artists: (q: ArtistQuery) =>
Promise.resolve(this.artists.map(artistToArtistSummary)) Promise.resolve(this.artists.map(artistToArtistSummary).map(it => ({ ...it, sortName: it.name })))
.then(slice2(q)) .then(slice2(q))
.then(asResult), .then(asResult),
artist: (id: string) => artist: (id: string) =>

View File

@@ -1,7 +1,57 @@
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { anArtist } from "./builders"; import { anArtist } from "./builders";
import { artistToArtistSummary } from "../src/music_service"; import { artistToArtistSummary, slice2 } from "../src/music_service";
describe("slice2", () => {
const things = ["a", "b", "c", "d", "e", "f", "g", "h", "i"];
describe("when slice is a subset of the things", () => {
it("should return the page", () => {
expect(slice2({ _index: 3, _count: 4 })(things)).toEqual([
["d", "e", "f", "g"],
things.length
])
});
});
describe("when slice goes off the end of the things", () => {
it("should return the page", () => {
expect(slice2({ _index: 5, _count: 100 })(things)).toEqual([
["f", "g", "h", "i"],
things.length
])
});
});
describe("when no _count is provided", () => {
it("should return from the index", () => {
expect(slice2({ _index: 5 })(things)).toEqual([
["f", "g", "h", "i"],
things.length
])
});
});
describe("when no _index is provided", () => {
it("should assume from the start", () => {
expect(slice2({ _count: 3 })(things)).toEqual([
["a", "b", "c"],
things.length
])
});
});
describe("when no _index or _count is provided", () => {
it("should return all the things", () => {
expect(slice2()(things)).toEqual([
things,
things.length
])
});
});
});
describe("artistToArtistSummary", () => { describe("artistToArtistSummary", () => {
it("should map fields correctly", () => { it("should map fields correctly", () => {

View File

@@ -863,10 +863,12 @@ describe("defaultArtistArtURI", () => {
describe("scrollIndicesFrom", () => { describe("scrollIndicesFrom", () => {
describe("artists", () => { describe("artists", () => {
describe("when sortName is the same as name", () => {
it("should be scroll indicies", () => { it("should be scroll indicies", () => {
const artistNames = [ const artistNames = [
"10,000 Maniacs", "10,000 Maniacs",
"99 Bacon Sandwiches", "99 Bacon Sandwiches",
"[something with square brackets]",
"Aerosmith", "Aerosmith",
"Bob Marley", "Bob Marley",
"beatles", // intentionally lower case "beatles", // intentionally lower case
@@ -877,11 +879,34 @@ describe("scrollIndicesFrom", () => {
"Numpty", "Numpty",
"Yellow brick road" "Yellow brick road"
] ]
const scrollIndicies = scrollIndicesFrom(_.shuffle(artistNames).map(name => anArtist({ name }))) const scrollIndicies = scrollIndicesFrom(artistNames.map(name => ({ name, sortName: name })))
expect(scrollIndicies).toEqual("A,2,B,3,C,5,D,5,E,6,F,6,G,6,H,6,I,6,J,6,K,6,L,6,M,7,N,9,O,9,P,9,Q,9,R,9,S,9,T,9,U,9,V,9,W,9,X,9,Y,10,Z,10") expect(scrollIndicies).toEqual("A,3,B,4,C,6,D,6,E,7,F,7,G,7,H,7,I,7,J,7,K,7,L,7,M,8,N,10,O,10,P,10,Q,10,R,10,S,10,T,10,U,10,V,10,W,10,X,10,Y,11,Z,11")
}); });
}); });
describe("when sortName is different to the name name", () => {
it("should be scroll indicies", () => {
const artistSortNames = [
"10,000 Maniacs",
"99 Bacon Sandwiches",
"[something with square brackets]",
"Aerosmith",
"Bob Marley",
"beatles", // intentionally lower case
"Cans",
"egg heads", // intentionally lower case
"Moon Cakes",
"Moon Boots",
"Numpty",
"Yellow brick road"
]
const scrollIndicies = scrollIndicesFrom(artistSortNames.map(name => ({ name: uuid(), sortName: name })))
expect(scrollIndicies).toEqual("A,3,B,4,C,6,D,6,E,7,F,7,G,7,H,7,I,7,J,7,K,7,L,7,M,8,N,10,O,10,P,10,Q,10,R,10,S,10,T,10,U,10,V,10,W,10,X,10,Y,11,Z,11")
});
})
});
}); });
describe("wsdl api", () => { describe("wsdl api", () => {
@@ -3152,6 +3177,9 @@ describe("wsdl api", () => {
const artist5 = anArtist({ name: "Metallica" }); const artist5 = anArtist({ name: "Metallica" });
const artist6 = anArtist({ name: "Yellow Brick Road" }); const artist6 = anArtist({ name: "Yellow Brick Road" });
const artists = [artist1, artist2, artist3, artist4, artist5, artist6];
const artistsWithSortName = artists.map(it => ({ ...it, sortName: it.name }));
beforeEach(async () => { beforeEach(async () => {
ws = await createClientAsync(`${service.uri}?wsdl`, { ws = await createClientAsync(`${service.uri}?wsdl`, {
endpoint: service.uri, endpoint: service.uri,
@@ -3159,7 +3187,7 @@ describe("wsdl api", () => {
}); });
setupAuthenticatedRequest(ws); setupAuthenticatedRequest(ws);
musicLibrary.artists.mockResolvedValue({ musicLibrary.artists.mockResolvedValue({
results: [artist1, artist2, artist3, artist4, artist5, artist6], results: artistsWithSortName,
total: 6 total: 6
}); });
}); });
@@ -3170,11 +3198,11 @@ describe("wsdl api", () => {
}); });
expect(root[0]).toEqual({ expect(root[0]).toEqual({
getScrollIndicesResult: scrollIndicesFrom([artist1, artist2, artist3, artist4, artist5, artist6]) getScrollIndicesResult: scrollIndicesFrom(artistsWithSortName)
}); });
expect(musicService.login).toHaveBeenCalledWith(serviceToken); expect(musicService.login).toHaveBeenCalledWith(serviceToken);
expect(apiTokens.mint).toHaveBeenCalledWith(serviceToken); expect(apiTokens.mint).toHaveBeenCalledWith(serviceToken);
expect(musicLibrary.artists).toHaveBeenCalledWith({ _index: 0, _count: 999999999 }); expect(musicLibrary.artists).toHaveBeenCalledWith({ _index: 0, _count: undefined });
}); });
}); });
}); });

View File

@@ -22,6 +22,8 @@ import {
PingResponse, PingResponse,
parseToken, parseToken,
asToken, asToken,
artistSummaryFromNDArtist,
SubsonicCredentials,
} from "../src/subsonic"; } from "../src/subsonic";
import axios from "axios"; import axios from "axios";
@@ -46,7 +48,6 @@ import {
Playlist, Playlist,
SimilarArtist, SimilarArtist,
Rating, Rating,
Credentials,
AuthFailure, AuthFailure,
} from "../src/music_service"; } from "../src/music_service";
import { import {
@@ -576,6 +577,78 @@ const pingJson = (pingResponse: Partial<PingResponse> = {}) => ({
const PING_OK = pingJson({ status: "ok" }); const PING_OK = pingJson({ status: "ok" });
describe("artistSummaryFromNDArtist", () => {
describe("when the orderArtistName is undefined", () => {
it("should use name", () => {
const artist = {
id: uuid(),
name: `name ${uuid()}`,
orderArtistName: undefined,
largeImageUrl: 'http://example.com/something.jpg'
}
expect(artistSummaryFromNDArtist(artist)).toEqual({
id: artist.id,
name: artist.name,
sortName: artist.name,
image: artistImageURN({ artistId: artist.id, artistImageURL: artist.largeImageUrl })
})
});
});
describe("when the artist image is valid", () => {
it("should create an ArtistSummary with Sortable", () => {
const artist = {
id: uuid(),
name: `name ${uuid()}`,
orderArtistName: `orderArtistName ${uuid()}`,
largeImageUrl: 'http://example.com/something.jpg'
}
expect(artistSummaryFromNDArtist(artist)).toEqual({
id: artist.id,
name: artist.name,
sortName: artist.orderArtistName,
image: artistImageURN({ artistId: artist.id, artistImageURL: artist.largeImageUrl })
})
});
});
describe("when the artist image is not valid", () => {
it("should create an ArtistSummary with Sortable", () => {
const artist = {
id: uuid(),
name: `name ${uuid()}`,
orderArtistName: `orderArtistName ${uuid()}`,
largeImageUrl: `http://example.com/${DODGY_IMAGE_NAME}`
}
expect(artistSummaryFromNDArtist(artist)).toEqual({
id: artist.id,
name: artist.name,
sortName: artist.orderArtistName,
image: artistImageURN({ artistId: artist.id, artistImageURL: artist.largeImageUrl })
});
});
});
describe("when the artist image is missing", () => {
it("should create an ArtistSummary with Sortable", () => {
const artist = {
id: uuid(),
name: `name ${uuid()}`,
orderArtistName: `orderArtistName ${uuid()}`,
largeImageUrl: undefined
}
expect(artistSummaryFromNDArtist(artist)).toEqual({
id: artist.id,
name: artist.name,
sortName: artist.orderArtistName,
image: artistImageURN({ artistId: artist.id, artistImageURL: artist.largeImageUrl })
});
});
});
});
describe("artistURN", () => { describe("artistURN", () => {
describe("when artist URL is", () => { describe("when artist URL is", () => {
describe("a valid external URL", () => { describe("a valid external URL", () => {
@@ -731,12 +804,18 @@ describe("Subsonic", () => {
"User-Agent": "bonob", "User-Agent": "bonob",
}; };
const tokenFor = (credentials: Credentials) => pipe( const tokenFor = (credentials: Partial<SubsonicCredentials>) => pipe(
subsonic.generateToken(credentials), subsonic.generateToken({
username: "some username",
password: "some password",
bearer: undefined,
type: "subsonic",
...credentials
}),
TE.fold(e => { throw e }, T.of) TE.fold(e => { throw e }, T.of)
) )
const login = (credentials: Credentials) => tokenFor(credentials)() const login = (credentials: Partial<SubsonicCredentials> = {}) => tokenFor(credentials)()
.then((it) => subsonic.login(it.serviceToken)) .then((it) => subsonic.login(it.serviceToken))
describe("generateToken", () => { describe("generateToken", () => {
@@ -1555,6 +1634,7 @@ describe("Subsonic", () => {
}); });
describe("getting artists", () => { describe("getting artists", () => {
describe("when subsonic flavour is generic", () => {
describe("when there are indexes, but no artists", () => { describe("when there are indexes, but no artists", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
@@ -1654,6 +1734,7 @@ describe("Subsonic", () => {
id: artist1.id, id: artist1.id,
image: artist1.image, image: artist1.image,
name: artist1.name, name: artist1.name,
sortName: artist1.name
}]; }];
expect(artists).toEqual({ expect(artists).toEqual({
@@ -1694,6 +1775,7 @@ describe("Subsonic", () => {
id: it.id, id: it.id,
image: it.image, image: it.image,
name: it.name, name: it.name,
sortName: it.name,
}) })
); );
@@ -1726,6 +1808,7 @@ describe("Subsonic", () => {
id: it.id, id: it.id,
image: it.image, image: it.image,
name: it.name, name: it.name,
sortName: it.name,
})); }));
expect(artists).toEqual({ results: expectedResults, total: 4 }); expect(artists).toEqual({ results: expectedResults, total: 4 });
@@ -1739,6 +1822,179 @@ describe("Subsonic", () => {
}); });
}); });
describe("when the subsonic type is navidrome", () => {
const ndArtist1 = {
id: uuid(),
name: "Artist1",
orderArtistName: "Artist1",
largeImageUrl: "http://example.com/artist1/image.jpg"
};
const ndArtist2 = {
id: uuid(),
name: "Artist2",
orderArtistName: "The Artist2",
largeImageUrl: undefined
};
const ndArtist3 = {
id: uuid(),
name: "Artist3",
orderArtistName: "An Artist3",
largeImageUrl: `http://example.com/artist3/${DODGY_IMAGE_NAME}`
};
const ndArtist4 = {
id: uuid(),
name: "Artist4",
orderArtistName: "An Artist4",
largeImageUrl: `http://example.com/artist4/${DODGY_IMAGE_NAME}`
};
const bearer = `bearer-${uuid()}`;
describe("when no paging is specified", () => {
beforeEach(() => {
(axios.get as jest.Mock)
.mockImplementationOnce(() => Promise.resolve(ok(pingJson({ type: "navidrome" }))))
.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: [
ndArtist1,
ndArtist2,
ndArtist3,
ndArtist4,
],
headers: {
"x-total-count": "4"
}
})
);
(axios.post as jest.Mock).mockResolvedValue(ok({ token: bearer }));
});
it("should fetch all artists", async () => {
const artists = await login({ username, password, bearer, type: "navidrome" })
.then((it) => it.artists({ _index: undefined, _count: undefined }));
expect(artists).toEqual({
results: [
artistSummaryFromNDArtist(ndArtist1),
artistSummaryFromNDArtist(ndArtist2),
artistSummaryFromNDArtist(ndArtist3),
artistSummaryFromNDArtist(ndArtist4),
],
total: 4,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/api/artist`, {
params: asURLSearchParams({
_sort: "name",
_order: "ASC",
_start: "0"
}),
headers: {
"User-Agent": "bonob",
"x-nd-authorization": `Bearer ${bearer}`,
},
});
});
});
describe("when start index is specified", () => {
beforeEach(() => {
(axios.get as jest.Mock)
.mockImplementationOnce(() => Promise.resolve(ok(pingJson({ type: "navidrome" }))))
.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: [
ndArtist3,
ndArtist4,
],
headers: {
"x-total-count": "5"
}
})
);
(axios.post as jest.Mock).mockResolvedValue(ok({ token: bearer }));
});
it("should fetch all artists", async () => {
const artists = await login({ username, password, bearer, type: "navidrome" })
.then((it) => it.artists({ _index: 2, _count: undefined }));
expect(artists).toEqual({
results: [
artistSummaryFromNDArtist(ndArtist3),
artistSummaryFromNDArtist(ndArtist4),
],
total: 5,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/api/artist`, {
params: asURLSearchParams({
_sort: "name",
_order: "ASC",
_start: "2"
}),
headers: {
"User-Agent": "bonob",
"x-nd-authorization": `Bearer ${bearer}`,
},
});
});
});
describe("when start index and count is specified", () => {
beforeEach(() => {
(axios.get as jest.Mock)
.mockImplementationOnce(() => Promise.resolve(ok(pingJson({ type: "navidrome" }))))
.mockImplementationOnce(() =>
Promise.resolve({
status: 200,
data: [
ndArtist3,
ndArtist4,
],
headers: {
"x-total-count": "5"
}
})
);
(axios.post as jest.Mock).mockResolvedValue(ok({ token: bearer }));
});
it("should fetch all artists", async () => {
const artists = await login({ username, password, bearer, type: "navidrome" })
.then((it) => it.artists({ _index: 2, _count: 23 }));
expect(artists).toEqual({
results: [
artistSummaryFromNDArtist(ndArtist3),
artistSummaryFromNDArtist(ndArtist4),
],
total: 5,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/api/artist`, {
params: asURLSearchParams({
_sort: "name",
_order: "ASC",
_start: "2",
_end: "25"
}),
headers: {
"User-Agent": "bonob",
"x-nd-authorization": `Bearer ${bearer}`,
},
});
});
});
});
});
describe("getting albums", () => { describe("getting albums", () => {
describe("filtering", () => { describe("filtering", () => {
const album1 = anAlbum({ id: "album1", genre: asGenre("Pop") }); const album1 = anAlbum({ id: "album1", genre: asGenre("Pop") });