Fix album scolling so goes past 100 (#44)

This commit is contained in:
Simon J
2021-09-03 21:19:40 +10:00
committed by GitHub
parent 9092050c37
commit b99ff0e5dc
2 changed files with 632 additions and 143 deletions

View File

@@ -197,12 +197,12 @@ export type GetPlaylistsResponse = {
};
export type GetSimilarSongsResponse = {
similarSongs: { song: song[] }
}
similarSongs: { song: song[] };
};
export type GetTopSongsResponse = {
topSongs: { song: song[] }
}
topSongs: { song: song[] };
};
export type GetSongResponse = {
song: song;
@@ -236,7 +236,7 @@ export type getAlbumListParams = {
genre?: string;
};
const MAX_ALBUM_LIST = 500;
export const MAX_ALBUM_LIST = 500;
const asTrack = (album: Album, song: song) => ({
id: song._id,
@@ -248,7 +248,7 @@ const asTrack = (album: Album, song: song) => ({
album,
artist: {
id: song._artistId,
name: song._artist
name: song._artist,
},
});
@@ -389,13 +389,16 @@ export class Navidrome implements MusicService {
)
);
getArtists = (credentials: Credentials): Promise<IdName[]> =>
getArtists = (
credentials: Credentials
): Promise<(IdName & { albumCount: number })[]> =>
this.getJSON<GetArtistsResponse>(credentials, "/rest/getArtists")
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) =>
artists.map((artist) => ({
id: artist._id,
name: artist._name,
albumCount: Number.parseInt(artist._albumCount),
}))
);
@@ -403,7 +406,7 @@ export class Navidrome implements MusicService {
this.getJSON<GetArtistInfoResponse>(credentials, "/rest/getArtistInfo", {
id,
count: 50,
includeNotPresent: true
includeNotPresent: true,
}).then((it) => ({
image: {
small: validate(it.artistInfo.smallImageUrl),
@@ -516,20 +519,29 @@ export class Navidrome implements MusicService {
})),
artist: async (id: string): Promise<Artist> =>
navidrome.getArtistWithInfo(credentials, id),
albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList", {
...pick(q, "type", "genre"),
size: Math.min(MAX_ALBUM_LIST, q._count),
offset: q._index,
})
.then((response) => response.albumList.album || [])
.then(navidrome.toAlbumSummary)
.then(slice2(q))
.then(([page, total]) => ({
results: page,
total: Math.min(MAX_ALBUM_LIST, total),
})),
albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> => {
return Promise.all([
navidrome
.getArtists(credentials)
.then((it) =>
_.inject(it, (total, artist) => total + artist.albumCount, 0)
),
navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList", {
...pick(q, "type", "genre"),
size: 500,
offset: q._index,
})
.then((response) => response.albumList.album || [])
.then(navidrome.toAlbumSummary),
]).then(([total, albums]) => ({
results: albums.slice(0, q._count),
total:
albums.length == 500
? total
: q._index + albums.length,
}));
},
album: (id: string): Promise<Album> =>
navidrome.getAlbum(credentials, id),
genres: () =>
@@ -538,7 +550,7 @@ export class Navidrome implements MusicService {
.then((it) =>
pipe(
it.genres.genre || [],
A.filter(it => Number.parseInt(it._albumCount) > 0),
A.filter((it) => Number.parseInt(it._albumCount) > 0),
A.map((it) => it.__text),
A.sort(ordString),
A.map((it) => ({ id: it, name: it }))
@@ -712,7 +724,7 @@ export class Navidrome implements MusicService {
},
artist: {
id: entry._artistId,
name: entry._artist
name: entry._artist,
},
})),
};
@@ -744,24 +756,41 @@ export class Navidrome implements MusicService {
songIndexToRemove: indicies,
})
.then((_) => true),
similarSongs: async (id: string) => navidrome
.getJSON<GetSimilarSongsResponse>(credentials, "/rest/getSimilarSongs", { id, count: 50 })
.then((it) => (it.similarSongs.song || []))
.then(songs =>
Promise.all(
songs.map((song) => navidrome.getAlbum(credentials, song._albumId).then(album => asTrack(album, song)))
similarSongs: async (id: string) =>
navidrome
.getJSON<GetSimilarSongsResponse>(
credentials,
"/rest/getSimilarSongs",
{ id, count: 50 }
)
),
topSongs: async (artistId: string) => navidrome
.getArtist(credentials, artistId)
.then(({ name }) => navidrome
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", { artist: name, count: 50 })
.then((it) => (it.topSongs.song || []))
.then(songs =>
.then((it) => it.similarSongs.song || [])
.then((songs) =>
Promise.all(
songs.map((song) => navidrome.getAlbum(credentials, song._albumId).then(album => asTrack(album, song)))
songs.map((song) =>
navidrome
.getAlbum(credentials, song._albumId)
.then((album) => asTrack(album, song))
)
)
))
),
topSongs: async (artistId: string) =>
navidrome.getArtist(credentials, artistId).then(({ name }) =>
navidrome
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
artist: name,
count: 50,
})
.then((it) => it.topSongs.song || [])
.then((songs) =>
Promise.all(
songs.map((song) =>
navidrome
.getAlbum(credentials, song._albumId)
.then((album) => asTrack(album, song))
)
)
)
),
};
return Promise.resolve(musicLibrary);

