diff --git a/src/navidrome.ts b/src/navidrome.ts index 6296b18..4cb888c 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -232,7 +232,17 @@ export class Navidrome implements MusicService { q: {} = {} ): Promise => this.get({ username, password }, path, q) - .then((response) => new X2JS().xml2js(response.data) as SubconicEnvelope) + .then( + (response) => + new X2JS({ + arrayAccessFormPaths: [ + "subsonic-response.artist.album", + "subsonic-response.albumList.album", + "subsonic-response.album.song", + "subsonic-response.genres.genre" + ], + }).xml2js(response.data) as SubconicEnvelope + ) .then((json) => json["subsonic-response"]) .then((json) => { if (isError(json)) throw json.error._message; @@ -296,16 +306,19 @@ export class Navidrome implements MusicService { id, }) .then((it) => it.artist) - .then((it) => ({ - id: it._id, - name: it._name, - albums: it.album.map((album) => ({ - id: album._id, - name: album._name, - year: album._year, - genre: album._genre, - })), - })); + .then((it) => { + console.log(`artist is ${JSON.stringify(it)}`); + return { + id: it._id, + name: it._name, + albums: it.album.map((album) => ({ + id: album._id, + name: album._name, + year: album._year, + genre: album._genre, + })), + }; + }); async login(token: string) { const navidrome = this; diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index 5510810..6700d4b 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -232,50 +232,79 @@ describe("Navidrome", () => { }); describe("getting genres", () => { - const genres = ["HipHop", "Rap", "TripHop", "Pop", "Rock"]; - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(genresXml(genres)))); + describe("when there is only 1", () => { + const genres = ["HipHop"]; + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => Promise.resolve(ok(genresXml(genres)))); + }); + + it("should return them alphabetically sorted", async () => { + const result = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.genres()); + + expect(result).toEqual(genres.sort()); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { + params: { + ...authParams, + }, + headers, + }); + }); }); - it("should return them alphabetically sorted", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.authToken)) - .then((it) => it.genres()); - - expect(result).toEqual(genres.sort()); - - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { - params: { - ...authParams, - }, - headers, + describe("when there are many", () => { + const genres = ["HipHop", "Rap", "TripHop", "Pop", "Rock"]; + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => Promise.resolve(ok(genresXml(genres)))); + }); + + it("should return them alphabetically sorted", async () => { + const result = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.genres()); + + expect(result).toEqual(genres.sort()); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { + params: { + ...authParams, + }, + headers, + }); }); }); }); describe("getting an artist", () => { - const album1: Album = anAlbum(); - const album2: Album = anAlbum(); + describe("when the artist exists and has multiple albums", () => { + const album1: Album = anAlbum(); - const artist: Artist = anArtist({ - albums: [album1, album2], - }); - - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) - .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(artist.image))) - ); - }); - - describe("when the artist exists", () => { + const album2: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album1, album2], + }); + + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist.image))) + ); + }); + it("should return it", async () => { const result: Artist = await navidrome .generateToken({ username, password }) @@ -307,6 +336,55 @@ describe("Navidrome", () => { }); }); }); + + describe("when the artist exists and has only 1 album", () => { + const album: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album], + }); + + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist.image))) + ); + }); + + it("should return it", async () => { + const result: Artist = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ + id: artist.id, + name: artist.name, + image: artist.image, + albums: artist.albums, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + }); + }); + }); describe("getting artists", () => { @@ -362,6 +440,9 @@ describe("Navidrome", () => { + + + `; @@ -541,6 +622,49 @@ describe("Navidrome", () => { }); }); + describe("when the artist has only 1 album", () => { + const artist1 = anArtist({ + name: "one hit wonder", + albums: [anAlbum()], + }); + const artists = [artist1]; + const albums = artists.flatMap((artist) => artist.albums); + + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(albumListXml(asArtistAlbumPairs(artists)))) + ); + }); + + it("should return the album", async () => { + const paging = { _index: 0, _count: 500 }; + const result = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.albums(paging)); + + expect(result).toEqual({ + results: albums, + total: 1, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { + params: { + type: "alphabeticalByArtist", + size: 500, + offset: 0, + ...authParams, + }, + headers, + }); + }); + + }); + + describe("when there are less than 500 albums", () => { const artist1 = anArtist({ name: "abba", @@ -710,7 +834,7 @@ describe("Navidrome", () => { describe("getting tracks", () => { describe("for an album", () => { - describe("when it exists", () => { + describe("when the album has multiple tracks", () => { const album = anAlbum({ id: "album1", name: "Burnin" }); const albumSummary = albumToAlbumSummary(album); @@ -757,6 +881,51 @@ describe("Navidrome", () => { }); }); }); + + describe("when the album has only 1 track", () => { + const album = anAlbum({ id: "album1", name: "Burnin" }); + const albumSummary = albumToAlbumSummary(album); + + const artist = anArtist({ + id: "artist1", + name: "Bob Marley", + albums: [album], + }); + const artistSummary = { + ...artistToArtistSummary(artist), + image: NO_IMAGES, + }; + + const tracks = [ + aTrack({ artist: artistSummary, album: albumSummary }), + ]; + + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(getAlbumXml(artist, album, tracks))) + ); + }); + + it("should return the album", async () => { + const result = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.tracks(album.id)); + + expect(result).toEqual(tracks); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, { + params: { + id: album.id, + ...authParams, + }, + headers, + }); + }); + }); }); describe("a single track", () => {