diff --git a/package.json b/package.json
index cb6f250..8bd0ab9 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,9 @@
"supertest": "^6.1.3",
"ts-jest": "^26.4.4",
"ts-mockito": "^2.6.1",
- "ts-node": "^9.1.1"
+ "ts-node": "^9.1.1",
+ "xmldom-ts": "^0.3.1",
+ "xpath-ts": "^1.3.13"
},
"scripts": {
"build": "tsc",
diff --git a/src/server.ts b/src/server.ts
index 10e9da1..2b31589 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -7,6 +7,7 @@ import {
SOAP_PATH,
STRINGS_ROUTE,
PRESENTATION_MAP_ROUTE,
+ SONOS_RECOMMENDED_IMAGE_SIZES,
LOGIN_ROUTE,
} from "./smapi";
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
@@ -101,19 +102,29 @@ function server(
res.type("application/xml").send(`
- Linking sonos with bonob
- string2
+ Linking sonos with ${bonobService.name}
- Linking sonos with bonob fr
- string2 fr
+ Lier les sonos à la ${bonobService.name}
`);
});
app.get(PRESENTATION_MAP_ROUTE, (_, res) => {
- res.send("");
+ res.type("application/xml").send(`
+
+
+
+
+ ${SONOS_RECOMMENDED_IMAGE_SIZES.map(
+ (size) =>
+ ``
+ )}
+
+
+
+ `);
});
app.get("/stream/track/:id", async (req, res) => {
@@ -138,7 +149,7 @@ function server(
}
});
- app.get("/album/:albumId/art", (req, res) => {
+ app.get("/album/:albumId/art/size/:size", (req, res) => {
const authToken = accessTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
);
@@ -147,7 +158,12 @@ function server(
} else {
return musicService
.login(authToken)
- .then((it) => it.coverArt(req.params["albumId"]!, 200))
+ .then((it) =>
+ it.coverArt(
+ req.params["albumId"]!,
+ Number.parseInt(req.params["size"]!)
+ )
+ )
.then((coverArt) => {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
diff --git a/src/smapi.ts b/src/smapi.ts
index b30e9de..8fb7cd5 100644
--- a/src/smapi.ts
+++ b/src/smapi.ts
@@ -20,6 +20,21 @@ export const LOGIN_ROUTE = "/login";
export const SOAP_PATH = "/ws/sonos";
export const STRINGS_ROUTE = "/sonos/strings.xml";
export const PRESENTATION_MAP_ROUTE = "/sonos/presentationMap.xml";
+export const SONOS_RECOMMENDED_IMAGE_SIZES = [
+ "60",
+ "80",
+ "120",
+ "180",
+ "192",
+ "200",
+ "230",
+ "300",
+ "600",
+ "640",
+ "750",
+ "1242",
+ "1500",
+];
const WSDL_FILE = path.resolve(
__dirname,
@@ -203,7 +218,7 @@ const album = (
itemType: "album",
id: `album:${album.id}`,
title: album.name,
- albumArtURI: `${webAddress}/album/${album.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`,
+ albumArtURI: `${webAddress}/album/${album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`,
});
const track = (track: Track) => ({
diff --git a/src/sonos.ts b/src/sonos.ts
index da7ecc5..c8271d5 100644
--- a/src/sonos.ts
+++ b/src/sonos.ts
@@ -4,9 +4,11 @@ import { parse } from "node-html-parser";
import { MusicService } from "@svrooij/sonos/lib/services";
import { head } from "underscore";
import logger from "./logger";
-import STRINGS from "./strings";
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
+export const STRINGS_VERSION = "2";
+export const PRESENTATION_MAP_VERSION = "7";
+
export type Device = {
name: string;
group: string;
@@ -40,11 +42,11 @@ export const bonobService = (
secureUri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
strings: {
uri: `${stripTailingSlash(bonobRoot)}${STRINGS_ROUTE}`,
- version: STRINGS.version,
+ version: STRINGS_VERSION,
},
presentation: {
uri: `${stripTailingSlash(bonobRoot)}${PRESENTATION_MAP_ROUTE}`,
- version: "1",
+ version: PRESENTATION_MAP_VERSION,
},
pollInterval: 1200,
authType,
diff --git a/src/strings.ts b/src/strings.ts
deleted file mode 100644
index 7d395b5..0000000
--- a/src/strings.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-const STRINGS = {
- version: "1",
- values: {
- "foo": "bar"
- }
-}
-
-export default STRINGS;
\ No newline at end of file
diff --git a/tests/server.test.ts b/tests/server.test.ts
index cf59ef6..7496d84 100644
--- a/tests/server.test.ts
+++ b/tests/server.test.ts
@@ -299,7 +299,6 @@ describe("server", () => {
expect(res.header["content-type"]).toEqual(
stream.headers["content-type"]
);
- // expect(res.header["content-length"]).toEqual(stream.headers["content-length"]);
expect(res.header["accept-ranges"]).toEqual(
stream.headers["accept-ranges"]
);
@@ -396,7 +395,7 @@ describe("server", () => {
});
});
- describe("/album/:albumId/art", () => {
+ describe("/album/:albumId/art/size", () => {
const musicService = {
login: jest.fn(),
};
@@ -425,7 +424,7 @@ describe("server", () => {
describe("when there is no access-token", () => {
it("should return a 401", async () => {
- const res = await request(server).get(`/album/123/art`);
+ const res = await request(server).get(`/album/123/art/size/180`);
expect(res.status).toEqual(401);
});
@@ -436,7 +435,7 @@ describe("server", () => {
now = now.add(1, "day");
const res = await request(server).get(
- `/album/123/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
+ `/album/123/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
);
expect(res.status).toEqual(401);
@@ -457,7 +456,7 @@ describe("server", () => {
const res = await request(server)
.get(
- `/album/${albumId}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
+ `/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -465,7 +464,7 @@ describe("server", () => {
expect(res.header["content-type"]).toEqual(coverArt.contentType);
expect(musicService.login).toHaveBeenCalledWith(authToken);
- expect(musicLibrary.coverArt).toHaveBeenCalledWith(albumId, 200);
+ expect(musicLibrary.coverArt).toHaveBeenCalledWith(albumId, 180);
});
});
});
diff --git a/tests/smapi.test.ts b/tests/smapi.test.ts
index a060b1a..5bc901f 100644
--- a/tests/smapi.test.ts
+++ b/tests/smapi.test.ts
@@ -1,9 +1,11 @@
import crypto from "crypto";
import request from "supertest";
import { Client, createClientAsync } from "soap";
-import X2JS from "x2js";
import { v4 as uuid } from "uuid";
+import { DOMParserImpl } from "xmldom-ts";
+import * as xpath from "xpath-ts";
+
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
import makeServer, { BONOB_ACCESS_TOKEN_HEADER } from "../src/server";
import { bonobService, SONOS_DISABLED } from "../src/sonos";
@@ -12,6 +14,8 @@ import {
LOGIN_ROUTE,
getMetadataResult,
getMetadataResult2,
+ PRESENTATION_MAP_ROUTE,
+ SONOS_RECOMMENDED_IMAGE_SIZES,
} from "../src/smapi";
import {
@@ -27,27 +31,62 @@ import supersoap from "./supersoap";
import { AuthSuccess } from "../src/music_service";
import { AccessTokens } from "../src/access_tokens";
-describe("service config", () => {
- describe("strings.xml", () => {
- const server = makeServer(
- SONOS_DISABLED,
- aService(),
- "http://localhost:1234",
- new InMemoryMusicService()
- );
+const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
+describe("service config", () => {
+ const server = makeServer(
+ SONOS_DISABLED,
+ aService({ name: "music land" }),
+ "http://localhost:1234",
+ new InMemoryMusicService()
+ );
+
+ describe(STRINGS_ROUTE, () => {
it("should return xml for the strings", async () => {
const res = await request(server).get(STRINGS_ROUTE).send();
expect(res.status).toEqual(200);
- const strings: any = new X2JS({
- arrayAccessFormPaths: ["stringtables", "stringtables.stringtable"],
- }).xml2js(res.text);
-
- expect(strings.stringtables.stringtable[0].string[0]._stringId).toEqual(
- "AppLinkMessage"
+ // 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 sonosString = (id: string, lang: string) =>
+ xpath.select(
+ `string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
+ xml
+ );
+
+ expect(sonosString("AppLinkMessage", "en-US")).toEqual(
+ "Linking sonos with music land"
+ );
+ expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
+ "Lier les sonos à la music land"
+ );
+ });
+ });
+
+ describe(PRESENTATION_MAP_ROUTE, () => {
+ it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
+ const res = await request(server).get(PRESENTATION_MAP_ROUTE).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 imageSizeMap = (size: string) =>
+ xpath.select(
+ `string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
+ xml
+ );
+
+ SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
+ expect(imageSizeMap(size)).toEqual(`/art/size/${size}`);
+ });
});
});
});
@@ -467,7 +506,11 @@ describe("api", () => {
itemType: "album",
id: `album:${it.id}`,
title: it.name,
- albumArtURI: `${rootUrl}/album/${it.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(token.authToken)}`,
+ albumArtURI: `${rootUrl}/album/${
+ it.id
+ }/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(
+ token.authToken
+ )}`,
})),
index: 0,
total: artistWithManyAlbums.albums.length,
@@ -492,7 +535,11 @@ describe("api", () => {
itemType: "album",
id: `album:${it.id}`,
title: it.name,
- albumArtURI: `${rootUrl}/album/${it.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(token.authToken)}`,
+ albumArtURI: `${rootUrl}/album/${
+ it.id
+ }/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(
+ token.authToken
+ )}`,
})),
index: 2,
total: artistWithManyAlbums.albums.length,
@@ -622,7 +669,11 @@ describe("api", () => {
itemType: "album",
id: `album:${it.id}`,
title: it.name,
- albumArtURI: `${rootUrl}/album/${it.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(token.authToken)}`,
+ albumArtURI: `${rootUrl}/album/${
+ it.id
+ }/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(
+ token.authToken
+ )}`,
})),
index: 0,
total: 6,
@@ -648,7 +699,11 @@ describe("api", () => {
itemType: "album",
id: `album:${it.id}`,
title: it.name,
- albumArtURI: `${rootUrl}/album/${it.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(token.authToken)}`,
+ albumArtURI: `${rootUrl}/album/${
+ it.id
+ }/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessTokens.mint(
+ token.authToken
+ )}`,
})),
index: 2,
total: 6,
diff --git a/tests/sonos.test.ts b/tests/sonos.test.ts
index 98879ed..8be66a4 100644
--- a/tests/sonos.test.ts
+++ b/tests/sonos.test.ts
@@ -19,6 +19,8 @@ import sonos, {
asCustomdForm,
bonobService,
Service,
+ STRINGS_VERSION,
+ PRESENTATION_MAP_VERSION,
} from "../src/sonos";
import { aSonosDevice, aService } from "./builders";
@@ -115,11 +117,11 @@ describe("sonos", () => {
secureUri: `http://bonob.example.com/ws/sonos`,
strings: {
uri: `http://bonob.example.com/sonos/strings.xml`,
- version: "1",
+ version: STRINGS_VERSION,
},
presentation: {
uri: `http://bonob.example.com/sonos/presentationMap.xml`,
- version: "1",
+ version: PRESENTATION_MAP_VERSION,
},
pollInterval: 1200,
authType: "AppLink",
@@ -138,11 +140,11 @@ describe("sonos", () => {
secureUri: `http://bonob.example.com/ws/sonos`,
strings: {
uri: `http://bonob.example.com/sonos/strings.xml`,
- version: "1",
+ version: STRINGS_VERSION,
},
presentation: {
uri: `http://bonob.example.com/sonos/presentationMap.xml`,
- version: "1",
+ version: PRESENTATION_MAP_VERSION,
},
pollInterval: 1200,
authType: "AppLink",
@@ -161,11 +163,11 @@ describe("sonos", () => {
secureUri: `http://bonob.example.com/ws/sonos`,
strings: {
uri: `http://bonob.example.com/sonos/strings.xml`,
- version: "1",
+ version: STRINGS_VERSION,
},
presentation: {
uri: `http://bonob.example.com/sonos/presentationMap.xml`,
- version: "1",
+ version: PRESENTATION_MAP_VERSION,
},
pollInterval: 1200,
authType: "DeviceLink",
diff --git a/yarn.lock b/yarn.lock
index f07fa44..d1b6c08 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5023,6 +5023,11 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+xmldom-ts@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/xmldom-ts/-/xmldom-ts-0.3.1.tgz#a70df029e44e9af3c03ba22d88f174a953830091"
+ integrity sha512-dmEBAK3Msm+BPVZOiwhXCyM0/q3BeiI4eoAPj2Us1nDhsPPhePtZ5RkgEdngNQQFp3j6QFKMLHlBIRUxdpomcQ==
+
xmldom@0.1.27:
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
@@ -5033,6 +5038,11 @@ xmldom@^0.1.19:
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
+xpath-ts@^1.3.13:
+ version "1.3.13"
+ resolved "https://registry.yarnpkg.com/xpath-ts/-/xpath-ts-1.3.13.tgz#abca4f15dd7010161acf5b9cd01566f7b8d9541f"
+ integrity sha512-eNVXzDWbCV9KEB6fGNQ3qHFGC9PWBH7y2h13vZ+CMPNqOTZ+fgYTG4Sb0p5bVHiAwZrzgE6/tx987003P3dYpA==
+
xpath@0.0.27:
version "0.0.27"
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"