From b99ff0e5dc52ba588074923282b21176ea8a6512 Mon Sep 17 00:00:00 2001 From: Simon J Date: Fri, 3 Sep 2021 21:19:40 +1000 Subject: [PATCH] Fix album scolling so goes past 100 (#44) --- src/navidrome.ts | 107 ++++--- tests/navidrome.test.ts | 668 +++++++++++++++++++++++++++++++++------- 2 files changed, 632 insertions(+), 143 deletions(-) diff --git a/src/navidrome.ts b/src/navidrome.ts index dbf0b42..1d0442f 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -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 => + getArtists = ( + credentials: Credentials + ): Promise<(IdName & { albumCount: number })[]> => this.getJSON(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(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 => navidrome.getArtistWithInfo(credentials, id), - albums: (q: AlbumQuery): Promise> => - navidrome - .getJSON(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> => { + return Promise.all([ + navidrome + .getArtists(credentials) + .then((it) => + _.inject(it, (total, artist) => total + artist.albumCount, 0) + ), + navidrome + .getJSON(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 => 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(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( + credentials, + "/rest/getSimilarSongs", + { id, count: 50 } ) - ), - topSongs: async (artistId: string) => navidrome - .getArtist(credentials, artistId) - .then(({ name }) => navidrome - .getJSON(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(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); diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index 559da64..a7b3cf3 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -26,7 +26,6 @@ import { AuthSuccess, Images, albumToAlbumSummary, - range, asArtistAlbumPairs, Track, AlbumSummary, @@ -381,6 +380,50 @@ const searchResult3 = ({ `; +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) => + ``; + + return ` + + + ${as.map(artistSummaryXml).join("")} + + + ${bs.map(artistSummaryXml).join("")} + + + ${cs.map(artistSummaryXml).join("")} + + + ${rest.map(artistSummaryXml).join("")} + + + `; +}; + const EMPTY = ``; const PING_OK = ``; @@ -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 = ` - - - - - - - - - - - - - - - - `; + 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, + }); + }); + }); + }); + }); });