mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to browser related artists
This commit is contained in:
168
src/smapi.ts
168
src/smapi.ts
@@ -7,6 +7,7 @@ import logger from "./logger";
|
|||||||
|
|
||||||
import { LinkCodes } from "./link_codes";
|
import { LinkCodes } from "./link_codes";
|
||||||
import {
|
import {
|
||||||
|
Album,
|
||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
ArtistSummary,
|
ArtistSummary,
|
||||||
MusicLibrary,
|
MusicLibrary,
|
||||||
@@ -77,52 +78,30 @@ export type MediaCollection = {
|
|||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetMetadataResponse = {
|
export type getMetadataResult = {
|
||||||
getMetadataResult: {
|
count: number;
|
||||||
count: number;
|
index: number;
|
||||||
index: number;
|
total: number;
|
||||||
total: number;
|
mediaCollection?: any[];
|
||||||
mediaCollection: any[] | undefined;
|
mediaMetadata?: any[];
|
||||||
mediaMetadata: any[] | undefined;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getMetadataResult({
|
export type GetMetadataResponse = {
|
||||||
mediaCollection,
|
getMetadataResult: getMetadataResult;
|
||||||
index,
|
};
|
||||||
total,
|
|
||||||
}: {
|
|
||||||
mediaCollection: any[] | undefined;
|
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
}): GetMetadataResponse {
|
|
||||||
return {
|
|
||||||
getMetadataResult: {
|
|
||||||
count: mediaCollection?.length || 0,
|
|
||||||
index,
|
|
||||||
total,
|
|
||||||
mediaCollection: mediaCollection || [],
|
|
||||||
mediaMetadata: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMetadataResult2({
|
export function getMetadataResult(
|
||||||
mediaMetadata,
|
result: Partial<getMetadataResult>
|
||||||
index,
|
): GetMetadataResponse {
|
||||||
total,
|
const count =
|
||||||
}: {
|
(result?.mediaCollection?.length || 0) +
|
||||||
mediaMetadata: any[] | undefined;
|
(result?.mediaMetadata?.length || 0);
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
}): GetMetadataResponse {
|
|
||||||
return {
|
return {
|
||||||
getMetadataResult: {
|
getMetadataResult: {
|
||||||
count: mediaMetadata?.length || 0,
|
count,
|
||||||
index,
|
index: 0,
|
||||||
total,
|
total: count,
|
||||||
mediaCollection: undefined,
|
...result,
|
||||||
mediaMetadata: mediaMetadata || [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -225,7 +204,7 @@ export const defaultArtistArtURI = (
|
|||||||
) =>
|
) =>
|
||||||
`${webAddress}/artist/${artist.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
|
`${webAddress}/artist/${artist.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
|
||||||
|
|
||||||
const album = (
|
export const album = (
|
||||||
webAddress: string,
|
webAddress: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
album: AlbumSummary
|
album: AlbumSummary
|
||||||
@@ -234,7 +213,7 @@ const album = (
|
|||||||
id: `album:${album.id}`,
|
id: `album:${album.id}`,
|
||||||
title: album.name,
|
title: album.name,
|
||||||
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
|
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
|
||||||
canPlay: true
|
canPlay: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const track = (
|
export const track = (
|
||||||
@@ -262,6 +241,18 @@ export const track = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const artist = (
|
||||||
|
webAddress: string,
|
||||||
|
accessToken: string,
|
||||||
|
artist: ArtistSummary
|
||||||
|
) => ({
|
||||||
|
itemType: "artist",
|
||||||
|
id: `artist:${artist.id}`,
|
||||||
|
artistId: artist.id,
|
||||||
|
title: artist.name,
|
||||||
|
albumArtURI: defaultArtistArtURI(webAddress, accessToken, artist),
|
||||||
|
});
|
||||||
|
|
||||||
type SoapyHeaders = {
|
type SoapyHeaders = {
|
||||||
credentials?: Credentials;
|
credentials?: Credentials;
|
||||||
};
|
};
|
||||||
@@ -355,6 +346,68 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getExtendedMetadata: async (
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
count,
|
||||||
|
}: // recursive,
|
||||||
|
{ id: string; index: number; count: number; recursive: boolean },
|
||||||
|
_,
|
||||||
|
headers?: SoapyHeaders
|
||||||
|
) => {
|
||||||
|
if (!headers?.credentials) {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnsupported",
|
||||||
|
faultstring: "Missing credentials...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const authToken = headers.credentials.loginToken.token;
|
||||||
|
const login = await musicService.login(authToken).catch((_) => {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnauthorized",
|
||||||
|
faultstring: "Credentials not found...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const musicLibrary = login as MusicLibrary;
|
||||||
|
|
||||||
|
const [type, typeId] = id.split(":");
|
||||||
|
const paging = { _index: index, _count: count };
|
||||||
|
switch (type) {
|
||||||
|
case "artist":
|
||||||
|
return await musicLibrary.artist(typeId!).then((artist) => {
|
||||||
|
const [page, total] = slice2<Album>(paging)(artist.albums);
|
||||||
|
const accessToken = accessTokens.mint(authToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
count: page.length,
|
||||||
|
index: paging._index,
|
||||||
|
total,
|
||||||
|
mediaCollection: page.map((it) =>
|
||||||
|
album(webAddress, accessToken, it)
|
||||||
|
),
|
||||||
|
relatedBrowse:
|
||||||
|
artist.similarArtists.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: `relatedArtists:${artist.id}`,
|
||||||
|
type: "RELATED_ARTISTS",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw `Unsupported id:${id}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
getMetadata: async (
|
getMetadata: async (
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
@@ -387,7 +440,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
|
|
||||||
const [type, typeId] = id.split(":");
|
const [type, typeId] = id.split(":");
|
||||||
const paging = { _index: index, _count: count };
|
const paging = { _index: index, _count: count };
|
||||||
logger.debug(`Fetching type=${type}, typeId=${typeId}`);
|
logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "root":
|
case "root":
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
@@ -403,13 +456,9 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
return await musicLibrary.artists(paging).then((result) => {
|
return await musicLibrary.artists(paging).then((result) => {
|
||||||
const accessToken = accessTokens.mint(authToken);
|
const accessToken = accessTokens.mint(authToken);
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaCollection: result.results.map((it) => ({
|
mediaCollection: result.results.map((it) =>
|
||||||
itemType: "artist",
|
artist(webAddress, accessToken, it)
|
||||||
id: `artist:${it.id}`,
|
),
|
||||||
artistId: it.id,
|
|
||||||
title: it.name,
|
|
||||||
albumArtURI: defaultArtistArtURI(webAddress, accessToken, it),
|
|
||||||
})),
|
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
});
|
});
|
||||||
@@ -451,13 +500,28 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
total,
|
total,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
case "relatedArtists":
|
||||||
|
return await musicLibrary
|
||||||
|
.artist(typeId!)
|
||||||
|
.then((artist) => artist.similarArtists)
|
||||||
|
.then(slice2(paging))
|
||||||
|
.then(([page, total]) => {
|
||||||
|
const accessToken = accessTokens.mint(authToken);
|
||||||
|
return getMetadataResult({
|
||||||
|
mediaCollection: page.map((it) =>
|
||||||
|
artist(webAddress, accessToken, it)
|
||||||
|
),
|
||||||
|
index: paging._index,
|
||||||
|
total,
|
||||||
|
});
|
||||||
|
});
|
||||||
case "album":
|
case "album":
|
||||||
return await musicLibrary
|
return await musicLibrary
|
||||||
.tracks(typeId!)
|
.tracks(typeId!)
|
||||||
.then(slice2(paging))
|
.then(slice2(paging))
|
||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
const accessToken = accessTokens.mint(authToken);
|
const accessToken = accessTokens.mint(authToken);
|
||||||
return getMetadataResult2({
|
return getMetadataResult({
|
||||||
mediaMetadata: page.map((it) =>
|
mediaMetadata: page.map((it) =>
|
||||||
track(webAddress, accessToken, it)
|
track(webAddress, accessToken, it)
|
||||||
),
|
),
|
||||||
|
|||||||
24
src/sonos.ts
24
src/sonos.ts
@@ -5,9 +5,26 @@ 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"
|
||||||
|
|
||||||
export const STRINGS_VERSION = "2";
|
export const STRINGS_VERSION = "2";
|
||||||
export const PRESENTATION_MAP_VERSION = "7";
|
export const PRESENTATION_MAP_VERSION = "7";
|
||||||
|
export type Capability =
|
||||||
|
| "search"
|
||||||
|
| "trFavorites"
|
||||||
|
| "alFavorites"
|
||||||
|
| "ucPlaylists"
|
||||||
|
| "extendedMD"
|
||||||
|
| "contextHeaders"
|
||||||
|
| "authorizationHeader";
|
||||||
|
|
||||||
|
export const BONOB_CAPABILITIES: Capability[] = [
|
||||||
|
// "search",
|
||||||
|
// "trFavorites",
|
||||||
|
// "alFavorites",
|
||||||
|
// "ucPlaylists",
|
||||||
|
"extendedMD",
|
||||||
|
];
|
||||||
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -103,6 +120,7 @@ export const asCustomdForm = (csrfToken: string, service: Service) => ({
|
|||||||
manifestVersion: "0",
|
manifestVersion: "0",
|
||||||
manifestUri: "",
|
manifestUri: "",
|
||||||
containerType: "MService",
|
containerType: "MService",
|
||||||
|
caps: BONOB_CAPABILITIES,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupDiscovery = (
|
const setupDiscovery = (
|
||||||
@@ -156,7 +174,9 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Registering ${service.name}(SID:${service.sid}) with sonos device ${anyDevice.Name} @ ${anyDevice.Host}`)
|
logger.info(
|
||||||
|
`Registering ${service.name}(SID:${service.sid}) with sonos device ${anyDevice.Name} @ ${anyDevice.Host}`
|
||||||
|
);
|
||||||
|
|
||||||
const customd = `http://${anyDevice.Host}:${anyDevice.Port}/customsd`;
|
const customd = `http://${anyDevice.Host}:${anyDevice.Port}/customsd`;
|
||||||
|
|
||||||
@@ -175,7 +195,7 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.post(customd, new URLSearchParams(asCustomdForm(csrfToken, service)), {
|
.post(customd, new URLSearchParams(qs.stringify(asCustomdForm(csrfToken, service))), {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import {
|
|||||||
STRINGS_ROUTE,
|
STRINGS_ROUTE,
|
||||||
LOGIN_ROUTE,
|
LOGIN_ROUTE,
|
||||||
getMetadataResult,
|
getMetadataResult,
|
||||||
getMetadataResult2,
|
|
||||||
PRESENTATION_MAP_ROUTE,
|
PRESENTATION_MAP_ROUTE,
|
||||||
SONOS_RECOMMENDED_IMAGE_SIZES,
|
SONOS_RECOMMENDED_IMAGE_SIZES,
|
||||||
track,
|
track,
|
||||||
|
album,
|
||||||
defaultAlbumArtURI,
|
defaultAlbumArtURI,
|
||||||
defaultArtistArtURI,
|
defaultArtistArtURI,
|
||||||
} from "../src/smapi";
|
} from "../src/smapi";
|
||||||
@@ -96,18 +96,20 @@ describe("service config", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getMetadataResult", () => {
|
describe("getMetadataResult", () => {
|
||||||
describe("when there are a zero mediaCollections", () => {
|
describe("when there are a no mediaCollections & no mediaMetadata", () => {
|
||||||
it("should have zero count", () => {
|
it("should have zero count", () => {
|
||||||
const result = getMetadataResult({
|
const result = getMetadataResult({
|
||||||
mediaCollection: [],
|
|
||||||
index: 33,
|
index: 33,
|
||||||
total: 99,
|
total: 99,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.getMetadataResult.count).toEqual(0);
|
expect(result).toEqual({
|
||||||
expect(result.getMetadataResult.index).toEqual(33);
|
getMetadataResult: {
|
||||||
expect(result.getMetadataResult.total).toEqual(99);
|
count: 0,
|
||||||
expect(result.getMetadataResult.mediaCollection).toEqual([]);
|
index: 33,
|
||||||
|
total: 99,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,10 +122,57 @@ describe("getMetadataResult", () => {
|
|||||||
total: 3,
|
total: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.getMetadataResult.count).toEqual(2);
|
expect(result).toEqual({
|
||||||
expect(result.getMetadataResult.index).toEqual(22);
|
getMetadataResult: {
|
||||||
expect(result.getMetadataResult.total).toEqual(3);
|
count: 2,
|
||||||
expect(result.getMetadataResult.mediaCollection).toEqual(mediaCollection);
|
index: 22,
|
||||||
|
total: 3,
|
||||||
|
mediaCollection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are a number of mediaMetadata", () => {
|
||||||
|
it("should add correct counts", () => {
|
||||||
|
const mediaMetadata = [{}, {}];
|
||||||
|
const result = getMetadataResult({
|
||||||
|
mediaMetadata,
|
||||||
|
index: 22,
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
getMetadataResult: {
|
||||||
|
count: 2,
|
||||||
|
index: 22,
|
||||||
|
total: 3,
|
||||||
|
mediaMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are both a number of mediaMetadata & mediaCollections", () => {
|
||||||
|
it("should sum the counts", () => {
|
||||||
|
const mediaCollection = [{}, {}, {}];
|
||||||
|
const mediaMetadata = [{}, {}];
|
||||||
|
const result = getMetadataResult({
|
||||||
|
mediaCollection,
|
||||||
|
mediaMetadata,
|
||||||
|
index: 22,
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
getMetadataResult: {
|
||||||
|
count: 5,
|
||||||
|
index: 22,
|
||||||
|
total: 3,
|
||||||
|
mediaCollection,
|
||||||
|
mediaMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -165,6 +214,22 @@ describe("track", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("album", () => {
|
||||||
|
it("should map to a sonos album", () => {
|
||||||
|
const webAddress = "http://localhost:9988";
|
||||||
|
const accessToken = uuid();
|
||||||
|
const someAlbum = anAlbum({ id: "id123", name: "What a great album" });
|
||||||
|
|
||||||
|
expect(album(webAddress, accessToken, someAlbum)).toEqual({
|
||||||
|
itemType: "album",
|
||||||
|
id: `album:${someAlbum.id}`,
|
||||||
|
title: someAlbum.name,
|
||||||
|
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, someAlbum),
|
||||||
|
canPlay: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("defaultAlbumArtURI", () => {
|
describe("defaultAlbumArtURI", () => {
|
||||||
it("should create the correct URI", () => {
|
it("should create the correct URI", () => {
|
||||||
const webAddress = "http://localhost:1234";
|
const webAddress = "http://localhost:1234";
|
||||||
@@ -576,7 +641,7 @@ describe("api", () => {
|
|||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
||||||
canPlay: true
|
canPlay: true,
|
||||||
})),
|
})),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: artistWithManyAlbums.albums.length,
|
total: artistWithManyAlbums.albums.length,
|
||||||
@@ -602,7 +667,7 @@ describe("api", () => {
|
|||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
||||||
canPlay: true
|
canPlay: true,
|
||||||
})),
|
})),
|
||||||
index: 2,
|
index: 2,
|
||||||
total: artistWithManyAlbums.albums.length,
|
total: artistWithManyAlbums.albums.length,
|
||||||
@@ -663,7 +728,11 @@ describe("api", () => {
|
|||||||
id: `artist:${it.id}`,
|
id: `artist:${it.id}`,
|
||||||
artistId: it.id,
|
artistId: it.id,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultArtistArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultArtistArtURI(
|
||||||
|
rootUrl,
|
||||||
|
accessToken,
|
||||||
|
it
|
||||||
|
),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -674,6 +743,117 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("asking for relatedArtists", () => {
|
||||||
|
describe("when the artist has many", () => {
|
||||||
|
const relatedArtist1 = anArtist();
|
||||||
|
const relatedArtist2 = anArtist();
|
||||||
|
const relatedArtist3 = anArtist();
|
||||||
|
const relatedArtist4 = anArtist();
|
||||||
|
|
||||||
|
const artist = anArtist({
|
||||||
|
similarArtists: [
|
||||||
|
relatedArtist1,
|
||||||
|
relatedArtist2,
|
||||||
|
relatedArtist3,
|
||||||
|
relatedArtist4,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasArtists(
|
||||||
|
artist,
|
||||||
|
relatedArtist1,
|
||||||
|
relatedArtist2,
|
||||||
|
relatedArtist3,
|
||||||
|
relatedArtist4
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when they fit on one page", () => {
|
||||||
|
it("should return them", async () => {
|
||||||
|
const result = await ws.getMetadataAsync({
|
||||||
|
id: `relatedArtists:${artist.id}`,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
relatedArtist1,
|
||||||
|
relatedArtist2,
|
||||||
|
relatedArtist3,
|
||||||
|
relatedArtist4,
|
||||||
|
].map((it) => ({
|
||||||
|
itemType: "artist",
|
||||||
|
id: `artist:${it.id}`,
|
||||||
|
artistId: it.id,
|
||||||
|
title: it.name,
|
||||||
|
albumArtURI: defaultArtistArtURI(
|
||||||
|
rootUrl,
|
||||||
|
accessToken,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
index: 0,
|
||||||
|
total: 4,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when they dont fit on one page", () => {
|
||||||
|
it("should return them", async () => {
|
||||||
|
const result = await ws.getMetadataAsync({
|
||||||
|
id: `relatedArtists:${artist.id}`,
|
||||||
|
index: 1,
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
getMetadataResult({
|
||||||
|
mediaCollection: [relatedArtist2, relatedArtist3].map(
|
||||||
|
(it) => ({
|
||||||
|
itemType: "artist",
|
||||||
|
id: `artist:${it.id}`,
|
||||||
|
artistId: it.id,
|
||||||
|
title: it.name,
|
||||||
|
albumArtURI: defaultArtistArtURI(
|
||||||
|
rootUrl,
|
||||||
|
accessToken,
|
||||||
|
it
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
index: 1,
|
||||||
|
total: 4,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the artist has none", () => {
|
||||||
|
const artist = anArtist({ similarArtists: [] });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasArtists(artist);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty list", async () => {
|
||||||
|
const result = await ws.getMetadataAsync({
|
||||||
|
id: `relatedArtists:${artist.id}`,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
getMetadataResult({
|
||||||
|
index: 0,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("asking for albums", () => {
|
describe("asking for albums", () => {
|
||||||
const artist1 = anArtist({
|
const artist1 = anArtist({
|
||||||
albums: [anAlbum(), anAlbum(), anAlbum()],
|
albums: [anAlbum(), anAlbum(), anAlbum()],
|
||||||
@@ -708,7 +888,7 @@ describe("api", () => {
|
|||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
||||||
canPlay: true
|
canPlay: true,
|
||||||
})),
|
})),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 6,
|
total: 6,
|
||||||
@@ -735,7 +915,7 @@ describe("api", () => {
|
|||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
||||||
canPlay: true
|
canPlay: true,
|
||||||
})),
|
})),
|
||||||
index: 2,
|
index: 2,
|
||||||
total: 6,
|
total: 6,
|
||||||
@@ -771,7 +951,7 @@ describe("api", () => {
|
|||||||
count: 100,
|
count: 100,
|
||||||
});
|
});
|
||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult2({
|
getMetadataResult({
|
||||||
mediaMetadata: [
|
mediaMetadata: [
|
||||||
track1,
|
track1,
|
||||||
track2,
|
track2,
|
||||||
@@ -796,7 +976,7 @@ describe("api", () => {
|
|||||||
count: 2,
|
count: 2,
|
||||||
});
|
});
|
||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult2({
|
getMetadataResult({
|
||||||
mediaMetadata: [track3, track4].map((it) =>
|
mediaMetadata: [track3, track4].map((it) =>
|
||||||
track(rootUrl, accessTokens.mint(token.authToken), it)
|
track(rootUrl, accessTokens.mint(token.authToken), it)
|
||||||
),
|
),
|
||||||
@@ -811,6 +991,149 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getExtendedMetadata", () => {
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
service,
|
||||||
|
rootUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes,
|
||||||
|
accessTokens
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("when no credentials header provided", () => {
|
||||||
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws
|
||||||
|
.getExtendedMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
|
.then(() => fail("shouldnt get here"))
|
||||||
|
.catch((e: any) => {
|
||||||
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
|
faultcode: "Client.LoginUnsupported",
|
||||||
|
faultstring: "Missing credentials...",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when invalid credentials are provided", () => {
|
||||||
|
it("should return a fault of LoginUnauthorized", async () => {
|
||||||
|
const username = "userThatGetsDeleted";
|
||||||
|
const password = "password1";
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
const token = (await musicService.generateToken({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})) as AuthSuccess;
|
||||||
|
musicService.hasNoUsers();
|
||||||
|
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||||
|
await ws
|
||||||
|
.getExtendedMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
|
.then(() => fail("shouldnt get here"))
|
||||||
|
.catch((e: any) => {
|
||||||
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
|
faultcode: "Client.LoginUnauthorized",
|
||||||
|
faultstring: "Credentials not found...",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when valid credentials are provided", () => {
|
||||||
|
const username = "validUser";
|
||||||
|
const password = "validPassword";
|
||||||
|
let token: AuthSuccess;
|
||||||
|
let ws: Client;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
token = (await musicService.generateToken({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})) as AuthSuccess;
|
||||||
|
|
||||||
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for an artist", () => {
|
||||||
|
describe("when it has similar artists", () => {
|
||||||
|
const similar1 = anArtist();
|
||||||
|
const similar2 = anArtist();
|
||||||
|
|
||||||
|
const artist = anArtist({
|
||||||
|
similarArtists: [similar1, similar2],
|
||||||
|
albums: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasArtists(artist);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a RELATED_ARTISTS browse option", async () => {
|
||||||
|
const root = await ws.getExtendedMetadataAsync({
|
||||||
|
id: `artist:${artist.id}`,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(root[0]).toEqual({
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
count: "0",
|
||||||
|
index: "0",
|
||||||
|
total: "0",
|
||||||
|
relatedBrowse: [
|
||||||
|
{
|
||||||
|
id: `relatedArtists:${artist.id}`,
|
||||||
|
type: "RELATED_ARTISTS",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when it has no similar artists", () => {
|
||||||
|
const artist = anArtist({
|
||||||
|
similarArtists: [],
|
||||||
|
albums: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasArtists(artist);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not return a RELATED_ARTISTS browse option", async () => {
|
||||||
|
const root = await ws.getExtendedMetadataAsync({
|
||||||
|
id: `artist:${artist.id}`,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(root[0]).toEqual({
|
||||||
|
getExtendedMetadataResult: {
|
||||||
|
count: "0",
|
||||||
|
index: "0",
|
||||||
|
total: "0",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getMediaURI", () => {
|
describe("getMediaURI", () => {
|
||||||
const accessTokenMint = jest.fn();
|
const accessTokenMint = jest.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import qs from "querystring"
|
||||||
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
||||||
import {
|
import {
|
||||||
MusicServicesService,
|
MusicServicesService,
|
||||||
@@ -21,6 +22,7 @@ import sonos, {
|
|||||||
Service,
|
Service,
|
||||||
STRINGS_VERSION,
|
STRINGS_VERSION,
|
||||||
PRESENTATION_MAP_VERSION,
|
PRESENTATION_MAP_VERSION,
|
||||||
|
BONOB_CAPABILITIES,
|
||||||
} from "../src/sonos";
|
} from "../src/sonos";
|
||||||
|
|
||||||
import { aSonosDevice, aService } from "./builders";
|
import { aSonosDevice, aService } from "./builders";
|
||||||
@@ -209,6 +211,7 @@ describe("sonos", () => {
|
|||||||
manifestVersion: "0",
|
manifestVersion: "0",
|
||||||
manifestUri: "",
|
manifestUri: "",
|
||||||
containerType: "MService",
|
containerType: "MService",
|
||||||
|
caps: BONOB_CAPABILITIES
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -553,7 +556,7 @@ describe("sonos", () => {
|
|||||||
|
|
||||||
expect(mockPost).toHaveBeenCalledWith(
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
`http://${device1.Host}:${device1.Port}/customsd`,
|
`http://${device1.Host}:${device1.Port}/customsd`,
|
||||||
new URLSearchParams(asCustomdForm(csrfToken, serviceToAdd)),
|
new URLSearchParams(qs.stringify(asCustomdForm(csrfToken, serviceToAdd))),
|
||||||
POST_CONFIG
|
POST_CONFIG
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user