mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to heart and star tracks whilst playing
Ability to heart and star tracks whilst playing
This commit is contained in:
@@ -151,6 +151,7 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
||||
const id = uuid();
|
||||
const artist = anArtist();
|
||||
const genre = fields.genre || randomGenre();
|
||||
const rating = { love: false, stars: Math.floor(Math.random() * 5) };
|
||||
return {
|
||||
id,
|
||||
name: `Track ${id}`,
|
||||
@@ -163,9 +164,10 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
||||
anAlbum({ artistId: artist.id, artistName: artist.name, genre })
|
||||
),
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
rating,
|
||||
...fields,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||
const id = uuid();
|
||||
|
||||
@@ -175,8 +175,8 @@ describe("InMemoryMusicService", () => {
|
||||
describe("fetching tracks for an album", () => {
|
||||
it("should return only tracks on that album", async () => {
|
||||
expect(await musicLibrary.tracks(artist1Album1.id)).toEqual([
|
||||
track1,
|
||||
track2,
|
||||
{ ...track1, rating: { love: false, stars: 0 } },
|
||||
{ ...track2, rating: { love: false, stars: 0 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -192,7 +192,7 @@ describe("InMemoryMusicService", () => {
|
||||
describe("fetching a single track", () => {
|
||||
describe("when it exists", () => {
|
||||
it("should return the track", async () => {
|
||||
expect(await musicLibrary.track(track3.id)).toEqual(track3);
|
||||
expect(await musicLibrary.track(track3.id)).toEqual({ ...track3, rating: { love: false, stars: 0 } },);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -221,7 +221,10 @@ describe("InMemoryMusicService", () => {
|
||||
],
|
||||
});
|
||||
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] });
|
||||
const artist3 = anArtist({ name: "artist3", albums: [artist3_album1, artist3_album2] });
|
||||
const artist3 = anArtist({
|
||||
name: "artist3",
|
||||
albums: [artist3_album1, artist3_album2],
|
||||
});
|
||||
const artistWithNoAlbums = anArtist({ albums: [] });
|
||||
|
||||
const allAlbums = [artist1, artist2, artist3, artistWithNoAlbums].flatMap(
|
||||
@@ -258,7 +261,7 @@ describe("InMemoryMusicService", () => {
|
||||
});
|
||||
|
||||
expect(albums.total).toEqual(totalAlbumCount);
|
||||
expect(albums.results.length).toEqual(3)
|
||||
expect(albums.results.length).toEqual(3);
|
||||
// cannot really assert the results and they will change every time
|
||||
});
|
||||
});
|
||||
@@ -282,9 +285,9 @@ describe("InMemoryMusicService", () => {
|
||||
albumToAlbumSummary(artist1_album3),
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
|
||||
|
||||
albumToAlbumSummary(artist2_album1),
|
||||
|
||||
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
@@ -302,13 +305,11 @@ describe("InMemoryMusicService", () => {
|
||||
type: "alphabeticalByName",
|
||||
})
|
||||
).toEqual({
|
||||
results:
|
||||
_.sortBy(allAlbums, 'name').map(albumToAlbumSummary),
|
||||
results: _.sortBy(allAlbums, "name").map(albumToAlbumSummary),
|
||||
total: totalAlbumCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("fetching a page", () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
albumToAlbumSummary,
|
||||
Track,
|
||||
Genre,
|
||||
Rating,
|
||||
} from "../src/music_service";
|
||||
|
||||
export class InMemoryMusicService implements MusicService {
|
||||
@@ -75,9 +76,11 @@ export class InMemoryMusicService implements MusicService {
|
||||
switch (q.type) {
|
||||
case "alphabeticalByArtist":
|
||||
return artist2Album;
|
||||
case "alphabeticalByName":
|
||||
return artist2Album.sort((a, b) => a.album.name.localeCompare(b.album.name));
|
||||
case "byGenre":
|
||||
case "alphabeticalByName":
|
||||
return artist2Album.sort((a, b) =>
|
||||
a.album.name.localeCompare(b.album.name)
|
||||
);
|
||||
case "byGenre":
|
||||
return artist2Album.filter(
|
||||
(it) => it.album.genre?.id === q.genre
|
||||
);
|
||||
@@ -107,18 +110,21 @@ export class InMemoryMusicService implements MusicService {
|
||||
A.map((it) => O.fromNullable(it.genre)),
|
||||
A.compact,
|
||||
A.uniq(fromEquals((x, y) => x.id === y.id)),
|
||||
A.sort(
|
||||
fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id))
|
||||
)
|
||||
A.sort(fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id)))
|
||||
)
|
||||
),
|
||||
tracks: (albumId: string) =>
|
||||
Promise.resolve(this.tracks.filter((it) => it.album.id === albumId)),
|
||||
Promise.resolve(
|
||||
this.tracks
|
||||
.filter((it) => it.album.id === albumId)
|
||||
.map((it) => ({ ...it, rating: { love: false, stars: 0 } }))
|
||||
),
|
||||
rate: (_: string, _2: Rating) => Promise.resolve(false),
|
||||
track: (trackId: string) =>
|
||||
pipe(
|
||||
this.tracks.find((it) => it.id === trackId),
|
||||
O.fromNullable,
|
||||
O.map((it) => Promise.resolve(it)),
|
||||
O.map((it) => Promise.resolve({ ...it, rating: { love: false, stars: 0 } })),
|
||||
O.getOrElse(() =>
|
||||
Promise.reject(`Failed to find track with id ${trackId}`)
|
||||
)
|
||||
@@ -139,10 +145,14 @@ export class InMemoryMusicService implements MusicService {
|
||||
playlists: async () => Promise.resolve([]),
|
||||
playlist: async (id: string) =>
|
||||
Promise.reject(`No playlist with id ${id}`),
|
||||
createPlaylist: async (_: string) => Promise.reject("Unsupported operation"),
|
||||
deletePlaylist: async (_: string) => Promise.reject("Unsupported operation"),
|
||||
addToPlaylist: async (_: string) => Promise.reject("Unsupported operation"),
|
||||
removeFromPlaylist: async (_: string, _2: number[]) => Promise.reject("Unsupported operation"),
|
||||
createPlaylist: async (_: string) =>
|
||||
Promise.reject("Unsupported operation"),
|
||||
deletePlaylist: async (_: string) =>
|
||||
Promise.reject("Unsupported operation"),
|
||||
addToPlaylist: async (_: string) =>
|
||||
Promise.reject("Unsupported operation"),
|
||||
removeFromPlaylist: async (_: string, _2: number[]) =>
|
||||
Promise.reject("Unsupported operation"),
|
||||
similarSongs: async (_: string) => Promise.resolve([]),
|
||||
topSongs: async (_: string) => Promise.resolve([]),
|
||||
});
|
||||
|
||||
@@ -1668,7 +1668,7 @@ describe("server", () => {
|
||||
"playlists",
|
||||
"genres",
|
||||
"random",
|
||||
"starred",
|
||||
"heart",
|
||||
"recentlyAdded",
|
||||
"recentlyPlayed",
|
||||
"mostPlayed",
|
||||
|
||||
@@ -24,8 +24,11 @@ import {
|
||||
iconArtURI,
|
||||
playlistAlbumArtURL,
|
||||
sonosifyMimeType,
|
||||
ratingAsInt,
|
||||
ratingFromInt,
|
||||
} from "../src/smapi";
|
||||
|
||||
import { keys as i8nKeys } from '../src/i8n';
|
||||
import {
|
||||
aService,
|
||||
getAppLinkMessage,
|
||||
@@ -54,6 +57,32 @@ import { iconForGenre } from "../src/icon";
|
||||
|
||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||
|
||||
|
||||
describe("rating to and from ints", () => {
|
||||
describe("ratingAsInt", () => {
|
||||
[
|
||||
{ rating: { love: false, stars: 0 }, expectedValue: 100 },
|
||||
{ rating: { love: true, stars: 0 }, expectedValue: 101 },
|
||||
{ rating: { love: false, stars: 1 }, expectedValue: 110 },
|
||||
{ rating: { love: true, stars: 1 }, expectedValue: 111 },
|
||||
{ rating: { love: false, stars: 2 }, expectedValue: 120 },
|
||||
{ rating: { love: true, stars: 2 }, expectedValue: 121 },
|
||||
{ rating: { love: false, stars: 3 }, expectedValue: 130 },
|
||||
{ rating: { love: true, stars: 3 }, expectedValue: 131 },
|
||||
{ rating: { love: false, stars: 4 }, expectedValue: 140 },
|
||||
{ rating: { love: true, stars: 4 }, expectedValue: 141 },
|
||||
{ rating: { love: false, stars: 5 }, expectedValue: 150 },
|
||||
{ rating: { love: true, stars: 5 }, expectedValue: 151 },
|
||||
].forEach(({ rating, expectedValue }) => {
|
||||
it(`should map ${JSON.stringify(rating)} to a ${expectedValue} and back`, () => {
|
||||
const actualValue = ratingAsInt(rating);
|
||||
expect(actualValue).toEqual(expectedValue);
|
||||
expect(ratingFromInt(actualValue)).toEqual(rating);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("service config", () => {
|
||||
const bonobWithNoContextPath = url("http://localhost:1234");
|
||||
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
|
||||
@@ -72,18 +101,18 @@ describe("service config", () => {
|
||||
pathname: PRESENTATION_MAP_ROUTE,
|
||||
});
|
||||
|
||||
async function fetchStringsXml() {
|
||||
const res = await request(server).get(stringsUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
return parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
}
|
||||
|
||||
describe(STRINGS_ROUTE, () => {
|
||||
async function fetchStringsXml() {
|
||||
const res = await request(server).get(stringsUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
return parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
}
|
||||
|
||||
it("should return xml for the strings", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
|
||||
@@ -120,15 +149,17 @@ describe("service config", () => {
|
||||
});
|
||||
|
||||
describe(PRESENTATION_MAP_ROUTE, () => {
|
||||
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
||||
async function presentationMapXml() {
|
||||
const res = await request(server).get(presentationUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
const xml = parseXML(
|
||||
return parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
}
|
||||
|
||||
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
||||
const xml = await presentationMapXml();
|
||||
|
||||
const imageSizeMap = (size: string) =>
|
||||
xpath.select(
|
||||
@@ -142,14 +173,7 @@ describe("service config", () => {
|
||||
});
|
||||
|
||||
it("should have an BrowseIconSizeMap for all sizes recommended by sonos", async () => {
|
||||
const res = await request(server).get(presentationUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
const xml = parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
const xml = await presentationMapXml();
|
||||
|
||||
const imageSizeMap = (size: string) =>
|
||||
xpath.select(
|
||||
@@ -161,6 +185,64 @@ describe("service config", () => {
|
||||
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NowPlayingRatings", () => {
|
||||
it("should have Matches with propname = rating", async () => {
|
||||
const xml = await presentationMapXml();
|
||||
|
||||
const matchElements = xpath.select(
|
||||
`/Presentation/PresentationMap[@type="NowPlayingRatings"]/Match`,
|
||||
xml
|
||||
) as Element[];
|
||||
|
||||
expect(matchElements.length).toBe(12);
|
||||
|
||||
matchElements.forEach((match) => {
|
||||
expect(match.getAttributeNode("propname")?.value).toEqual(
|
||||
"rating"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have Rating stringIds that are in strings.xml", async () => {
|
||||
const xml = await presentationMapXml();
|
||||
|
||||
const ratingElements = xpath.select(
|
||||
`/Presentation/PresentationMap[@type="NowPlayingRatings"]/Match/Ratings/Rating`,
|
||||
xml
|
||||
) as Element[];
|
||||
|
||||
expect(ratingElements.length).toBeGreaterThan(1);
|
||||
|
||||
ratingElements.forEach((rating) => {
|
||||
const OnSuccessStringId =
|
||||
rating.getAttributeNode("OnSuccessStringId")!.value;
|
||||
const StringId = rating.getAttributeNode("StringId")!.value;
|
||||
|
||||
expect(i8nKeys()).toContain(OnSuccessStringId);
|
||||
expect(i8nKeys()).toContain(StringId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have Rating Ids that are valid ratings as ints", async () => {
|
||||
const xml = await presentationMapXml();
|
||||
|
||||
const ratingElements = xpath.select(
|
||||
`/Presentation/PresentationMap[@type="NowPlayingRatings"]/Match/Ratings/Rating`,
|
||||
xml
|
||||
) as Element[];
|
||||
|
||||
expect(ratingElements.length).toBeGreaterThan(1);
|
||||
|
||||
ratingElements.forEach((ratingElement) => {
|
||||
|
||||
const rating = ratingFromInt(Math.abs(Number.parseInt(ratingElement.getAttributeNode("Id")!.value)))
|
||||
expect(rating.love).toBeDefined();
|
||||
expect(rating.stars).toBeGreaterThanOrEqual(0);
|
||||
expect(rating.stars).toBeLessThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -264,13 +346,17 @@ describe("track", () => {
|
||||
genre: { id: "genre101", name: "some genre" },
|
||||
}),
|
||||
artist: anArtist({ name: "great artist", id: uuid() }),
|
||||
coverArt:"coverArt:887766"
|
||||
coverArt: "coverArt:887766",
|
||||
rating: {
|
||||
love: true,
|
||||
stars: 5
|
||||
}
|
||||
});
|
||||
|
||||
expect(track(bonobUrl, someTrack)).toEqual({
|
||||
itemType: "track",
|
||||
id: `track:${someTrack.id}`,
|
||||
mimeType: 'audio/flac',
|
||||
mimeType: "audio/flac",
|
||||
title: someTrack.name,
|
||||
|
||||
trackMetadata: {
|
||||
@@ -286,6 +372,14 @@ describe("track", () => {
|
||||
genreId: someTrack.album.genre?.id,
|
||||
trackNumber: someTrack.number,
|
||||
},
|
||||
dynamic: {
|
||||
property: [
|
||||
{
|
||||
name: "rating",
|
||||
value: `${ratingAsInt(someTrack.rating)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -328,7 +422,10 @@ describe("playlistAlbumArtURL", () => {
|
||||
it("should return question mark icon", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [aTrack({ coverArt: undefined }), aTrack({ coverArt: undefined })],
|
||||
entries: [
|
||||
aTrack({ coverArt: undefined }),
|
||||
aTrack({ coverArt: undefined }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
@@ -403,7 +500,9 @@ describe("playlistAlbumArtURL", () => {
|
||||
});
|
||||
|
||||
describe("defaultAlbumArtURI", () => {
|
||||
const bonobUrl = new URLBuilder("http://bonob.example.com:8080/context?search=yes");
|
||||
const bonobUrl = new URLBuilder(
|
||||
"http://bonob.example.com:8080/context?search=yes"
|
||||
);
|
||||
|
||||
describe("when there is an album coverArt", () => {
|
||||
it("should use it in the image url", () => {
|
||||
@@ -421,10 +520,7 @@ describe("defaultAlbumArtURI", () => {
|
||||
describe("when there is no album coverArt", () => {
|
||||
it("should return a vinly icon image", () => {
|
||||
expect(
|
||||
defaultAlbumArtURI(
|
||||
bonobUrl,
|
||||
anAlbum({ coverArt: undefined })
|
||||
).href()
|
||||
defaultAlbumArtURI(bonobUrl, anAlbum({ coverArt: undefined })).href()
|
||||
).toEqual(
|
||||
"http://bonob.example.com:8080/context/icon/vinyl/size/legacy?search=yes"
|
||||
);
|
||||
@@ -473,6 +569,7 @@ describe("api", () => {
|
||||
removeFromPlaylist: jest.fn(),
|
||||
scrobble: jest.fn(),
|
||||
nowPlaying: jest.fn(),
|
||||
rate: jest.fn(),
|
||||
};
|
||||
const accessTokens = {
|
||||
mint: jest.fn(),
|
||||
@@ -868,6 +965,18 @@ describe("api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Random",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "favouriteAlbums",
|
||||
title: "Favourites",
|
||||
albumArtURI: iconArtURI(bonobUrl, "heart").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "playlists",
|
||||
title: "Playlists",
|
||||
@@ -885,18 +994,6 @@ describe("api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Random",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "starredAlbums",
|
||||
title: "Starred",
|
||||
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "recentlyAdded",
|
||||
title: "Recently added",
|
||||
@@ -955,6 +1052,18 @@ describe("api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Willekeurig",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "favouriteAlbums",
|
||||
title: "Favorieten",
|
||||
albumArtURI: iconArtURI(bonobUrl, "heart").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "playlists",
|
||||
title: "Afspeellijsten",
|
||||
@@ -972,18 +1081,6 @@ describe("api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Willekeurig",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "starredAlbums",
|
||||
title: "Favorieten",
|
||||
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "recentlyAdded",
|
||||
title: "Onlangs toegevoegd",
|
||||
@@ -1568,7 +1665,7 @@ describe("api", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for starred albums", () => {
|
||||
describe("asking for favourite albums", () => {
|
||||
const albums = [rock2, rock1, pop2];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1585,7 +1682,7 @@ describe("api", () => {
|
||||
};
|
||||
|
||||
const result = await ws.getMetadataAsync({
|
||||
id: "starredAlbums",
|
||||
id: "favouriteAlbums",
|
||||
...paging,
|
||||
});
|
||||
|
||||
@@ -2325,42 +2422,90 @@ describe("api", () => {
|
||||
});
|
||||
|
||||
describe("asking for a track", () => {
|
||||
it("should return the track", async () => {
|
||||
const track = aTrack();
|
||||
describe("that has a love", () => {
|
||||
it("should return the track", async () => {
|
||||
const track = aTrack();
|
||||
|
||||
musicLibrary.track.mockResolvedValue(track);
|
||||
musicLibrary.track.mockResolvedValue(track);
|
||||
|
||||
const root = await ws.getExtendedMetadataAsync({
|
||||
id: `track:${track.id}`,
|
||||
});
|
||||
const root = await ws.getExtendedMetadataAsync({
|
||||
id: `track:${track.id}`,
|
||||
});
|
||||
|
||||
expect(root[0]).toEqual({
|
||||
getExtendedMetadataResult: {
|
||||
mediaMetadata: {
|
||||
id: `track:${track.id}`,
|
||||
itemType: "track",
|
||||
title: track.name,
|
||||
mimeType: track.mimeType,
|
||||
trackMetadata: {
|
||||
artistId: `artist:${track.artist.id}`,
|
||||
artist: track.artist.name,
|
||||
albumId: `album:${track.album.id}`,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: `artist:${track.artist.id}`,
|
||||
album: track.album.name,
|
||||
genre: track.genre?.name,
|
||||
genreId: track.genre?.id,
|
||||
duration: track.duration,
|
||||
albumArtURI: defaultAlbumArtURI(
|
||||
bonobUrlWithAccessToken,
|
||||
track
|
||||
).href(),
|
||||
trackNumber: track.number,
|
||||
expect(root[0]).toEqual({
|
||||
getExtendedMetadataResult: {
|
||||
mediaMetadata: {
|
||||
id: `track:${track.id}`,
|
||||
itemType: "track",
|
||||
title: track.name,
|
||||
mimeType: track.mimeType,
|
||||
trackMetadata: {
|
||||
artistId: `artist:${track.artist.id}`,
|
||||
artist: track.artist.name,
|
||||
albumId: `album:${track.album.id}`,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: `artist:${track.artist.id}`,
|
||||
album: track.album.name,
|
||||
genre: track.genre?.name,
|
||||
genreId: track.genre?.id,
|
||||
duration: track.duration,
|
||||
albumArtURI: defaultAlbumArtURI(
|
||||
bonobUrlWithAccessToken,
|
||||
track
|
||||
).href(),
|
||||
trackNumber: track.number,
|
||||
},
|
||||
dynamic: {
|
||||
property: [{ name: "rating", value: `${ratingAsInt(track.rating)}` }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(track.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("that does not have a love", () => {
|
||||
it("should return the track", async () => {
|
||||
const track = aTrack();
|
||||
|
||||
musicLibrary.track.mockResolvedValue(track);
|
||||
|
||||
const root = await ws.getExtendedMetadataAsync({
|
||||
id: `track:${track.id}`,
|
||||
});
|
||||
|
||||
expect(root[0]).toEqual({
|
||||
getExtendedMetadataResult: {
|
||||
mediaMetadata: {
|
||||
id: `track:${track.id}`,
|
||||
itemType: "track",
|
||||
title: track.name,
|
||||
mimeType: track.mimeType,
|
||||
trackMetadata: {
|
||||
artistId: `artist:${track.artist.id}`,
|
||||
artist: track.artist.name,
|
||||
albumId: `album:${track.album.id}`,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: `artist:${track.artist.id}`,
|
||||
album: track.album.name,
|
||||
genre: track.genre?.name,
|
||||
genreId: track.genre?.id,
|
||||
duration: track.duration,
|
||||
albumArtURI: defaultAlbumArtURI(
|
||||
bonobUrlWithAccessToken,
|
||||
track
|
||||
).href(),
|
||||
trackNumber: track.number,
|
||||
},
|
||||
dynamic: {
|
||||
property: [{ name: "rating", value: `${ratingAsInt(track.rating)}` }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(track.id);
|
||||
});
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(track.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2471,7 +2616,7 @@ describe("api", () => {
|
||||
getMediaURIResult: bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/track/${trackId}`,
|
||||
searchParams: { "bat": accessToken }
|
||||
searchParams: { bat: accessToken },
|
||||
})
|
||||
.href(),
|
||||
});
|
||||
@@ -2788,6 +2933,64 @@ describe("api", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rateItem", () => {
|
||||
let ws: Client;
|
||||
|
||||
beforeEach(async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
accessTokens.mint.mockReturnValue(accessToken);
|
||||
|
||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server),
|
||||
});
|
||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||
});
|
||||
|
||||
describe("rating a track with a positive rating value", () => {
|
||||
const trackId = "123";
|
||||
const ratingIntValue = 31;
|
||||
|
||||
it("should give the track a love", async () => {
|
||||
musicLibrary.rate.mockResolvedValue(true);
|
||||
|
||||
const result = await ws.rateItemAsync({
|
||||
id: `track:${trackId}`,
|
||||
rating: ratingIntValue,
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
rateItemResult: { shouldSkip: false },
|
||||
});
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(ratingIntValue));
|
||||
});
|
||||
});
|
||||
|
||||
describe("rating a track with a negative rating value", () => {
|
||||
const trackId = "123";
|
||||
const ratingIntValue = -20;
|
||||
|
||||
it("should give the track a love", async () => {
|
||||
musicLibrary.rate.mockResolvedValue(true);
|
||||
|
||||
const result = await ws.rateItemAsync({
|
||||
id: `track:${trackId}`,
|
||||
rating: ratingIntValue,
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
rateItemResult: { shouldSkip: false },
|
||||
});
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(Math.abs(ratingIntValue)));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("setPlayedSeconds", () => {
|
||||
let ws: Client;
|
||||
|
||||
@@ -2812,7 +3015,7 @@ describe("api", () => {
|
||||
}: {
|
||||
trackId: string;
|
||||
secondsPlayed: number;
|
||||
shouldMarkNowPlaying: boolean,
|
||||
shouldMarkNowPlaying: boolean;
|
||||
}) {
|
||||
it("should scrobble", async () => {
|
||||
musicLibrary.scrobble.mockResolvedValue(true);
|
||||
@@ -2827,7 +3030,7 @@ describe("api", () => {
|
||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId);
|
||||
if(shouldMarkNowPlaying) {
|
||||
if (shouldMarkNowPlaying) {
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
} else {
|
||||
expect(musicLibrary.nowPlaying).not.toHaveBeenCalled();
|
||||
@@ -2842,7 +3045,7 @@ describe("api", () => {
|
||||
}: {
|
||||
trackId: string;
|
||||
secondsPlayed: number;
|
||||
shouldMarkNowPlaying: boolean,
|
||||
shouldMarkNowPlaying: boolean;
|
||||
}) {
|
||||
it("should scrobble", async () => {
|
||||
const result = await ws.setPlayedSecondsAsync({
|
||||
@@ -2855,7 +3058,7 @@ describe("api", () => {
|
||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.scrobble).not.toHaveBeenCalled();
|
||||
if(shouldMarkNowPlaying) {
|
||||
if (shouldMarkNowPlaying) {
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
} else {
|
||||
expect(musicLibrary.nowPlaying).not.toHaveBeenCalled();
|
||||
@@ -2871,23 +3074,43 @@ describe("api", () => {
|
||||
});
|
||||
|
||||
describe("when the seconds played is 30 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 30, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 30,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is > 30 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 90, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 90,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is < 30 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 29, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 29,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 1 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 1, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 1,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 0 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 0, shouldMarkNowPlaying: false });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 0,
|
||||
shouldMarkNowPlaying: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2899,23 +3122,43 @@ describe("api", () => {
|
||||
});
|
||||
|
||||
describe("when the seconds played is 30 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 30, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 30,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is > 30 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 90, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 90,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is < 30 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 29, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 29,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 1 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 1, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 1,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 0 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 0, shouldMarkNowPlaying: false });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 0,
|
||||
shouldMarkNowPlaying: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2927,27 +3170,51 @@ describe("api", () => {
|
||||
});
|
||||
|
||||
describe("when the seconds played is 29 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 30, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 30,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is > 29 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 30, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 30,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 10 seconds", () => {
|
||||
itShouldScroble({ trackId, secondsPlayed: 10, shouldMarkNowPlaying: true });
|
||||
itShouldScroble({
|
||||
trackId,
|
||||
secondsPlayed: 10,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is < 10 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 9, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 9,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 1 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 1, shouldMarkNowPlaying: true });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 1,
|
||||
shouldMarkNowPlaying: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the seconds played is 0 seconds", () => {
|
||||
itShouldNotScroble({ trackId, secondsPlayed: 0, shouldMarkNowPlaying: false });
|
||||
itShouldNotScroble({
|
||||
trackId,
|
||||
secondsPlayed: 0,
|
||||
shouldMarkNowPlaying: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user