mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
scoll indices based on ND sort name for artists
This commit is contained in:
@@ -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>;
|
||||||
|
|||||||
11
src/smapi.ts
11
src/smapi.ts
@@ -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)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -863,24 +863,49 @@ describe("defaultArtistArtURI", () => {
|
|||||||
|
|
||||||
describe("scrollIndicesFrom", () => {
|
describe("scrollIndicesFrom", () => {
|
||||||
describe("artists", () => {
|
describe("artists", () => {
|
||||||
it("should be scroll indicies", () => {
|
describe("when sortName is the same as name", () => {
|
||||||
const artistNames = [
|
it("should be scroll indicies", () => {
|
||||||
"10,000 Maniacs",
|
const artistNames = [
|
||||||
"99 Bacon Sandwiches",
|
"10,000 Maniacs",
|
||||||
"Aerosmith",
|
"99 Bacon Sandwiches",
|
||||||
"Bob Marley",
|
"[something with square brackets]",
|
||||||
"beatles", // intentionally lower case
|
"Aerosmith",
|
||||||
"Cans",
|
"Bob Marley",
|
||||||
"egg heads", // intentionally lower case
|
"beatles", // intentionally lower case
|
||||||
"Moon Cakes",
|
"Cans",
|
||||||
"Moon Boots",
|
"egg heads", // intentionally lower case
|
||||||
"Numpty",
|
"Moon Cakes",
|
||||||
"Yellow brick road"
|
"Moon Boots",
|
||||||
]
|
"Numpty",
|
||||||
const scrollIndicies = scrollIndicesFrom(_.shuffle(artistNames).map(name => anArtist({ name })))
|
"Yellow brick road"
|
||||||
|
]
|
||||||
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")
|
const scrollIndicies = scrollIndicesFrom(artistNames.map(name => ({ name, 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("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")
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,13 +804,19 @@ 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", () => {
|
||||||
describe("when the credentials are valid", () => {
|
describe("when the credentials are valid", () => {
|
||||||
@@ -1555,187 +1634,364 @@ describe("Subsonic", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getting artists", () => {
|
describe("getting artists", () => {
|
||||||
describe("when there are indexes, but no artists", () => {
|
describe("when subsonic flavour is generic", () => {
|
||||||
beforeEach(() => {
|
describe("when there are indexes, but no artists", () => {
|
||||||
mockGET
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
|
||||||
.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
ok(
|
|
||||||
subsonicOK({
|
|
||||||
artists: {
|
|
||||||
index: [
|
|
||||||
{
|
|
||||||
name: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "A",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "B",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty", async () => {
|
|
||||||
const artists = await login({ username, password })
|
|
||||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
|
||||||
|
|
||||||
expect(artists).toEqual({
|
|
||||||
results: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when there no indexes and no artists", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockGET
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
|
||||||
.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
ok(
|
|
||||||
subsonicOK({
|
|
||||||
artists: {},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty", async () => {
|
|
||||||
const artists = await login({ username, password })
|
|
||||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
|
||||||
|
|
||||||
expect(artists).toEqual({
|
|
||||||
results: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when there is one index and one artist", () => {
|
|
||||||
const artist1 = anArtist({albums:[anAlbum(), anAlbum(), anAlbum(), anAlbum()]});
|
|
||||||
|
|
||||||
const asArtistsJson = subsonicOK({
|
|
||||||
artists: {
|
|
||||||
index: [
|
|
||||||
{
|
|
||||||
name: "#",
|
|
||||||
artist: [
|
|
||||||
{
|
|
||||||
id: artist1.id,
|
|
||||||
name: artist1.name,
|
|
||||||
albumCount: artist1.albums.length,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when it all fits on one page", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockGET
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(asArtistsJson)));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the single artist", async () => {
|
|
||||||
const artists = await login({ username, password })
|
|
||||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
|
||||||
|
|
||||||
const expectedResults = [{
|
|
||||||
id: artist1.id,
|
|
||||||
image: artist1.image,
|
|
||||||
name: artist1.name,
|
|
||||||
}];
|
|
||||||
|
|
||||||
expect(artists).toEqual({
|
|
||||||
results: expectedResults,
|
|
||||||
total: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
|
||||||
params: asURLSearchParams(authParamsPlusJson),
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when there are artists", () => {
|
|
||||||
const artist1 = anArtist({ name: "A Artist", albums:[anAlbum()] });
|
|
||||||
const artist2 = anArtist({ name: "B Artist" });
|
|
||||||
const artist3 = anArtist({ name: "C Artist" });
|
|
||||||
const artist4 = anArtist({ name: "D Artist" });
|
|
||||||
const artists = [artist1, artist2, artist3, artist4];
|
|
||||||
|
|
||||||
describe("when no paging is in effect", () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(asArtistsJson(artists)))
|
Promise.resolve(
|
||||||
|
ok(
|
||||||
|
subsonicOK({
|
||||||
|
artists: {
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
name: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "B",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return all the artists", async () => {
|
it("should return empty", async () => {
|
||||||
const artists = await login({ username, password })
|
const artists = await login({ username, password })
|
||||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||||
|
|
||||||
const expectedResults = [artist1, artist2, artist3, artist4].map(
|
expect(artists).toEqual({
|
||||||
(it) => ({
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there no indexes and no artists", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
ok(
|
||||||
|
subsonicOK({
|
||||||
|
artists: {},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty", async () => {
|
||||||
|
const artists = await login({ username, password })
|
||||||
|
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||||
|
|
||||||
|
expect(artists).toEqual({
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is one index and one artist", () => {
|
||||||
|
const artist1 = anArtist({albums:[anAlbum(), anAlbum(), anAlbum(), anAlbum()]});
|
||||||
|
|
||||||
|
const asArtistsJson = subsonicOK({
|
||||||
|
artists: {
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
name: "#",
|
||||||
|
artist: [
|
||||||
|
{
|
||||||
|
id: artist1.id,
|
||||||
|
name: artist1.name,
|
||||||
|
albumCount: artist1.albums.length,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when it all fits on one page", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(asArtistsJson)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the single artist", async () => {
|
||||||
|
const artists = await login({ username, password })
|
||||||
|
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||||
|
|
||||||
|
const expectedResults = [{
|
||||||
|
id: artist1.id,
|
||||||
|
image: artist1.image,
|
||||||
|
name: artist1.name,
|
||||||
|
sortName: artist1.name
|
||||||
|
}];
|
||||||
|
|
||||||
|
expect(artists).toEqual({
|
||||||
|
results: expectedResults,
|
||||||
|
total: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are artists", () => {
|
||||||
|
const artist1 = anArtist({ name: "A Artist", albums:[anAlbum()] });
|
||||||
|
const artist2 = anArtist({ name: "B Artist" });
|
||||||
|
const artist3 = anArtist({ name: "C Artist" });
|
||||||
|
const artist4 = anArtist({ name: "D Artist" });
|
||||||
|
const artists = [artist1, artist2, artist3, artist4];
|
||||||
|
|
||||||
|
describe("when no paging is in effect", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(asArtistsJson(artists)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return all the artists", async () => {
|
||||||
|
const artists = await login({ username, password })
|
||||||
|
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||||
|
|
||||||
|
const expectedResults = [artist1, artist2, artist3, artist4].map(
|
||||||
|
(it) => ({
|
||||||
|
id: it.id,
|
||||||
|
image: it.image,
|
||||||
|
name: it.name,
|
||||||
|
sortName: it.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(artists).toEqual({
|
||||||
|
results: expectedResults,
|
||||||
|
total: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when paging specified", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(asArtistsJson(artists)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return only the correct page of artists", async () => {
|
||||||
|
const artists = await login({ username, password })
|
||||||
|
.then((it) => it.artists({ _index: 1, _count: 2 }));
|
||||||
|
|
||||||
|
const expectedResults = [artist2, artist3].map((it) => ({
|
||||||
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(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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({
|
expect(artists).toEqual({
|
||||||
results: expectedResults,
|
results: [
|
||||||
|
artistSummaryFromNDArtist(ndArtist1),
|
||||||
|
artistSummaryFromNDArtist(ndArtist2),
|
||||||
|
artistSummaryFromNDArtist(ndArtist3),
|
||||||
|
artistSummaryFromNDArtist(ndArtist4),
|
||||||
|
],
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/api/artist`, {
|
||||||
params: asURLSearchParams(authParamsPlusJson),
|
params: asURLSearchParams({
|
||||||
headers,
|
_sort: "name",
|
||||||
|
_order: "ASC",
|
||||||
|
_start: "0"
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "bonob",
|
||||||
|
"x-nd-authorization": `Bearer ${bearer}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when paging specified", () => {
|
describe("when start index is specified", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
(axios.get as jest.Mock)
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(pingJson({ type: "navidrome" }))))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(asArtistsJson(artists)))
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
it("should return only the correct page of artists", async () => {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/api/artist`, {
|
||||||
const artists = await login({ username, password })
|
params: asURLSearchParams({
|
||||||
.then((it) => it.artists({ _index: 1, _count: 2 }));
|
_sort: "name",
|
||||||
|
_order: "ASC",
|
||||||
const expectedResults = [artist2, artist3].map((it) => ({
|
_start: "2"
|
||||||
id: it.id,
|
}),
|
||||||
image: it.image,
|
headers: {
|
||||||
name: it.name,
|
"User-Agent": "bonob",
|
||||||
}));
|
"x-nd-authorization": `Bearer ${bearer}`,
|
||||||
|
},
|
||||||
expect(artists).toEqual({ results: expectedResults, total: 4 });
|
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
|
||||||
params: asURLSearchParams(authParamsPlusJson),
|
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user