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

View File

@@ -26,7 +26,6 @@ import {
AuthSuccess, AuthSuccess,
Images, Images,
albumToAlbumSummary, albumToAlbumSummary,
range,
asArtistAlbumPairs, asArtistAlbumPairs,
Track, Track,
AlbumSummary, AlbumSummary,
@@ -381,6 +380,50 @@ const searchResult3 = ({
</searchResult3> </searchResult3>
</subsonic-response>`; </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 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>`; 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", () => { describe("when there are artists", () => {
const artist1 = anArtist(); const artist1 = anArtist({ name: "A Artist" });
const artist2 = anArtist(); const artist2 = anArtist({ name: "B Artist" });
const artist3 = anArtist(); const artist3 = anArtist({ name: "C Artist" });
const artist4 = anArtist(); const artist4 = anArtist({ name: "D Artist" });
const artists = [artist1, artist2, artist3, artist4];
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>`;
describe("when no paging is in effect", () => { describe("when no paging is in effect", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() => Promise.resolve(ok(getArtistsXml))); .mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
);
}); });
it("should return all the artists", async () => { it("should return all the artists", async () => {
@@ -1150,7 +1178,9 @@ describe("Navidrome", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .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 () => { it("should return only the correct page of artists", async () => {
@@ -1188,11 +1218,15 @@ describe("Navidrome", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve( Promise.resolve(
ok( ok(
albumListXml([ albumListXml([
[artist, album1], [artist, album1],
// album2 is not Pop
[artist, album3], [artist, album3],
]) ])
) )
@@ -1203,7 +1237,7 @@ describe("Navidrome", () => {
it("should pass the filter to navidrome", async () => { it("should pass the filter to navidrome", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 500, _count: 100,
genre: "Pop", genre: "Pop",
type: "byGenre", type: "byGenre",
}; };
@@ -1218,6 +1252,11 @@ describe("Navidrome", () => {
total: 2, total: 2,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
@@ -1235,12 +1274,16 @@ describe("Navidrome", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve( Promise.resolve(
ok( ok(
albumListXml([ albumListXml([
[artist, album3], [artist, album3],
[artist, album2], [artist, album2],
[artist, album1],
]) ])
) )
) )
@@ -1248,7 +1291,7 @@ describe("Navidrome", () => {
}); });
it("should pass the filter to navidrome", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1256,8 +1299,13 @@ describe("Navidrome", () => {
.then((it) => it.albums(q)); .then((it) => it.albums(q));
expect(result).toEqual({ expect(result).toEqual({
results: [album3, album2].map(albumToAlbumSummary), results: [album3, album2, album1].map(albumToAlbumSummary),
total: 2, total: 3,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
@@ -1276,12 +1324,16 @@ describe("Navidrome", () => {
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve( Promise.resolve(
ok( ok(
albumListXml([ albumListXml([
[artist, album3], [artist, album3],
[artist, album2], [artist, album2],
// album1 never played
]) ])
) )
) )
@@ -1289,7 +1341,7 @@ describe("Navidrome", () => {
}); });
it("should pass the filter to navidrome", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1301,6 +1353,11 @@ describe("Navidrome", () => {
total: 2, total: 2,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
@@ -1318,12 +1375,18 @@ describe("Navidrome", () => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml([artist])))
)
.mockImplementationOnce(
() =>
// album1 never played
Promise.resolve(ok(albumListXml([[artist, album2]]))) Promise.resolve(ok(albumListXml([[artist, album2]])))
// album3 never played
); );
}); });
it("should pass the filter to navidrome", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1335,6 +1398,11 @@ describe("Navidrome", () => {
total: 1, total: 1,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
@@ -1349,16 +1417,19 @@ describe("Navidrome", () => {
}); });
describe("when the artist has only 1 album", () => { describe("when the artist has only 1 album", () => {
const artist1 = anArtist({ const artist = anArtist({
name: "one hit wonder", name: "one hit wonder",
albums: [anAlbum({ genre: asGenre("Pop") })], albums: [anAlbum({ genre: asGenre("Pop") })],
}); });
const artists = [artist1]; const artists = [artist];
const albums = artists.flatMap((artist) => artist.albums); const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists)))) Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
); );
@@ -1367,7 +1438,7 @@ describe("Navidrome", () => {
it("should return the album", async () => { it("should return the album", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 500, _count: 100,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
}; };
const result = await navidrome const result = await navidrome
@@ -1381,6 +1452,11 @@ describe("Navidrome", () => {
total: 1, total: 1,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
@@ -1393,17 +1469,20 @@ describe("Navidrome", () => {
}); });
}); });
describe("when the artist has only no albums", () => { describe("when the only artist has no albums", () => {
const artist1 = anArtist({ const artist = anArtist({
name: "one hit wonder", name: "no hit wonder",
albums: [], albums: [],
}); });
const artists = [artist1]; const artists = [artist];
const albums = artists.flatMap((artist) => artist.albums); const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists)))) Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
); );
@@ -1412,7 +1491,7 @@ describe("Navidrome", () => {
it("should return the album", async () => { it("should return the album", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 500, _count: 100,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
}; };
const result = await navidrome const result = await navidrome
@@ -1426,6 +1505,11 @@ describe("Navidrome", () => {
total: 0, total: 0,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...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 genre1 = asGenre("genre1");
const genre2 = asGenre("genre2"); const genre2 = asGenre("genre2");
const genre3 = asGenre("genre3"); const genre3 = asGenre("genre3");
@@ -1446,35 +1530,36 @@ describe("Navidrome", () => {
const artist1 = anArtist({ const artist1 = anArtist({
name: "abba", name: "abba",
albums: [ albums: [
anAlbum({ genre: genre1 }), anAlbum({ name: "album1", genre: genre1 }),
anAlbum({ genre: genre2 }), anAlbum({ name: "album2", genre: genre2 }),
anAlbum({ genre: genre3 }), anAlbum({ name: "album3", genre: genre3 }),
], ],
}); });
const artist2 = anArtist({ const artist2 = anArtist({
name: "babba", name: "babba",
albums: [ albums: [
anAlbum({ genre: genre1 }), anAlbum({ name: "album4", genre: genre1 }),
anAlbum({ genre: genre2 }), anAlbum({ name: "album5", genre: genre2 }),
anAlbum({ genre: genre3 }), anAlbum({ name: "album6", genre: genre3 }),
], ],
}); });
const artists = [artist1, artist2]; const artists = [artist1, artist2];
const albums = artists.flatMap((artist) => artist.albums); const albums = artists.flatMap((artist) => artist.albums);
beforeEach(() => { describe("querying for all of them", () => {
it("should return all of them with corrent paging information", async () => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists)))) Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists))))
); );
});
describe("querying for all of them", () => {
it("should return all of them with corrent paging information", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 500, _count: 100,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
}; };
const result = await navidrome const result = await navidrome
@@ -1488,6 +1573,11 @@ describe("Navidrome", () => {
total: 6, total: 6,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
@@ -1502,6 +1592,25 @@ describe("Navidrome", () => {
describe("querying for a page of them", () => { describe("querying for a page of them", () => {
it("should return the page with the corrent paging information", async () => { 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 = { const q: AlbumQuery = {
_index: 2, _index: 2,
_count: 2, _count: 2,
@@ -1514,15 +1623,20 @@ describe("Navidrome", () => {
.then((it) => it.albums(q)); .then((it) => it.albums(q));
expect(result).toEqual({ expect(result).toEqual({
results: [albums[2], albums[3]], results: [artist1.albums[2], artist2.albums[0]],
total: 6, total: 6,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
params: asURLSearchParams({ params: asURLSearchParams({
...authParams, ...authParams,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
size: 2, size: 500,
offset: 2, offset: 2,
}), }),
headers, headers,
@@ -1531,36 +1645,58 @@ describe("Navidrome", () => {
}); });
}); });
describe("when there are more than 500 albums", () => { describe("when the number of albums reported by getArtists does not match that of getAlbums", () => {
const first500Albums = range(500).map((i) => const genre = asGenre("lofi");
anAlbum({ name: `album ${i}`, genre: asGenre(`genre ${i}`) })
);
const artist = anArtist({
name: "> 500 albums",
albums: [...first500Albums, anAlbum(), anAlbum(), anAlbum()],
});
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(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getArtistsXml(artists)))
)
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve( Promise.resolve(
ok( ok(
albumListXml( albumListXml([
first500Albums.map( [artist1, album1],
(album) => [artist, album] as [Artist, Album] [artist1, album2],
) [artist1, album3],
) // album4 is missing from the albums end point for some reason
[artist2, album5],
])
) )
) )
); );
}); });
describe("querying for all of them", () => { it("should return the page of albums, updating the total to be accurate", async () => {
it("will return only the first 500 with the correct paging information", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 1000, _count: 100,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
}; };
const result = await navidrome const result = await navidrome
@@ -1570,8 +1706,18 @@ describe("Navidrome", () => {
.then((it) => it.albums(q)); .then((it) => it.albums(q));
expect(result).toEqual({ expect(result).toEqual({
results: first500Albums.map(albumToAlbumSummary), results: [
total: 500, album1,
album2,
album3,
album5,
],
total: 4,
});
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
params: asURLSearchParams(authParams),
headers,
}); });
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
@@ -1579,12 +1725,326 @@ describe("Navidrome", () => {
...authParams, ...authParams,
type: "alphabeticalByArtist", type: "alphabeticalByArtist",
size: 500, size: 500,
offset: 0, offset: q._index,
}), }),
headers, 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,
});
});
});
});
}); });
}); });