From fa1ad8c18bc2b5842755a465687c387c6045f343 Mon Sep 17 00:00:00 2001 From: simojenki Date: Mon, 5 Apr 2021 13:25:48 +1000 Subject: [PATCH] Ability to query for recently added and recently played albums --- src/music_service.ts | 2 +- src/smapi.ts | 85 ++++++++-------- tests/navidrome.test.ts | 209 ++++++++++++++++++++++++++++++++++++---- tests/smapi.test.ts | 4 +- 4 files changed, 240 insertions(+), 60 deletions(-) diff --git a/src/music_service.ts b/src/music_service.ts index fc666f0..f0c4100 100644 --- a/src/music_service.ts +++ b/src/music_service.ts @@ -94,7 +94,7 @@ export const asResult = ([results, total]: [T[], number]) => ({ export type ArtistQuery = Paging; -export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent'; +export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest'; export type AlbumQuery = Paging & { type: AlbumQueryType; diff --git a/src/smapi.ts b/src/smapi.ts index 68cb851..5fe1d2f 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -8,6 +8,7 @@ import logger from "./logger"; import { LinkCodes } from "./link_codes"; import { Album, + AlbumQuery, AlbumSummary, ArtistSummary, Genre, @@ -442,6 +443,19 @@ function bindSmapiSoapServiceToExpress( const [type, typeId] = id.split(":"); const paging = { _index: index, _count: count }; logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`); + + const albums = (q: AlbumQuery): Promise => + musicLibrary.albums(q).then((result) => { + const accessToken = accessTokens.mint(authToken); + return getMetadataResult({ + mediaCollection: result.results.map((it) => + album(webAddress, accessToken, it) + ), + index: paging._index, + total: result.total, + }); + }); + switch (type) { case "root": return getMetadataResult({ @@ -450,9 +464,14 @@ function bindSmapiSoapServiceToExpress( container({ id: "albums", title: "Albums" }), container({ id: "genres", title: "Genres" }), container({ id: "randomAlbums", title: "Random" }), + container({ id: "recentlyAdded", title: "Recently Added" }), + container({ + id: "recentlyPlayed", + title: "Recently Played", + }), ], index: 0, - total: 4, + total: 6, }); case "artists": return await musicLibrary.artists(paging).then((result) => { @@ -465,32 +484,33 @@ function bindSmapiSoapServiceToExpress( total: result.total, }); }); - case "albums": - return await musicLibrary - .albums({ type: "alphabeticalByArtist", ...paging }) - .then((result) => { - const accessToken = accessTokens.mint(authToken); - return getMetadataResult({ - mediaCollection: result.results.map((it) => - album(webAddress, accessToken, it) - ), - index: paging._index, - total: result.total, - }); - }); + case "albums": { + return await albums({ + type: "alphabeticalByArtist", + ...paging, + }); + } case "randomAlbums": - return await musicLibrary - .albums({ type: "random", ...paging }) - .then((result) => { - const accessToken = accessTokens.mint(authToken); - return getMetadataResult({ - mediaCollection: result.results.map((it) => - album(webAddress, accessToken, it) - ), - index: paging._index, - total: result.total, - }); - }); + return await albums({ + type: "random", + ...paging, + }); + case "genre": + return await albums({ + type: "byGenre", + genre: typeId, + ...paging, + }); + case "recentlyAdded": + return await albums({ + type: "newest", + ...paging, + }); + case "recentlyPlayed": + return await albums({ + type: "frequent", + ...paging, + }); case "genres": return await musicLibrary .genres() @@ -546,19 +566,6 @@ function bindSmapiSoapServiceToExpress( total, }); }); - case "genre": - return await musicLibrary - .albums({ type: "byGenre", genre: typeId, ...paging }) - .then((result) => { - const accessToken = accessTokens.mint(authToken); - return getMetadataResult({ - mediaCollection: result.results.map((it) => - album(webAddress, accessToken, it) - ), - index: paging._index, - total: result.total, - }); - }); default: throw `Unsupported id:${id}`; } diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index bc3a31a..777d115 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -7,7 +7,7 @@ import { t, BROWSER_HEADERS, DODGY_IMAGE_NAME, - asGenre + asGenre, } from "../src/navidrome"; import encryption from "../src/encryption"; @@ -280,7 +280,7 @@ describe("Navidrome", () => { }); describe("when there are many", () => { - const genres = ["g1", "g2", "g3", "g3"] + const genres = ["g1", "g2", "g3", "g3"]; beforeEach(() => { mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -868,7 +868,12 @@ describe("Navidrome", () => { }); it("should pass the filter to navidrome", async () => { - const q: AlbumQuery = { _index: 0, _count: 500, genre: "Pop", type: 'byGenre' }; + const q: AlbumQuery = { + _index: 0, + _count: 500, + genre: "Pop", + type: "byGenre", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -892,6 +897,122 @@ describe("Navidrome", () => { }); }); }); + + describe("by newest", () => { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve( + ok( + albumListXml([ + [artist, album3], + [artist, album2], + ]) + ) + ) + ); + }); + + it("should pass the filter to navidrome", async () => { + const q: AlbumQuery = { _index: 0, _count: 500, type: "newest" }; + 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, album2].map(albumToAlbumSummary), + total: 2, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { + params: { + type: "newest", + size: 500, + offset: 0, + ...authParams, + }, + headers, + }); + }); + }); + + describe("by recently played", () => { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve( + ok( + albumListXml([ + [artist, album3], + [artist, album2], + ]) + ) + ) + ); + }); + + it("should pass the filter to navidrome", async () => { + const q: AlbumQuery = { _index: 0, _count: 500, type: "recent" }; + 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, album2].map(albumToAlbumSummary), + total: 2, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { + params: { + type: "recent", + size: 500, + offset: 0, + ...authParams, + }, + headers, + }); + }); + }); + + describe("by frequently played", () => { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(albumListXml([[artist, album2]]))) + ); + }); + + it("should pass the filter to navidrome", async () => { + const q: AlbumQuery = { _index: 0, _count: 500, type: "frequent" }; + 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: [album2].map(albumToAlbumSummary), + total: 1, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { + params: { + type: "frequent", + size: 500, + offset: 0, + ...authParams, + }, + headers, + }); + }); + }); }); describe("when the artist has only 1 album", () => { @@ -911,7 +1032,11 @@ describe("Navidrome", () => { }); it("should return the album", async () => { - const q: AlbumQuery = { _index: 0, _count: 500, type: 'alphabeticalByArtist' }; + const q: AlbumQuery = { + _index: 0, + _count: 500, + type: "alphabeticalByArtist", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -952,7 +1077,11 @@ describe("Navidrome", () => { }); it("should return the album", async () => { - const q: AlbumQuery = { _index: 0, _count: 500, type: 'alphabeticalByArtist' }; + const q: AlbumQuery = { + _index: 0, + _count: 500, + type: "alphabeticalByArtist", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -983,11 +1112,19 @@ describe("Navidrome", () => { const artist1 = anArtist({ name: "abba", - albums: [anAlbum({ genre: genre1 }), anAlbum({ genre: genre2 }), anAlbum({ genre: genre3 })], + albums: [ + anAlbum({ genre: genre1 }), + anAlbum({ genre: genre2 }), + anAlbum({ genre: genre3 }), + ], }); const artist2 = anArtist({ name: "babba", - albums: [anAlbum({ genre: genre1 }), anAlbum({ genre: genre2 }), anAlbum({ genre: genre3 })], + albums: [ + anAlbum({ genre: genre1 }), + anAlbum({ genre: genre2 }), + anAlbum({ genre: genre3 }), + ], }); const artists = [artist1, artist2]; const albums = artists.flatMap((artist) => artist.albums); @@ -1002,7 +1139,11 @@ describe("Navidrome", () => { describe("querying for all of them", () => { it("should return all of them with corrent paging information", async () => { - const q : AlbumQuery= { _index: 0, _count: 500, type: 'alphabeticalByArtist' }; + const q: AlbumQuery = { + _index: 0, + _count: 500, + type: "alphabeticalByArtist", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -1028,7 +1169,11 @@ describe("Navidrome", () => { describe("querying for a page of them", () => { it("should return the page with the corrent paging information", async () => { - const q : AlbumQuery = { _index: 2, _count: 2, type: 'alphabeticalByArtist' }; + const q: AlbumQuery = { + _index: 2, + _count: 2, + type: "alphabeticalByArtist", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -1080,7 +1225,11 @@ describe("Navidrome", () => { 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 q: AlbumQuery = { + _index: 0, + _count: 1000, + type: "alphabeticalByArtist", + }; const result = await navidrome .generateToken({ username, password }) .then((it) => it as AuthSuccess) @@ -1152,8 +1301,8 @@ describe("Navidrome", () => { describe("getting tracks", () => { describe("for an album", () => { describe("when the album has multiple tracks", () => { - const hipHop = asGenre("Hip-Hop") - const tripHop = asGenre("Trip-Hop") + const hipHop = asGenre("Hip-Hop"); + const tripHop = asGenre("Trip-Hop"); const album = anAlbum({ id: "album1", name: "Burnin", genre: hipHop }); const albumSummary = albumToAlbumSummary(album); @@ -1171,8 +1320,16 @@ describe("Navidrome", () => { const tracks = [ aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }), aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }), - aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }), - aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }), + aTrack({ + artist: artistSummary, + album: albumSummary, + genre: tripHop, + }), + aTrack({ + artist: artistSummary, + album: albumSummary, + genre: tripHop, + }), ]; beforeEach(() => { @@ -1203,9 +1360,13 @@ describe("Navidrome", () => { }); describe("when the album has only 1 track", () => { - const flipFlop = asGenre("Flip-Flop") + const flipFlop = asGenre("Flip-Flop"); - const album = anAlbum({ id: "album1", name: "Burnin", genre: flipFlop }); + const album = anAlbum({ + id: "album1", + name: "Burnin", + genre: flipFlop, + }); const albumSummary = albumToAlbumSummary(album); const artist = anArtist({ @@ -1218,7 +1379,13 @@ describe("Navidrome", () => { image: NO_IMAGES, }; - const tracks = [aTrack({ artist: artistSummary, album: albumSummary, genre: flipFlop })]; + const tracks = [ + aTrack({ + artist: artistSummary, + album: albumSummary, + genre: flipFlop, + }), + ]; beforeEach(() => { mockGET @@ -1287,7 +1454,7 @@ describe("Navidrome", () => { }); describe("a single track", () => { - const pop = asGenre("Pop") + const pop = asGenre("Pop"); const album = anAlbum({ id: "album1", name: "Burnin", genre: pop }); const albumSummary = albumToAlbumSummary(album); @@ -1302,7 +1469,11 @@ describe("Navidrome", () => { image: NO_IMAGES, }; - const track = aTrack({ artist: artistSummary, album: albumSummary, genre: pop }); + const track = aTrack({ + artist: artistSummary, + album: albumSummary, + genre: pop, + }); beforeEach(() => { mockGET diff --git a/tests/smapi.test.ts b/tests/smapi.test.ts index 6622c7e..fecba9b 100644 --- a/tests/smapi.test.ts +++ b/tests/smapi.test.ts @@ -557,9 +557,11 @@ describe("api", () => { { itemType: "container", id: "albums", title: "Albums" }, { itemType: "container", id: "genres", title: "Genres" }, { itemType: "container", id: "randomAlbums", title: "Random" }, + { itemType: "container", id: "recentlyAdded", title: "Recently Added" }, + { itemType: "container", id: "recentlyPlayed", title: "Recently Played" }, ], index: 0, - total: 4, + total: 6, }) ); });