Ability to play a playlist

This commit is contained in:
simojenki
2021-05-08 10:33:59 +10:00
parent 5c692f6eb2
commit 4229ad1836
8 changed files with 634 additions and 115 deletions

View File

@@ -9,7 +9,7 @@ Currently only a single integration allowing Navidrome to be registered with son
## Features
- Integrates with Navidrome
- Browse by Artist, Albums, Genres, Random Albums, Starred Albums, Recently Added Albums, Recently Played Albums, Most Played Albums
- Browse by Artist, Albums, Genres, Playlist, Random Albums, Starred Albums, Recently Added Albums, Recently Played Albums, Most Played Albums
- Artist Art
- Album Art
- View Related Artists via Artist -> '...' -> Menu -> Related Arists
@@ -20,6 +20,7 @@ Currently only a single integration allowing Navidrome to be registered with son
- Multiple registrations within a single household.
- Transcoding performed by Navidrome with specific player for bonob/sonos, customisable by mimeType
- Ability to search by Album, Artist, Track
- Ability to play a playlist
## Running
@@ -83,4 +84,4 @@ BONOB_NAVIDROME_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for cust
## TODO
- Artist Radio
- Playlist support
- Add tracks to playlists

View File

@@ -131,6 +131,15 @@ export type CoverArt = {
data: Buffer;
}
export type PlaylistSummary = {
id: string,
name: string
}
export type Playlist = PlaylistSummary & {
entries: Track[]
}
export const range = (size: number) => [...Array(size).keys()];
export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] =>
@@ -163,4 +172,6 @@ export interface MusicLibrary {
searchArtists(query: string): Promise<ArtistSummary[]>;
searchAlbums(query: string): Promise<AlbumSummary[]>;
searchTracks(query: string): Promise<Track[]>;
playlists(): Promise<PlaylistSummary[]>;
playlist(id: string): Promise<Playlist>;
}

View File

@@ -164,6 +164,38 @@ export type GetAlbumResponse = {
};
};
export type playlist = {
_id: string;
_name: string;
};
export type entry = {
_id: string;
_parent: string;
_title: string;
_album: string;
_artist: string;
_track: string;
_year: string;
_genre: string;
_contentType: string;
_duration: string;
_albumId: string;
_artistId: string;
};
export type GetPlaylistResponse = {
playlist: {
_id: string;
_name: string;
entry: entry[];
};
};
export type GetPlaylistsResponse = {
playlists: { playlist: playlist[] };
};
export type GetSongResponse = {
song: song;
};
@@ -218,7 +250,7 @@ const asAlbum = (album: album) => ({
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist
artistName: album._artist,
});
export const asGenre = (genreName: string) => ({
@@ -297,13 +329,15 @@ export class Navidrome implements MusicService {
(response) =>
new X2JS({
arrayAccessFormPaths: [
"subsonic-response.artist.album",
"subsonic-response.albumList.album",
"subsonic-response.album.song",
"subsonic-response.genres.genre",
"subsonic-response.albumList.album",
"subsonic-response.artist.album",
"subsonic-response.artistInfo.similarArtist",
"subsonic-response.searchResult3.artist",
"subsonic-response.genres.genre",
"subsonic-response.playlist.entry",
"subsonic-response.playlists.playlist",
"subsonic-response.searchResult3.album",
"subsonic-response.searchResult3.artist",
"subsonic-response.searchResult3.song",
],
}).xml2js(response.data) as SubconicEnvelope
@@ -366,7 +400,7 @@ export class Navidrome implements MusicService {
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist
artistName: album._artist,
}));
getArtist = (
@@ -431,7 +465,7 @@ export class Navidrome implements MusicService {
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist
artistName: album._artist,
}));
search3 = (credentials: Credentials, q: any) =>
@@ -610,6 +644,44 @@ export class Navidrome implements MusicService {
songs.map((it) => navidrome.getTrack(credentials, it._id))
)
),
playlists: async () =>
navidrome
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
.then((it) => it.playlists.playlist || [])
.then((playlists) =>
playlists.map((it) => ({ id: it._id, name: it._name }))
),
playlist: async (id: string) =>
navidrome
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
id,
})
.then((it) => it.playlist)
.then((playlist) => ({
id: playlist._id,
name: playlist._name,
entries: (playlist.entry || []).map((entry) => ({
id: entry._id,
name: entry._title,
mimeType: entry._contentType,
duration: parseInt(entry._duration || "0"),
number: parseInt(entry._track || "0"),
genre: maybeAsGenre(entry._genre),
album: {
id: entry._albumId,
name: entry._album,
year: entry._year,
genre: maybeAsGenre(entry._genre),
artistName: entry._artist,
artistId: entry._artistId,
},
artist: {
id: entry._artistId,
name: entry._artist,
},
})),
})),
};
return Promise.resolve(musicLibrary);