View File

@@ -26,7 +26,6 @@ import {
AuthSuccess,
Images,
albumToAlbumSummary,
range,
asArtistAlbumPairs,
Track,
AlbumSummary,
@@ -381,6 +380,50 @@ const searchResult3 = ({
</searchResult3>
</subsonic-response>`;
const getArtistsXml = (artists: Artist[]) => {
const as: Artist[] = [];
const bs: Artist[] = [];
const cs: Artist[] = [];
const rest: Artist[] = [];
artists.forEach((it) => {
const firstChar = it.name.toLowerCase()[0];
switch (firstChar) {
case "a":
as.push(it);
break;
case "b":
bs.push(it);
break;
case "c":
cs.push(it);
break;
default:
rest.push(it);
break;
}
});
const artistSummaryXml = (artist: Artist) =>
`<artist id="${artist.id}" name="${artist.name}" albumCount="${artist.albums.length}"></artist>`;
return `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<artists lastModified="1614586749000" ignoredArticles="The El La Los Las Le Les Os As O A">
<index name="A">
${as.map(artistSummaryXml).join("")}
</index>
<index name="B">
${bs.map(artistSummaryXml).join("")}
</index>
<index name="C">
${cs.map(artistSummaryXml).join("")}
</index>
<index name="D-Z">
${rest.map(artistSummaryXml).join("")}
</index>
</artists>
</subsonic-response>`;
};
const EMPTY = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
@@ -1090,34 +1133,19 @@ describe("Navidrome", () => {
});
describe("when there are artists", () => {
const artist1 = anArtist();
const artist2 = anArtist();
const artist3 = anArtist();
const artist4 = anArtist();
const getArtistsXml = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<artists lastModified="1614586749000" ignoredArticles="The El La Los Las Le Les Os As O A">
<index name="#">
<artist id="${artist1.id}" name="${artist1.name}" albumCount="22"></artist>
<artist id="${artist2.id}" name="${artist2.name}" albumCount="9"></artist>
</index>
<index name="A">
<artist id="${artist3.id}" name="${artist3.name}" albumCount="2"></artist>
</index>
<index name="B">
<artist id="${artist4.id}" name="${artist4.name}" albumCount="2"></artist>
</index>
<index name="C">
<!-- intentionally no artists -->
</index>
</artists>
</subsonic-response>`;
const artist1 = anArtist({ name: "A Artist" });
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(getArtistsXml)));
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
);
});
it("should return all the artists", async () => {
@@ -1150,7 +1178,9 @@ describe("Navidrome", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() => Promise.resolve(ok(getArtistsXml)));
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
);
});
it("should return only the correct page of artists", async () => {
@@ -1188,11 +1218,15 @@ describe("Navidrome", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist, album1],
// album2 is not Pop
[artist, album3],
])
)
@@ -1203,7 +1237,7 @@ describe("Navidrome", () => {
it("should pass the filter to navidrome", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 500,
_count: 100,
genre: "Pop",
type: "byGenre",
};
@@ -1218,6 +1252,11 @@ describe("Navidrome", () => {
total: 2,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1235,12 +1274,16 @@ describe("Navidrome", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist, album3],
[artist, album2],
[artist, album1],
])
)
)
@@ -1248,7 +1291,7 @@ describe("Navidrome", () => {
});
it("should pass the filter to navidrome", async () => {
const q: AlbumQuery = { _index: 0, _count: 500, type: "newest" };
const q: AlbumQuery = { _index: 0, _count: 100, type: "newest" };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
@@ -1256,8 +1299,13 @@ describe("Navidrome", () => {
.then((it) => it.albums(q));
expect(result).toEqual({
results: [album3, album2].map(albumToAlbumSummary),
total: 2,
results: [album3, album2, album1].map(albumToAlbumSummary),
total: 3,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
@@ -1276,12 +1324,16 @@ describe("Navidrome", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist, album3],
[artist, album2],
// album1 never played
])
)
)
@@ -1289,7 +1341,7 @@ describe("Navidrome", () => {
});
it("should pass the filter to navidrome", async () => {
const q: AlbumQuery = { _index: 0, _count: 500, type: "recent" };
const q: AlbumQuery = { _index: 0, _count: 100, type: "recent" };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
@@ -1301,6 +1353,11 @@ describe("Navidrome", () => {
total: 2,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1318,12 +1375,18 @@ describe("Navidrome", () => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml([[artist, album2]])))
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(
() =>
// album1 never played
Promise.resolve(ok(albumListXml([[artist, album2]])))
// album3 never played
);
});
it("should pass the filter to navidrome", async () => {
const q: AlbumQuery = { _index: 0, _count: 500, type: "frequent" };
const q: AlbumQuery = { _index: 0, _count: 100, type: "frequent" };
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
@@ -1335,6 +1398,11 @@ describe("Navidrome", () => {
total: 1,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1349,16 +1417,19 @@ describe("Navidrome", () => {
});
describe("when the artist has only 1 album", () => {
const artist1 = anArtist({
const artist = anArtist({
name: "one hit wonder",
albums: [anAlbum({ genre: asGenre("Pop") })],
});
const artists = [artist1];
const artists = [artist];
const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
);
@@ -1367,7 +1438,7 @@ describe("Navidrome", () => {
it("should return the album", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 500,
_count: 100,
type: "alphabeticalByArtist",
};
const result = await navidrome
@@ -1381,6 +1452,11 @@ describe("Navidrome", () => {
total: 1,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1393,17 +1469,20 @@ describe("Navidrome", () => {
});
});
describe("when the artist has only no albums", () => {
const artist1 = anArtist({
name: "one hit wonder",
describe("when the only artist has no albums", () => {
const artist = anArtist({
name: "no hit wonder",
albums: [],
});
const artists = [artist1];
const artists = [artist];
const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
);
@@ -1412,7 +1491,7 @@ describe("Navidrome", () => {
it("should return the album", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 500,
_count: 100,
type: "alphabeticalByArtist",
};
const result = await navidrome
@@ -1426,6 +1505,11 @@ describe("Navidrome", () => {
total: 0,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1438,7 +1522,7 @@ describe("Navidrome", () => {
});
});
describe("when there are less than 500 albums", () => {
describe("when there are 6 albums in total", () => {
const genre1 = asGenre("genre1");
const genre2 = asGenre("genre2");
const genre3 = asGenre("genre3");
@@ -1446,35 +1530,36 @@ describe("Navidrome", () => {
const artist1 = anArtist({
name: "abba",
albums: [
anAlbum({ genre: genre1 }),
anAlbum({ genre: genre2 }),
anAlbum({ genre: genre3 }),
anAlbum({ name: "album1", genre: genre1 }),
anAlbum({ name: "album2", genre: genre2 }),
anAlbum({ name: "album3", genre: genre3 }),
],
});
const artist2 = anArtist({
name: "babba",
albums: [
anAlbum({ genre: genre1 }),
anAlbum({ genre: genre2 }),
anAlbum({ genre: genre3 }),
anAlbum({ name: "album4", genre: genre1 }),
anAlbum({ name: "album5", genre: genre2 }),
anAlbum({ name: "album6", genre: genre3 }),
],
});
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 () => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
);
const q: AlbumQuery = {
_index: 0,
_count: 500,
_count: 100,
type: "alphabeticalByArtist",
};
const result = await navidrome
@@ -1488,6 +1573,11 @@ describe("Navidrome", () => {
total: 6,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
@@ -1502,6 +1592,25 @@ describe("Navidrome", () => {
describe("querying for a page of them", () => {
it("should return the page with the corrent paging information", async () => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, artist1.albums[2]!],
[artist2, artist2.albums[0]!],
// due to pre-fetch will get next 2 albums also
[artist2, artist2.albums[1]!],
[artist2, artist2.albums[2]!],
])
)
)
);
const q: AlbumQuery = {
_index: 2,
_count: 2,
@@ -1514,15 +1623,20 @@ describe("Navidrome", () => {
.then((it) => it.albums(q));
expect(result).toEqual({
results: [albums[2], albums[3]],
results: [artist1.albums[2], artist2.albums[0]],
total: 6,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 2,
size: 500,
offset: 2,
}),
headers,
@@ -1531,60 +1645,406 @@ describe("Navidrome", () => {
});
});
describe("when there are more than 500 albums", () => {
const first500Albums = range(500).map((i) =>
anAlbum({ name: `album ${i}`, genre: asGenre(`genre ${i}`) })
);
const artist = anArtist({
name: "> 500 albums",
albums: [...first500Albums, anAlbum(), anAlbum(), anAlbum()],
});
describe("when the number of albums reported by getArtists does not match that of getAlbums", () => {
const genre = asGenre("lofi");
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml(
first500Albums.map(
(album) => [artist, album] as [Artist, Album]
const album1 = anAlbum({ name: "album1", genre });
const album2 = anAlbum({ name: "album2", genre });
const album3 = anAlbum({ name: "album3", genre });
const album4 = anAlbum({ name: "album4", genre });
const album5 = anAlbum({ name: "album5", genre });
// the artists have 5 albums in the getArtists endpoint
const artist1 = anArtist({
albums: [
album1,
album2,
album3,
album4,
],
});
const artist2 = anArtist({
albums: [
album5,
],
});
const artists = [artist1, artist2];
describe("when the number of albums returned from getAlbums is less the number of albums in the getArtists endpoint", () => {
describe("when the query comes back on 1 page", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, album1],
[artist1, album2],
[artist1, album3],
// album4 is missing from the albums end point for some reason
[artist2, album5],
])
)
)
)
)
);
});
describe("querying for all of them", () => {
it("will return only the first 500 with the correct paging information", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 1000,
type: "alphabeticalByArtist",
};
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: first500Albums.map(albumToAlbumSummary),
total: 500,
);
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
it("should return the page of albums, updating the total to be accurate", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
size: 500,
offset: 0,
}),
headers,
};
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,
album2,
album3,
album5,
],
total: 4,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
describe("when the query is for the first page", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, album1],
[artist1, album2],
// album3 & album5 is returned due to the prefetch
[artist1, album3],
// album4 is missing from the albums end point for some reason
[artist2, album5],
])
)
)
);
});
it("should filter out the pre-fetched albums", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 2,
type: "alphabeticalByArtist",
};
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,
album2,
],
total: 4,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
describe("when the query is for the last page only", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
// album1 is on the first page
// album2 is on the first page
[artist1, album3],
// album4 is missing from the albums end point for some reason
[artist2, album5],
])
)
)
);
});
it("should return the last page of albums, updating the total to be accurate", async () => {
const q: AlbumQuery = {
_index: 2,
_count: 100,
type: "alphabeticalByArtist",
};
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: [
album3,
album5,
],
total: 4,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
});
describe("when the number of albums returned from getAlbums is more than the number of albums in the getArtists endpoint", () => {
describe("when the query comes back on 1 page", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([
// artist1 has lost 2 albums on the getArtists end point
{ ...artist1, albums: [album1, album2] },
artist2,
])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, album1],
[artist1, album2],
[artist1, album3],
[artist1, album4],
[artist2, album5],
])
)
)
);
});
it("should return the page of albums, updating the total to be accurate", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
};
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,
album2,
album3,
album4,
album5,
],
total: 5,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
describe("when the query is for the first page", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([
// artist1 has lost 2 albums on the getArtists end point
{ ...artist1, albums: [album1, album2] },
artist2,
])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, album1],
[artist1, album2],
[artist1, album3],
[artist1, album4],
[artist2, album5],
])
)
)
);
});
it("should filter out the pre-fetched albums", async () => {
const q: AlbumQuery = {
_index: 0,
_count: 2,
type: "alphabeticalByArtist",
};
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,
album2,
],
total: 5,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
describe("when the query is for the last page only", () => {
beforeEach(() => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([
// artist1 has lost 2 albums on the getArtists end point
{ ...artist1, albums: [album1, album2] },
artist2,
])))
)
.mockImplementationOnce(() =>
Promise.resolve(
ok(
albumListXml([
[artist1, album3],
[artist1, album4],
[artist2, album5],
])
)
)
);
});
it("should return the last page of albums, updating the total to be accurate", async () => {
const q: AlbumQuery = {
_index: 2,
_count: 100,
type: "alphabeticalByArtist",
};
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: [
album3,
album4,
album5,
],
total: 5,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({
...authParams,
type: "alphabeticalByArtist",
size: 500,
offset: q._index,
}),
headers,
});
});
});
});
});
});