mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Fix album scolling so goes past 100 (#44)
This commit is contained in:
107
src/navidrome.ts
107
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<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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user