View File

@@ -13,6 +13,7 @@ import {
ArtistSummary,
Genre,
MusicService,
PlaylistSummary,
slice2,
Track,
} from "./music_service";
@@ -194,7 +195,7 @@ export type Container = {
itemType: ContainerType;
id: string;
title: string;
displayType: string | undefined
displayType: string | undefined;
};
const genre = (genre: Genre) => ({
@@ -203,6 +204,13 @@ const genre = (genre: Genre) => ({
title: genre.name,
});
const playlist = (playlist: PlaylistSummary) => ({
itemType: "album",
id: `playlist:${playlist.id}`,
title: playlist.name,
canPlay: true,
});
export const defaultAlbumArtURI = (
webAddress: string,
accessToken: string,
@@ -487,6 +495,11 @@ function bindSmapiSoapServiceToExpress(
id: "albums",
title: "Albums",
},
{
itemType: "container",
id: "playlists",
title: "Playlists",
},
{
itemType: "container",
id: "genres",
@@ -519,7 +532,7 @@ function bindSmapiSoapServiceToExpress(
},
],
index: 0,
total: 8,
total: 9,
});
case "search":
return getMetadataResult({
@@ -589,6 +602,31 @@ function bindSmapiSoapServiceToExpress(
total,
})
);
case "playlists":
return musicLibrary
.playlists()
.then(slice2(paging))
.then(([page, total]) =>
getMetadataResult({
mediaCollection: page.map(playlist),
index: paging._index,
total,
})
);
case "playlist":
return musicLibrary
.playlist(typeId!)
.then(playlist => playlist.entries)
.then(slice2(paging))
.then(([page, total]) => {
return getMetadataResult({
mediaMetadata: page.map((it) =>
track(webAddress, accessToken, it)
),
index: paging._index,
total,
});
});
case "artist":
return musicLibrary
.artist(typeId!)

View File

@@ -3,7 +3,7 @@ import { v4 as uuid } from "uuid";
import { Credentials } from "../src/smapi";
import { Service, Device } from "../src/sonos";
import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary } from "../src/music_service";
import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary, PlaylistSummary, Playlist } from "../src/music_service";
import randomString from "../src/random_string";
const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max));
@@ -28,6 +28,23 @@ export const aService = (fields: Partial<Service> = {}): Service => ({
...fields,
});
export function aPlaylistSummary(fields: Partial<PlaylistSummary> = {}): PlaylistSummary {
return {
id: `playlist-${uuid()}`,
name: `playlistname-${randomString()}`,
...fields
}
}
export function aPlaylist(fields: Partial<Playlist> = {}): Playlist {
return {
id: `playlist-${uuid()}`,
name: `playlist-${randomString()}`,
entries: [aTrack(), aTrack()],
...fields
}
}
export function aDevice(fields: Partial<Device> = {}): Device {
return {
name: `device-${uuid()}`,
@@ -109,15 +126,17 @@ export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];
export function aTrack(fields: Partial<Track> = {}): Track {
const id = uuid();
const artist = anArtist();
const genre = fields.genre || randomGenre();
return {
id,
name: `Track ${id}`,
mimeType: `audio/mp3-${id}`,
duration: randomInt(500),
number: randomInt(100),
genre: randomGenre(),
artist: artistToArtistSummary(anArtist()),
album: albumToAlbumSummary(anAlbum()),
genre,
artist: artistToArtistSummary(artist),
album: albumToAlbumSummary(anAlbum({ artistId: artist.id, artistName: artist.name, genre })),
...fields,
};
}

View File

@@ -133,6 +133,9 @@ export class InMemoryMusicService implements MusicService {
searchArtists: async (_: string) => Promise.resolve([]),
searchAlbums: async (_: string) => Promise.resolve([]),
searchTracks: async (_: string) => Promise.resolve([]),
playlists: async () => Promise.resolve([]),
playlist: async (id: string) =>
Promise.reject(`No playlist with id ${id}`),
});
}

View File

