mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Scrobble on completion of song if song was listened to
This commit is contained in:
@@ -13,7 +13,7 @@ Currently only a single integration allowing Navidrome to be registered with son
|
|||||||
- Artist Art
|
- Artist Art
|
||||||
- Album Art
|
- Album Art
|
||||||
- View Related Artists via Artist -> '...' -> Menu -> Related Arists
|
- View Related Artists via Artist -> '...' -> Menu -> Related Arists
|
||||||
- Track scrobbling
|
- Now playing & Track Scrobbling
|
||||||
- Auto discovery of sonos devices
|
- Auto discovery of sonos devices
|
||||||
- Discovery of sonos devices using seed IP address
|
- Discovery of sonos devices using seed IP address
|
||||||
- Auto register bonob service with sonos system
|
- Auto register bonob service with sonos system
|
||||||
@@ -109,6 +109,7 @@ BONOB_NAVIDROME_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for cust
|
|||||||
- Within navidrome a new player will be created, 'bonob (username)', so you can configure transcoding specifically for sonos
|
- Within navidrome a new player will be created, 'bonob (username)', so you can configure transcoding specifically for sonos
|
||||||
|
|
||||||
## Implementing a different music source other than navidrome
|
## Implementing a different music source other than navidrome
|
||||||
|
|
||||||
- Implement the MusicService/MusicLibrary interface
|
- Implement the MusicService/MusicLibrary interface
|
||||||
- Startup bonob with your new implementation.
|
- Startup bonob with your new implementation.
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function server(
|
|||||||
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
|
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
|
||||||
(size) =>
|
(size) =>
|
||||||
`<sizeEntry size="${size}" substitution="/art/size/${size}"/>`
|
`<sizeEntry size="${size}" substitution="/art/size/${size}"/>`
|
||||||
)}
|
).join("")}
|
||||||
</imageSizeMap>
|
</imageSizeMap>
|
||||||
</Match>
|
</Match>
|
||||||
</PresentationMap>
|
</PresentationMap>
|
||||||
|
|||||||
58
src/smapi.ts
58
src/smapi.ts
@@ -242,6 +242,10 @@ export const album = (
|
|||||||
title: album.name,
|
title: album.name,
|
||||||
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
|
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
|
// defaults
|
||||||
|
// canScroll: false,
|
||||||
|
// canEnumerate: true,
|
||||||
|
// canAddToFavorites: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const track = (
|
export const track = (
|
||||||
@@ -487,6 +491,26 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
case "album":
|
||||||
|
return musicLibrary.album(typeId).then((it) => ({
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
mediaCollection: {
|
||||||
|
attributes: {
|
||||||
|
readOnly: true,
|
||||||
|
userContent: false,
|
||||||
|
renameable: false,
|
||||||
|
},
|
||||||
|
...album(webAddress, accessToken, it),
|
||||||
|
},
|
||||||
|
// <mediaCollection readonly="true">
|
||||||
|
// </mediaCollection>
|
||||||
|
// <relatedText>
|
||||||
|
// <id>AL:123456</id>
|
||||||
|
// <type>ALBUM_NOTES</type>
|
||||||
|
// </relatedText>
|
||||||
|
// </getExtendedMetadataResult>
|
||||||
|
},
|
||||||
|
}));
|
||||||
default:
|
default:
|
||||||
throw `Unsupported getExtendedMetadata id=${id}`;
|
throw `Unsupported getExtendedMetadata id=${id}`;
|
||||||
}
|
}
|
||||||
@@ -784,10 +808,42 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((_) => ({ removeFromContainerResult: { updateId: "" } })),
|
.then((_) => ({ removeFromContainerResult: { updateId: "" } })),
|
||||||
|
setPlayedSeconds: async (
|
||||||
|
{ id, seconds }: { id: string; seconds: string },
|
||||||
|
_,
|
||||||
|
headers?: SoapyHeaders
|
||||||
|
) =>
|
||||||
|
auth(musicService, accessTokens, headers)
|
||||||
|
.then(splitId(id))
|
||||||
|
.then(({ musicLibrary, type, typeId }) => {
|
||||||
|
switch (type) {
|
||||||
|
case "track":
|
||||||
|
musicLibrary.track(typeId).then(({ duration }) => {
|
||||||
|
if (
|
||||||
|
(duration < 30 && +seconds >= 10) ||
|
||||||
|
(duration >= 30 && +seconds >= 30)
|
||||||
|
) {
|
||||||
|
musicLibrary.scrobble(typeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.info("Unsupported scrobble", { id, seconds });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((_) => ({
|
||||||
|
setPlayedSecondsResult: {},
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
readFileSync(WSDL_FILE, "utf8")
|
readFileSync(WSDL_FILE, "utf8"),
|
||||||
|
(err: any, res: any) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error("BOOOOM", { err, res });
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
soapyService.log = (type, data) => {
|
soapyService.log = (type, data) => {
|
||||||
|
|||||||
30
src/sonos.ts
30
src/sonos.ts
@@ -5,10 +5,12 @@ import { MusicService } from "@svrooij/sonos/lib/services";
|
|||||||
import { head } from "underscore";
|
import { head } from "underscore";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
|
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
|
||||||
import qs from "querystring"
|
import qs from "querystring";
|
||||||
|
|
||||||
export const PRESENTATION_AND_STRINGS_VERSION = "18";
|
export const PRESENTATION_AND_STRINGS_VERSION = "18";
|
||||||
|
|
||||||
|
// NOTE: manifest requires https for the URL,
|
||||||
|
// otherwise you will get an error trying to register
|
||||||
export type Capability =
|
export type Capability =
|
||||||
| "search"
|
| "search"
|
||||||
| "trFavorites"
|
| "trFavorites"
|
||||||
@@ -16,7 +18,9 @@ export type Capability =
|
|||||||
| "ucPlaylists"
|
| "ucPlaylists"
|
||||||
| "extendedMD"
|
| "extendedMD"
|
||||||
| "contextHeaders"
|
| "contextHeaders"
|
||||||
| "authorizationHeader";
|
| "authorizationHeader"
|
||||||
|
| "logging"
|
||||||
|
| "manifest";
|
||||||
|
|
||||||
export const BONOB_CAPABILITIES: Capability[] = [
|
export const BONOB_CAPABILITIES: Capability[] = [
|
||||||
"search",
|
"search",
|
||||||
@@ -24,6 +28,7 @@ export const BONOB_CAPABILITIES: Capability[] = [
|
|||||||
// "alFavorites",
|
// "alFavorites",
|
||||||
"ucPlaylists",
|
"ucPlaylists",
|
||||||
"extendedMD",
|
"extendedMD",
|
||||||
|
"logging",
|
||||||
];
|
];
|
||||||
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
@@ -38,13 +43,13 @@ export type Service = {
|
|||||||
sid: number;
|
sid: number;
|
||||||
uri: string;
|
uri: string;
|
||||||
secureUri: string;
|
secureUri: string;
|
||||||
strings: { uri?: string; version?: string };
|
strings?: { uri?: string; version?: string };
|
||||||
presentation: { uri?: string; version?: string };
|
presentation?: { uri?: string; version?: string };
|
||||||
pollInterval?: number;
|
pollInterval?: number;
|
||||||
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId";
|
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId";
|
||||||
};
|
};
|
||||||
|
|
||||||
const stripTailingSlash = (url: string) =>
|
export const stripTailingSlash = (url: string) =>
|
||||||
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
||||||
|
|
||||||
export const bonobService = (
|
export const bonobService = (
|
||||||
@@ -113,12 +118,10 @@ export const asCustomdForm = (csrfToken: string, service: Service) => ({
|
|||||||
secureUri: service.secureUri,
|
secureUri: service.secureUri,
|
||||||
pollInterval: `${service.pollInterval || 1200}`,
|
pollInterval: `${service.pollInterval || 1200}`,
|
||||||
authType: service.authType,
|
authType: service.authType,
|
||||||
stringsVersion: service.strings.version || "",
|
stringsVersion: service.strings?.version || "0",
|
||||||
stringsUri: service.strings.uri || "",
|
stringsUri: service.strings?.uri || "",
|
||||||
presentationMapVersion: service.presentation.version || "",
|
presentationMapVersion: service.presentation?.version || "0",
|
||||||
presentationMapUri: service.presentation.uri || "",
|
presentationMapUri: service.presentation?.uri || "",
|
||||||
manifestVersion: "0",
|
|
||||||
manifestUri: "",
|
|
||||||
containerType: "MService",
|
containerType: "MService",
|
||||||
caps: BONOB_CAPABILITIES,
|
caps: BONOB_CAPABILITIES,
|
||||||
});
|
});
|
||||||
@@ -193,9 +196,10 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const customdForm = asCustomdForm(csrfToken, service);
|
||||||
|
logger.info(`Registering with sonos @ ${customd}`, { customdForm });
|
||||||
return axios
|
return axios
|
||||||
.post(customd, new URLSearchParams(qs.stringify(asCustomdForm(csrfToken, service))), {
|
.post(customd, new URLSearchParams(qs.stringify(customdForm)), {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ class SonosDriver {
|
|||||||
expect(this.service.authType).toEqual("AppLink");
|
expect(this.service.authType).toEqual("AppLink");
|
||||||
|
|
||||||
await request(this.server)
|
await request(this.server)
|
||||||
.get(this.stripServiceRoot(this.service.strings.uri!))
|
.get(this.stripServiceRoot(this.service.strings!.uri!))
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
await request(this.server)
|
await request(this.server)
|
||||||
.get(this.stripServiceRoot(this.service.presentation.uri!))
|
.get(this.stripServiceRoot(this.service.presentation!.uri!))
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const client = await createClientAsync(`${this.service.uri}?wsdl`, {
|
const client = await createClientAsync(`${this.service.uri}?wsdl`, {
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ describe("api", () => {
|
|||||||
genres: jest.fn(),
|
genres: jest.fn(),
|
||||||
playlists: jest.fn(),
|
playlists: jest.fn(),
|
||||||
playlist: jest.fn(),
|
playlist: jest.fn(),
|
||||||
|
album: jest.fn(),
|
||||||
albums: jest.fn(),
|
albums: jest.fn(),
|
||||||
tracks: jest.fn(),
|
tracks: jest.fn(),
|
||||||
track: jest.fn(),
|
track: jest.fn(),
|
||||||
@@ -301,6 +302,7 @@ describe("api", () => {
|
|||||||
addToPlaylist: jest.fn(),
|
addToPlaylist: jest.fn(),
|
||||||
deletePlaylist: jest.fn(),
|
deletePlaylist: jest.fn(),
|
||||||
removeFromPlaylist: jest.fn(),
|
removeFromPlaylist: jest.fn(),
|
||||||
|
scrobble: jest.fn(),
|
||||||
};
|
};
|
||||||
const accessTokens = {
|
const accessTokens = {
|
||||||
mint: jest.fn(),
|
mint: jest.fn(),
|
||||||
@@ -1939,7 +1941,7 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("asking for a track", () => {
|
describe("asking for a track", () => {
|
||||||
it("should return the albums", async () => {
|
it("should return the track", async () => {
|
||||||
const track = aTrack();
|
const track = aTrack();
|
||||||
|
|
||||||
musicLibrary.track.mockResolvedValue(track);
|
musicLibrary.track.mockResolvedValue(track);
|
||||||
@@ -1975,6 +1977,38 @@ describe("api", () => {
|
|||||||
expect(musicLibrary.track).toHaveBeenCalledWith(track.id);
|
expect(musicLibrary.track).toHaveBeenCalledWith(track.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("asking for an album", () => {
|
||||||
|
it("should return the album", async () => {
|
||||||
|
const album = anAlbum();
|
||||||
|
|
||||||
|
musicLibrary.album.mockResolvedValue(album);
|
||||||
|
|
||||||
|
const root = await ws.getExtendedMetadataAsync({
|
||||||
|
id: `album:${album.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(root[0]).toEqual({
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
mediaCollection: {
|
||||||
|
attributes: {
|
||||||
|
readOnly: "true",
|
||||||
|
userContent: "false",
|
||||||
|
renameable: "false",
|
||||||
|
},
|
||||||
|
itemType: "album",
|
||||||
|
id: `album:${album.id}`,
|
||||||
|
title: album.name,
|
||||||
|
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, album),
|
||||||
|
canPlay: true,
|
||||||
|
artistId: album.artistId,
|
||||||
|
artist: album.artistName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(musicLibrary.album).toHaveBeenCalledWith(album.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2161,7 +2195,10 @@ describe("api", () => {
|
|||||||
const idOfNewPlaylist = uuid();
|
const idOfNewPlaylist = uuid();
|
||||||
|
|
||||||
it("should create a playlist", async () => {
|
it("should create a playlist", async () => {
|
||||||
musicLibrary.createPlaylist.mockResolvedValue({ id: idOfNewPlaylist, name: title });
|
musicLibrary.createPlaylist.mockResolvedValue({
|
||||||
|
id: idOfNewPlaylist,
|
||||||
|
name: title,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await ws.createContainerAsync({
|
const result = await ws.createContainerAsync({
|
||||||
title,
|
title,
|
||||||
@@ -2181,16 +2218,19 @@ describe("api", () => {
|
|||||||
|
|
||||||
describe("with a title and a seed track", () => {
|
describe("with a title and a seed track", () => {
|
||||||
const title = "aNewPlaylist2";
|
const title = "aNewPlaylist2";
|
||||||
const trackId = 'track123';
|
const trackId = "track123";
|
||||||
const idOfNewPlaylist = 'playlistId';
|
const idOfNewPlaylist = "playlistId";
|
||||||
|
|
||||||
it("should create a playlist with the track", async () => {
|
it("should create a playlist with the track", async () => {
|
||||||
musicLibrary.createPlaylist.mockResolvedValue({ id: idOfNewPlaylist, name: title });
|
musicLibrary.createPlaylist.mockResolvedValue({
|
||||||
|
id: idOfNewPlaylist,
|
||||||
|
name: title,
|
||||||
|
});
|
||||||
musicLibrary.addToPlaylist.mockResolvedValue(true);
|
musicLibrary.addToPlaylist.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await ws.createContainerAsync({
|
const result = await ws.createContainerAsync({
|
||||||
title,
|
title,
|
||||||
seedId: `track:${trackId}`
|
seedId: `track:${trackId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result[0]).toEqual({
|
expect(result[0]).toEqual({
|
||||||
@@ -2202,9 +2242,11 @@ describe("api", () => {
|
|||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title);
|
expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title);
|
||||||
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(idOfNewPlaylist, trackId);
|
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(
|
||||||
|
idOfNewPlaylist,
|
||||||
|
trackId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2238,102 +2280,271 @@ describe("api", () => {
|
|||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.deletePlaylist).toHaveBeenCalledWith(id);
|
expect(musicLibrary.deletePlaylist).toHaveBeenCalledWith(id);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("addToContainer", () => {
|
describe("addToContainer", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
const authToken = `authToken-${uuid()}`;
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
const accessToken = `accessToken-${uuid()}`;
|
||||||
const trackId = "track123";
|
const trackId = "track123";
|
||||||
const playlistId = "parent123";
|
const playlistId = "parent123";
|
||||||
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
accessTokens.mint.mockReturnValue(accessToken);
|
accessTokens.mint.mockReturnValue(accessToken);
|
||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server, rootUrl),
|
||||||
});
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
|
||||||
});
|
});
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
it("should delete the playlist", async () => {
|
});
|
||||||
musicLibrary.addToPlaylist.mockResolvedValue(true);
|
|
||||||
|
it("should delete the playlist", async () => {
|
||||||
const result = await ws.addToContainerAsync({
|
musicLibrary.addToPlaylist.mockResolvedValue(true);
|
||||||
id: `track:${trackId}`,
|
|
||||||
parentId: `parent:${playlistId}`
|
const result = await ws.addToContainerAsync({
|
||||||
|
id: `track:${trackId}`,
|
||||||
|
parentId: `parent:${playlistId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({ addToContainerResult: { updateId: null } });
|
||||||
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(
|
||||||
|
playlistId,
|
||||||
|
trackId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("removeFromContainer", () => {
|
||||||
|
const authToken = `authToken-${uuid()}`;
|
||||||
|
const accessToken = `accessToken-${uuid()}`;
|
||||||
|
|
||||||
|
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, rootUrl),
|
||||||
|
});
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("removing tracks from a playlist", () => {
|
||||||
|
const playlistId = "parent123";
|
||||||
|
|
||||||
|
it("should remove the track from playlist", async () => {
|
||||||
|
musicLibrary.removeFromPlaylist.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await ws.removeFromContainerAsync({
|
||||||
|
id: `playlist:${playlistId}`,
|
||||||
|
indices: `1,6,9`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
removeFromContainerResult: { updateId: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result[0]).toEqual({ addToContainerResult: { updateId: null } });
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(playlistId, trackId);
|
expect(musicLibrary.removeFromPlaylist).toHaveBeenCalledWith(
|
||||||
|
playlistId,
|
||||||
|
[1, 6, 9]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("removeFromContainer", () => {
|
describe("removing a playlist", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
const playlist1 = aPlaylist({ id: "p1" });
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
const playlist2 = aPlaylist({ id: "p2" });
|
||||||
|
const playlist3 = aPlaylist({ id: "p3" });
|
||||||
let ws: Client;
|
const playlist4 = aPlaylist({ id: "p4" });
|
||||||
|
const playlist5 = aPlaylist({ id: "p5" });
|
||||||
beforeEach(async () => {
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
|
||||||
accessTokens.mint.mockReturnValue(accessToken);
|
|
||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
|
||||||
endpoint: service.uri,
|
|
||||||
httpClient: supersoap(server, rootUrl),
|
|
||||||
});
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("removing tracks from a playlist", () => {
|
it("should delete the playlist", async () => {
|
||||||
const playlistId = "parent123";
|
musicLibrary.playlists.mockResolvedValue([
|
||||||
|
playlist1,
|
||||||
it("should remove the track from playlist", async () => {
|
playlist2,
|
||||||
musicLibrary.removeFromPlaylist.mockResolvedValue(true);
|
playlist3,
|
||||||
|
playlist4,
|
||||||
const result = await ws.removeFromContainerAsync({
|
playlist5,
|
||||||
id: `playlist:${playlistId}`,
|
]);
|
||||||
indices: `1,6,9`
|
musicLibrary.deletePlaylist.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await ws.removeFromContainerAsync({
|
||||||
|
id: `playlists`,
|
||||||
|
indices: `0,2,4`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
removeFromContainerResult: { updateId: null },
|
||||||
|
});
|
||||||
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(musicLibrary.deletePlaylist).toHaveBeenCalledTimes(3);
|
||||||
|
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
playlist1.id
|
||||||
|
);
|
||||||
|
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
playlist3.id
|
||||||
|
);
|
||||||
|
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
playlist5.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setPlayedSeconds", () => {
|
||||||
|
const authToken = `authToken-${uuid()}`;
|
||||||
|
const accessToken = `accessToken-${uuid()}`;
|
||||||
|
|
||||||
|
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, rootUrl),
|
||||||
|
});
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when id is for a track", () => {
|
||||||
|
const trackId = "123456";
|
||||||
|
|
||||||
|
function itShouldScroble({
|
||||||
|
trackId,
|
||||||
|
secondsPlayed,
|
||||||
|
}: {
|
||||||
|
trackId: string;
|
||||||
|
secondsPlayed: number;
|
||||||
|
}) {
|
||||||
|
it("should scrobble", async () => {
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await ws.setPlayedSecondsAsync({
|
||||||
|
id: `track:${trackId}`,
|
||||||
|
seconds: `${secondsPlayed}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result[0]).toEqual({ removeFromContainerResult: { updateId: null } });
|
expect(result[0]).toEqual({ setPlayedSecondsResult: null });
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.removeFromPlaylist).toHaveBeenCalledWith(playlistId, [1,6,9]);
|
expect(musicLibrary.track).toHaveBeenCalledWith(trackId);
|
||||||
|
expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function itShouldNotScroble({
|
||||||
|
trackId,
|
||||||
|
secondsPlayed,
|
||||||
|
}: {
|
||||||
|
trackId: string;
|
||||||
|
secondsPlayed: number;
|
||||||
|
}) {
|
||||||
|
it("should scrobble", async () => {
|
||||||
|
const result = await ws.setPlayedSecondsAsync({
|
||||||
|
id: `track:${trackId}`,
|
||||||
|
seconds: `${secondsPlayed}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({ setPlayedSecondsResult: null });
|
||||||
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(musicLibrary.track).toHaveBeenCalledWith(trackId);
|
||||||
|
expect(musicLibrary.scrobble).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("when the track length is 30 seconds", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
musicLibrary.track.mockResolvedValue(
|
||||||
|
aTrack({ id: trackId, duration: 30 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is 30 seconds", () => {
|
||||||
|
itShouldScroble({ trackId, secondsPlayed: 30 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is > 30 seconds", () => {
|
||||||
|
itShouldScroble({ trackId, secondsPlayed: 90 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is < 30 seconds", () => {
|
||||||
|
itShouldNotScroble({ trackId, secondsPlayed: 29 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("removing a playlist", () => {
|
describe("when the track length is > 30 seconds", () => {
|
||||||
const playlist1 = aPlaylist({ id: 'p1' });
|
beforeEach(() => {
|
||||||
const playlist2 = aPlaylist({ id: 'p2' });
|
musicLibrary.track.mockResolvedValue(
|
||||||
const playlist3 = aPlaylist({ id: 'p3' });
|
aTrack({ id: trackId, duration: 31 })
|
||||||
const playlist4 = aPlaylist({ id: 'p4' });
|
);
|
||||||
const playlist5 = aPlaylist({ id: 'p5' });
|
});
|
||||||
|
|
||||||
it("should delete the playlist", async () => {
|
describe("when the played length is 30 seconds", () => {
|
||||||
musicLibrary.playlists.mockResolvedValue([playlist1, playlist2, playlist3, playlist4, playlist5]);
|
itShouldScroble({ trackId, secondsPlayed: 30 });
|
||||||
musicLibrary.deletePlaylist.mockResolvedValue(true);
|
});
|
||||||
|
|
||||||
const result = await ws.removeFromContainerAsync({
|
describe("when the played length is > 30 seconds", () => {
|
||||||
id: `playlists`,
|
itShouldScroble({ trackId, secondsPlayed: 90 });
|
||||||
indices: `0,2,4`
|
});
|
||||||
});
|
|
||||||
|
describe("when the played length is < 30 seconds", () => {
|
||||||
expect(result[0]).toEqual({ removeFromContainerResult: { updateId: null } });
|
itShouldNotScroble({ trackId, secondsPlayed: 29 });
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
});
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
});
|
||||||
expect(musicLibrary.deletePlaylist).toHaveBeenCalledTimes(3);
|
|
||||||
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(1, playlist1.id);
|
describe("when the track length is 29 seconds", () => {
|
||||||
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(2, playlist3.id);
|
beforeEach(() => {
|
||||||
expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith(3, playlist5.id);
|
musicLibrary.track.mockResolvedValue(
|
||||||
|
aTrack({ id: trackId, duration: 29 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is 29 seconds", () => {
|
||||||
|
itShouldScroble({ trackId, secondsPlayed: 30 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is > 29 seconds", () => {
|
||||||
|
itShouldScroble({ trackId, secondsPlayed: 30 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is 10 seconds", () => {
|
||||||
|
itShouldScroble({ trackId, secondsPlayed: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the played length is < 10 seconds", () => {
|
||||||
|
itShouldNotScroble({ trackId, secondsPlayed: 9 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
describe("when the id is for something that isnt a track", () => {
|
||||||
|
it("should not scrobble", async () => {
|
||||||
|
const result = await ws.setPlayedSecondsAsync({
|
||||||
|
id: `album:666`,
|
||||||
|
seconds: "100",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({ setPlayedSecondsResult: null });
|
||||||
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
|
expect(musicLibrary.scrobble).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -207,8 +207,6 @@ describe("sonos", () => {
|
|||||||
stringsUri: "http://strings.example.com",
|
stringsUri: "http://strings.example.com",
|
||||||
presentationMapVersion: "27",
|
presentationMapVersion: "27",
|
||||||
presentationMapUri: "http://presentation.example.com",
|
presentationMapUri: "http://presentation.example.com",
|
||||||
manifestVersion: "0",
|
|
||||||
manifestUri: "",
|
|
||||||
containerType: "MService",
|
containerType: "MService",
|
||||||
caps: BONOB_CAPABILITIES
|
caps: BONOB_CAPABILITIES
|
||||||
});
|
});
|
||||||
@@ -230,9 +228,9 @@ describe("sonos", () => {
|
|||||||
});
|
});
|
||||||
const form = asCustomdForm(uuid(), service)
|
const form = asCustomdForm(uuid(), service)
|
||||||
expect(form.stringsUri).toEqual("");
|
expect(form.stringsUri).toEqual("");
|
||||||
expect(form.stringsVersion).toEqual("");
|
expect(form.stringsVersion).toEqual("0");
|
||||||
expect(form.presentationMapUri).toEqual("");
|
expect(form.presentationMapUri).toEqual("");
|
||||||
expect(form.presentationMapVersion).toEqual("");
|
expect(form.presentationMapVersion).toEqual("0");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user