Scrobble on completion of song if song was listened to

This commit is contained in:
simojenki
2021-07-07 17:28:26 +10:00
parent eec3313587
commit f7a1b3f52c
7 changed files with 378 additions and 108 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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",
}, },

View File

@@ -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`, {

View File

@@ -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();
});
});
});
}); });
}); });

View File

@@ -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");
}); });
}); });
}); });