@@ -31,8 +31,17 @@ import {
AlbumSummary,
artistToArtistSummary,
AlbumQuery,
PlaylistSummary,
Playlist,
ArtistSummary,
} from "../src/music_service";
import { anAlbum, anArtist, aTrack } from "./builders";
import {
anAlbum,
anArtist,
aPlaylist,
aPlaylistSummary,
aTrack,
} from "./builders";
jest.mock("../src/random_string");
@@ -103,6 +112,9 @@ const ok = (data: string) => ({
data,
});
const similarArtistXml = (artistSummary: ArtistSummary) =>
`<similarArtist id="${artistSummary.id}" name="${artistSummary.name}" albumCount="3"></similarArtist>`;
const getArtistInfoXml = (
artist: Artist
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
@@ -113,10 +125,7 @@ const getArtistInfoXml = (
<smallImageUrl>${artist.image.small || ""}</smallImageUrl>
<mediumImageUrl>${artist.image.medium || ""}</mediumImageUrl>
<largeImageUrl>${artist.image.large || ""}</largeImageUrl>
${artist.similarArtists.map(
(it) =>
`<similarArtist id="${it.id}" name="${it.name}" albumCount="3"></similarArtist>`
)}
${artist.similarArtists.map(similarArtistXml).join("")}
</artistInfo>
</subsonic-response>`;
@@ -137,7 +146,7 @@ const albumXml = (
created="2021-01-07T08:19:55.834207205Z"
artistId="${artist.id}"
songCount="19"
isVideo="false">${tracks.map((track) => songXml(track))}</album>`;
isVideo="false">${tracks.map(songXml).join("")}</album>`;
const songXml = (track: Track) => `<song
id="${track.id}"
@@ -165,18 +174,20 @@ const albumListXml = (
albums: [Artist, Album][]
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<albumList>
${albums.map(([artist, album]) =>
albumXml(artist, album)
)}
${albums
.map(([artist, album]) => albumXml(artist, album))
.join("")}
</albumList>
</subsonic-response>`;
const artistXml = (artist: Artist) => `<artist id="${artist.id}" name="${
artist.name
}" albumCount="${artist.albums.length}" artistImageUrl="....">
${artist.albums.map((album) =>
${artist.albums
.map((album) =>
albumXml(artist, album)
)}
)
.join("")}
</artist>`;
const getArtistXml = (
@@ -185,14 +196,14 @@ const getArtistXml = (
${artistXml(artist)}
</subsonic-response>`;
const genreXml = (genre: string) =>
`<genre songCount="1475" albumCount="86">${genre}</genre>`;
const genresXml = (
genres: string[]
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
<genres>
${genres.map(
(it) =>
`<genre songCount="1475" albumCount="86">${it}</genre>`
)}
${genres.map(genreXml).join("")}
</genres>
</subsonic-response>`;
@@ -221,6 +232,56 @@ export type ArtistWithAlbum = {
album: Album;
};
const playlistXml = (playlist: PlaylistSummary) =>
`<playlist id="${playlist.id}" name="${playlist.name}" songCount="1" duration="190" public="true" owner="bob" created="2021-05-06T02:07:24.308007023Z" changed="2021-05-06T02:08:06Z"></playlist>`;
const getPlayLists = (
playlists: PlaylistSummary[]
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.42.0 (f1bd736b)">
<playlists>
${playlists.map(playlistXml).join("")}
</playlists>
</subsonic-response>`;
const error = (code: string, message: string) =>
`<subsonic-response xmlns="http://subsonic.org/restapi" status="failed" version="1.16.1" type="navidrome" serverVersion="0.42.0 (f1bd736b)"><error code="${code}" message="${message}"></error></subsonic-response>`;
const getPlayList = (
playlist: Playlist
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.42.0 (f1bd736b)">
<playlist id="${playlist.id}" name="${playlist.name}" songCount="${
playlist.entries.length
}" duration="627" public="true" owner="bob" created="2021-05-06T02:07:30.460465988Z" changed="2021-05-06T02:40:04Z">
${playlist.entries
.map(
(it) => `<entry
id="${it.id}"
parent="..."
isDir="false"
title="${it.name}"
album="${it.album.name}"
artist="${it.artist.name}"
track="${it.number}"
year="${it.album.year}"
genre="${it.album.genre?.name}"
coverArt="..."
size="123"
contentType="${it.mimeType}"
suffix="mp3"
duration="${it.duration}"
bitRate="128"
path="..."
discNumber="1"
created="2019-09-04T04:07:00.138169924Z"
albumId="${it.album.id}"
artistId="${it.artist.id}"
type="music"
isVideo="false"></entry>`
)
.join("")}
</playlist>
</subsonic-response>`;
const searchResult3 = ({
artists,
albums,
@@ -231,14 +292,16 @@ const searchResult3 = ({
tracks: Track[];
}>) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.41.1 (43bb0758)">
<searchResult3>
${(artists || []).map((it) =>
${(artists || [])
.map((it) =>
artistXml({
...it,
albums: [],
})
)}
${(albums || []).map((it) => albumXml(it.artist, it.album, []))}
${(tracks || []).map((it) => songXml(it))}
)
.join("")}
${(albums || []).map((it) => albumXml(it.artist, it.album, [])).join("")}
${(tracks || []).map((it) => songXml(it)).join("")}
</searchResult3>
</subsonic-response>`;
@@ -1382,8 +1445,16 @@ describe("Navidrome", () => {
});
const tracks = [
aTrack({ artist: artistToArtistSummary(artist), album: albumToAlbumSummary(album), genre: hipHop }),
aTrack({ artist: artistToArtistSummary(artist), album: albumToAlbumSummary(album), genre: hipHop }),
aTrack({
artist: artistToArtistSummary(artist),
album: albumToAlbumSummary(album),
genre: hipHop,
}),
aTrack({
artist: artistToArtistSummary(artist),
album: albumToAlbumSummary(album),
genre: hipHop,
}),
aTrack({
artist: artistToArtistSummary(artist),
album: albumToAlbumSummary(album),
@@ -2728,7 +2799,7 @@ describe("Navidrome", () => {
name: "foo woo",
genre: { id: "pop", name: "pop" },
});
const artist = anArtist({ name: "#1", albums:[album] });
const artist = anArtist({ name: "#1", albums: [album] });
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
@@ -2988,4 +3059,171 @@ describe("Navidrome", () => {
});
});
});
describe("playlists", () => {
describe("getting playlists", () => {
describe("when there is 1 playlist results", () => {
it("should return it", async () => {
const playlist = aPlaylistSummary();
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getPlayLists([playlist])))
);
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlists());
expect(result).toEqual([playlist]);
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
params: authParams,
headers,
});
});
});
describe("when there are many playlists", () => {
it("should return them", async () => {
const playlist1 = aPlaylistSummary();
const playlist2 = aPlaylistSummary();
const playlist3 = aPlaylistSummary();
const playlists = [playlist1, playlist2, playlist3];
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getPlayLists(playlists)))
);
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlists());
expect(result).toEqual(playlists);
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
params: authParams,
headers,
});
});
});
describe("when there are no playlists", () => {
it("should return []", async () => {
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getPlayLists([])))
);
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlists());
expect(result).toEqual([]);
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
params: authParams,
headers,
});
});
});
});
describe("getting a single playlist", () => {
describe("when there is no playlist with the id", () => {
it("should raise error", async () => {
const id = "id404";
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(error("70", "not there")))
);
return expect(
navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlist(id))
).rejects.toEqual("not there");
});
});
describe("when there is a playlist with the id", () => {
describe("and it has tracks", () => {
it("should return the playlist with entries", async () => {
const playlist = aPlaylist({
entries: [
aTrack({ genre: { id: "pop", name: "pop" } }),
aTrack({ genre: { id: "rock", name: "rock" } }),
],
});
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getPlayList(playlist)))
);
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlist(playlist.id));
expect(result).toEqual(playlist);
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylist`, {
params: {
id: playlist.id,
...authParams,
},
headers,
});
});
});
describe("and it has no tracks", () => {
it("should return the playlist with empty entries", async () => {
const playlist = aPlaylist({
entries: [],
});
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
Promise.resolve(ok(getPlayList(playlist)))
);
const result = await navidrome
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken))
.then((it) => it.playlist(playlist.id));
expect(result).toEqual(playlist);
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylist`, {
params: {
id: playlist.id,
...authParams,
},
headers,
});
});
});
});
});
});
});

