Ability to query for recently added and recently played albums

This commit is contained in:
simojenki
2021-04-05 13:25:48 +10:00
parent 4730511a84
commit fa1ad8c18b
4 changed files with 240 additions and 60 deletions

View File

@@ -94,7 +94,7 @@ export const asResult = <T>([results, total]: [T[], number]) => ({
export type ArtistQuery = Paging; 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 & { export type AlbumQuery = Paging & {
type: AlbumQueryType; type: AlbumQueryType;

View File

@@ -8,6 +8,7 @@ import logger from "./logger";
import { LinkCodes } from "./link_codes"; import { LinkCodes } from "./link_codes";
import { import {
Album, Album,
AlbumQuery,
AlbumSummary, AlbumSummary,
ArtistSummary, ArtistSummary,
Genre, Genre,
@@ -442,6 +443,19 @@ function bindSmapiSoapServiceToExpress(
const [type, typeId] = id.split(":"); const [type, typeId] = id.split(":");
const paging = { _index: index, _count: count }; const paging = { _index: index, _count: count };
logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`); logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`);
const albums = (q: AlbumQuery): Promise<GetMetadataResponse> =>
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) { switch (type) {
case "root": case "root":
return getMetadataResult({ return getMetadataResult({
@@ -450,9 +464,14 @@ function bindSmapiSoapServiceToExpress(
container({ id: "albums", title: "Albums" }), container({ id: "albums", title: "Albums" }),
container({ id: "genres", title: "Genres" }), container({ id: "genres", title: "Genres" }),
container({ id: "randomAlbums", title: "Random" }), container({ id: "randomAlbums", title: "Random" }),
container({ id: "recentlyAdded", title: "Recently Added" }),
container({
id: "recentlyPlayed",
title: "Recently Played",
}),
], ],
index: 0, index: 0,
total: 4, total: 6,
}); });
case "artists": case "artists":
return await musicLibrary.artists(paging).then((result) => { return await musicLibrary.artists(paging).then((result) => {
@@ -465,31 +484,32 @@ function bindSmapiSoapServiceToExpress(
total: result.total, total: result.total,
}); });
}); });
case "albums": case "albums": {
return await musicLibrary return await albums({
.albums({ type: "alphabeticalByArtist", ...paging }) type: "alphabeticalByArtist",
.then((result) => { ...paging,
const accessToken = accessTokens.mint(authToken);
return getMetadataResult({
mediaCollection: result.results.map((it) =>
album(webAddress, accessToken, it)
),
index: paging._index,
total: result.total,
});
}); });
}
case "randomAlbums": case "randomAlbums":
return await musicLibrary return await albums({
.albums({ type: "random", ...paging }) type: "random",
.then((result) => { ...paging,
const accessToken = accessTokens.mint(authToken);
return getMetadataResult({
mediaCollection: result.results.map((it) =>
album(webAddress, accessToken, it)
),
index: paging._index,
total: result.total,
}); });
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": case "genres":
return await musicLibrary return await musicLibrary
@@ -546,19 +566,6 @@ function bindSmapiSoapServiceToExpress(
total, 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: default:
throw `Unsupported id:${id}`; throw `Unsupported id:${id}`;
} }

View File

@@ -7,7 +7,7 @@ import {
t, t,
BROWSER_HEADERS, BROWSER_HEADERS,
DODGY_IMAGE_NAME, DODGY_IMAGE_NAME,
asGenre asGenre,
} from "../src/navidrome"; } from "../src/navidrome";
import encryption from "../src/encryption"; import encryption from "../src/encryption";
@@ -280,7 +280,7 @@ describe("Navidrome", () => {
}); });
describe("when there are many", () => { describe("when there are many", () => {
const genres = ["g1", "g2", "g3", "g3"] const genres = ["g1", "g2", "g3", "g3"];
beforeEach(() => { beforeEach(() => {
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
@@ -868,7 +868,12 @@ 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, genre: "Pop", type: 'byGenre' }; const q: AlbumQuery = {
_index: 0,
_count: 500,
genre: "Pop",
type: "byGenre",
};
const result = await navidrome const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .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", () => { describe("when the artist has only 1 album", () => {
@@ -911,7 +1032,11 @@ describe("Navidrome", () => {
}); });
it("should return the album", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -952,7 +1077,11 @@ describe("Navidrome", () => {
}); });
it("should return the album", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -983,11 +1112,19 @@ describe("Navidrome", () => {
const artist1 = anArtist({ const artist1 = anArtist({
name: "abba", 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({ const artist2 = anArtist({
name: "babba", 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 artists = [artist1, artist2];
const albums = artists.flatMap((artist) => artist.albums); const albums = artists.flatMap((artist) => artist.albums);
@@ -1002,7 +1139,11 @@ describe("Navidrome", () => {
describe("querying for all of them", () => { describe("querying for all of them", () => {
it("should return all of them with corrent paging information", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1028,7 +1169,11 @@ 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 () => {
const q : AlbumQuery = { _index: 2, _count: 2, type: 'alphabeticalByArtist' }; const q: AlbumQuery = {
_index: 2,
_count: 2,
type: "alphabeticalByArtist",
};
const result = await navidrome const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1080,7 +1225,11 @@ describe("Navidrome", () => {
describe("querying for all of them", () => { describe("querying for all of them", () => {
it("will return only the first 500 with the correct paging information", async () => { 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 const result = await navidrome
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
@@ -1152,8 +1301,8 @@ describe("Navidrome", () => {
describe("getting tracks", () => { describe("getting tracks", () => {
describe("for an album", () => { describe("for an album", () => {
describe("when the album has multiple tracks", () => { describe("when the album has multiple tracks", () => {
const hipHop = asGenre("Hip-Hop") const hipHop = asGenre("Hip-Hop");
const tripHop = asGenre("Trip-Hop") const tripHop = asGenre("Trip-Hop");
const album = anAlbum({ id: "album1", name: "Burnin", genre: hipHop }); const album = anAlbum({ id: "album1", name: "Burnin", genre: hipHop });
const albumSummary = albumToAlbumSummary(album); const albumSummary = albumToAlbumSummary(album);
@@ -1171,8 +1320,16 @@ describe("Navidrome", () => {
const tracks = [ const tracks = [
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }), aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }), aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }), aTrack({
aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }), artist: artistSummary,
album: albumSummary,
genre: tripHop,
}),
aTrack({
artist: artistSummary,
album: albumSummary,
genre: tripHop,
}),
]; ];
beforeEach(() => { beforeEach(() => {
@@ -1203,9 +1360,13 @@ describe("Navidrome", () => {
}); });
describe("when the album has only 1 track", () => { 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 albumSummary = albumToAlbumSummary(album);
const artist = anArtist({ const artist = anArtist({
@@ -1218,7 +1379,13 @@ describe("Navidrome", () => {
image: NO_IMAGES, image: NO_IMAGES,
}; };
const tracks = [aTrack({ artist: artistSummary, album: albumSummary, genre: flipFlop })]; const tracks = [
aTrack({
artist: artistSummary,
album: albumSummary,
genre: flipFlop,
}),
];
beforeEach(() => { beforeEach(() => {
mockGET mockGET
@@ -1287,7 +1454,7 @@ describe("Navidrome", () => {
}); });
describe("a single track", () => { describe("a single track", () => {
const pop = asGenre("Pop") const pop = asGenre("Pop");
const album = anAlbum({ id: "album1", name: "Burnin", genre: pop }); const album = anAlbum({ id: "album1", name: "Burnin", genre: pop });
const albumSummary = albumToAlbumSummary(album); const albumSummary = albumToAlbumSummary(album);
@@ -1302,7 +1469,11 @@ describe("Navidrome", () => {
image: NO_IMAGES, image: NO_IMAGES,
}; };
const track = aTrack({ artist: artistSummary, album: albumSummary, genre: pop }); const track = aTrack({
artist: artistSummary,
album: albumSummary,
genre: pop,
});
beforeEach(() => { beforeEach(() => {
mockGET mockGET

View File

@@ -557,9 +557,11 @@ describe("api", () => {
{ itemType: "container", id: "albums", title: "Albums" }, { itemType: "container", id: "albums", title: "Albums" },
{ itemType: "container", id: "genres", title: "Genres" }, { itemType: "container", id: "genres", title: "Genres" },
{ itemType: "container", id: "randomAlbums", title: "Random" }, { itemType: "container", id: "randomAlbums", title: "Random" },
{ itemType: "container", id: "recentlyAdded", title: "Recently Added" },
{ itemType: "container", id: "recentlyPlayed", title: "Recently Played" },
], ],
index: 0, index: 0,
total: 4, total: 6,
}) })
); );
}); });