mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Tests for browsing of artists and albums
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
138
src/smapi.ts
138
src/smapi.ts
@@ -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}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user