View File

@@ -242,7 +242,7 @@ describe("album", () => {
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, someAlbum),
canPlay: true,
artist: someAlbum.artistName,
artistId: someAlbum.artistId
artistId: someAlbum.artistId,
});
});
});
@@ -288,6 +288,8 @@ describe("api", () => {
artists: jest.fn(),
artist: jest.fn(),
genres: jest.fn(),
playlists: jest.fn(),
playlist: jest.fn(),
albums: jest.fn(),
tracks: jest.fn(),
track: jest.fn(),
@@ -644,7 +646,9 @@ describe("api", () => {
});
expect(result[0]).toEqual(
searchResult({
mediaCollection: tracks.map((it) => album(rootUrl, accessToken, it.album)),
mediaCollection: tracks.map((it) =>
album(rootUrl, accessToken, it.album)
),
index: 0,
total: 2,
})
@@ -725,6 +729,11 @@ describe("api", () => {
mediaCollection: [
{ itemType: "container", id: "artists", title: "Artists" },
{ itemType: "albumList", id: "albums", title: "Albums" },
{
itemType: "container",
id: "playlists",
title: "Playlists",
},
{ itemType: "container", id: "genres", title: "Genres" },
{
itemType: "albumList",
@@ -753,7 +762,7 @@ describe("api", () => {
},
],
index: 0,
total: 8,
total: 9,
})
);
});
@@ -830,6 +839,66 @@ describe("api", () => {
});
});
describe("asking for playlists", () => {
const expectedPlayLists = [
{ id: "1", name: "pl1" },
{ id: "2", name: "pl2" },
{ id: "3", name: "pl3" },
{ id: "4", name: "pl4" },
];
beforeEach(() => {
musicLibrary.playlists.mockResolvedValue(expectedPlayLists);
});
describe("asking for all playlists", () => {
it("should return a collection of playlists", async () => {
const result = await ws.getMetadataAsync({
id: `playlists`,
index: 0,
count: 100,
});
expect(result[0]).toEqual(
getMetadataResult({
mediaCollection: expectedPlayLists.map((playlist) => ({
itemType: "album",
id: `playlist:${playlist.id}`,
title: playlist.name,
canPlay: true,
})),
index: 0,
total: expectedPlayLists.length,
})
);
});
});
describe("asking for a page of playlists", () => {
it("should return just that page", async () => {
const result = await ws.getMetadataAsync({
id: `playlists`,
index: 1,
count: 2,
});
expect(result[0]).toEqual(
getMetadataResult({
mediaCollection: [
expectedPlayLists[1]!,
expectedPlayLists[2]!,
].map((playlist) => ({
itemType: "album",
id: `playlist:${playlist.id}`,
title: playlist.name,
canPlay: true,
})),
index: 1,
total: expectedPlayLists.length,
})
);
});
});
});
describe("asking for a single artist", () => {
const artistWithManyAlbums = anArtist({
albums: [anAlbum(), anAlbum(), anAlbum(), anAlbum(), anAlbum()],
@@ -856,7 +925,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: artistWithManyAlbums.albums.length,
@@ -889,7 +958,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 2,
total: artistWithManyAlbums.albums.length,
@@ -1144,7 +1213,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1189,7 +1258,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1234,7 +1303,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1279,7 +1348,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1324,7 +1393,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1367,7 +1436,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 6,
@@ -1410,7 +1479,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 2,
total: 6,
@@ -1451,7 +1520,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 4,
@@ -1495,7 +1564,7 @@ describe("api", () => {
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
canPlay: true,
artistId: it.artistId,
artist: it.artistName
artist: it.artistName,
})),
index: 0,
total: 4,
@@ -1512,8 +1581,7 @@ describe("api", () => {
});
});
describe("asking for tracks", () => {
describe("for an album", () => {
describe("asking for an album", () => {
const album = anAlbum();
const artist = anArtist({
albums: [album],
@@ -1583,6 +1651,75 @@ describe("api", () => {
});
});
});
describe("asking for a playlist", () => {
const track1 = aTrack();
const track2 = aTrack();
const track3 = aTrack();
const track4 = aTrack();
const track5 = aTrack();
const playlist = {
id: uuid(),
name: "playlist for test",
entries: [track1, track2, track3, track4, track5]
}
beforeEach(() => {
musicLibrary.playlist.mockResolvedValue(playlist);
});
describe("asking for all for a playlist", () => {
it("should return them all", async () => {
const paging = {
index: 0,
count: 100,
};
const result = await ws.getMetadataAsync({
id: `playlist:${playlist.id}`,
...paging,
});
expect(result[0]).toEqual(
getMetadataResult({
mediaMetadata: playlist.entries.map((it) =>
track(rootUrl, accessToken, it)
),
index: 0,
total: playlist.entries.length,
})
);
expect(musicLibrary.playlist).toHaveBeenCalledWith(playlist.id);
});
});
describe("asking for a single page of a playlists entries", () => {
const pageOfTracks = [track3, track4];
it("should return only that page", async () => {
const paging = {
index: 2,
count: 2,
};
const result = await ws.getMetadataAsync({
id: `playlist:${playlist.id}`,
...paging,
});
expect(result[0]).toEqual(
getMetadataResult({
mediaMetadata: pageOfTracks.map((it) =>
track(rootUrl, accessToken, it)
),
index: paging.index,
total: playlist.entries.length,
})
);
expect(musicLibrary.playlist).toHaveBeenCalledWith(playlist.id);
});
});
});
});
});