Tests for browsing of artists and albums

This commit is contained in:
simojenki
2021-03-01 12:46:23 +11:00
parent 0cb02707f1
commit 3b350c4402
12 changed files with 778 additions and 57 deletions

View File

@@ -10,7 +10,7 @@ export type Association = {
export interface LinkCodes {
mint(): string
clear(): any
count(): Number
count(): number
has(linkCode: string): boolean
associate(linkCode: string, association: Association): any
associationFor(linkCode: string): Association | undefined

View File

@@ -6,29 +6,72 @@ const navidrome = process.env["BONOB_NAVIDROME_URL"];
const u = process.env["BONOB_USER"];
const t = Md5.hashStr(`${process.env["BONOB_PASSWORD"]}${s}`);
export type Credentials = { username: string, password: string }
export type Credentials = { username: string; password: string };
export function isSuccess(authResult: AuthSuccess | AuthFailure): authResult is AuthSuccess {
export function isSuccess(
authResult: AuthSuccess | AuthFailure
): authResult is AuthSuccess {
return (authResult as AuthSuccess).authToken !== undefined;
}
export type AuthSuccess = {
authToken: string
userId: string
nickname: string
export function isFailure(
authResult: any | AuthFailure
): authResult is AuthFailure {
return (authResult as AuthFailure).message !== undefined;
}
export type AuthSuccess = {
authToken: string;
userId: string;
nickname: string;
};
export type AuthFailure = {
message: string
}
message: string;
};
export interface MusicService {
login(credentials: Credentials): AuthSuccess | AuthFailure
generateToken(credentials: Credentials): AuthSuccess | AuthFailure;
login(authToken: string): MusicLibrary | AuthFailure;
}
export type Artist = {
id: string;
name: string;
};
export type Album = {
id: string;
name: string;
};
export interface MusicLibrary {
artists(): Artist[];
artist(id: string): Artist;
albums({ artistId, _index, _count }: { artistId?: string, _index?: number, _count?: number }): Album[];
}
export class Navidrome implements MusicService {
login({ username }: Credentials) {
return { authToken: `v1:${username}`, userId: username, nickname: username }
generateToken({ username }: Credentials) {
return {
authToken: `v1:${username}`,
userId: username,
nickname: username,
};
}
login(_: string) {
return {
artists: () => [],
artist: (id: string) => ({
id,
name: id,
}),
albums: ({ artistId }: { artistId?: string }) => {
console.log(artistId)
return []
},
};
}
ping = (): Promise<boolean> =>
@@ -42,6 +85,3 @@ export class Navidrome implements MusicService {
return false;
});
}

View File

@@ -71,7 +71,7 @@ function server(
app.post(LOGIN_ROUTE, (req, res) => {
const { username, password, linkCode } = req.body;
const authResult = musicService.login({
const authResult = musicService.generateToken({
username,
password,
});
@@ -107,7 +107,7 @@ function server(
res.send("");
});
bindSmapiSoapServiceToExpress(app, SOAP_PATH, webAddress, linkCodes);
bindSmapiSoapServiceToExpress(app, SOAP_PATH, webAddress, linkCodes, musicService);
return app;
}

View File

@@ -6,8 +6,9 @@ import path from "path";
import logger from "./logger";
import { LinkCodes } from "./link_codes";
import { isFailure, MusicLibrary, MusicService } from "./music_service";
export const LOGIN_ROUTE = "/login"
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";
@@ -17,6 +18,15 @@ const WSDL_FILE = path.resolve(
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
);
export type Credentials = {
loginToken: {
token: string;
householdId: string;
};
deviceId: string;
deviceProvider: string;
};
export type GetAppLinkResult = {
getAppLinkResult: {
authorizeAccount: {
@@ -37,6 +47,36 @@ export type GetDeviceAuthTokenResult = {
};
};
export type MediaCollection = {
id: string;
itemType: "collection";
title: string;
};
export type GetMetadataResponse = {
getMetadataResult: {
count: number;
index: number;
total: number;
mediaCollection: MediaCollection[];
};
};
export function getMetadataResult({
mediaCollection,
}: {
mediaCollection: any[] | undefined;
}): GetMetadataResponse {
return {
getMetadataResult: {
count: mediaCollection?.length || 0,
index: 0,
total: mediaCollection?.length || 0,
mediaCollection: mediaCollection || [],
},
};
}
class SonosSoap {
linkCodes: LinkCodes;
webAddress: string;
@@ -97,7 +137,35 @@ class SonosSoap {
}
}
function bindSmapiSoapServiceToExpress(app: Express, soapPath:string, webAddress: string, linkCodes: LinkCodes) {
export type Container = {
itemType: "container";
id: string;
title: string;
};
export const container = ({
id,
title,
}: {
id: string;
title: string;
}): Container => ({
itemType: "container",
id,
title,
});
type SoapyHeaders = {
credentials?: Credentials;
};
function bindSmapiSoapServiceToExpress(
app: Express,
soapPath: string,
webAddress: string,
linkCodes: LinkCodes,
musicService: MusicService
) {
const sonosSoap = new SonosSoap(webAddress, linkCodes);
const soapyService = listen(
app,
@@ -108,6 +176,72 @@ function bindSmapiSoapServiceToExpress(app: Express, soapPath:string, webAddress
getAppLink: () => sonosSoap.getAppLink(),
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>
sonosSoap.getDeviceAuthToken({ linkCode }),
getMetadata: (
{
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 login = musicService.login(
headers.credentials.loginToken.token
);
if (isFailure(login)) {
throw {
Fault: {
faultcode: "Client.LoginUnauthorized",
faultstring: "Credentials not found...",
},
};
}
const musicLibrary = login as MusicLibrary;
// const [type, typeId] = id.split(":");
const type = id;
switch (type) {
case "root":
return getMetadataResult({
mediaCollection: [
container({ id: "artists", title: "Artists" }),
container({ id: "albums", title: "Albums" }),
],
});
case "artists":
return getMetadataResult({
mediaCollection: musicLibrary.artists().map((it) =>
container({
id: `artist:${it.id}`,
title: it.name,
})
),
});
case "albums":
return getMetadataResult({
mediaCollection: musicLibrary
.albums({ _index: index, _count: count })
.map((it) =>
container({
id: `album:${it.id}`,
title: it.name,
})
),
});
default:
throw `Unsupported id:${id}`;
}
},
},
},
},