Support for register using a seed host (#51)

This commit is contained in:
Simon J
2021-09-12 15:34:09 +10:00
committed by GitHub
parent 91cc450451
commit be4fcdff24
10 changed files with 219 additions and 44 deletions

View File

@@ -79,6 +79,8 @@ docker run \
Now within the LAN that contains the sonos devices run bonob the registration process.
#### Using auto-discovery
```bash
docker run \
--rm \
@@ -86,6 +88,16 @@ docker run \
simojenki/bonob register https://my-server.example.com/bonob
```
#### Using a seed host
```bash
docker run \
--rm \
-e BONOB_SONOS_SEED_HOST=192.168.1.163 \
simojenki/bonob register https://my-server.example.com/bonob
```
### Running bonob and navidrome using docker-compose
```yaml

View File

@@ -22,7 +22,7 @@ const bonob = bonobService(
"AppLink"
);
const sonosSystem = sonos(config.sonos.deviceDiscovery, config.sonos.seedHost);
const sonosSystem = sonos(config.sonos.discovery);
const streamUserAgent = config.navidrome.customClientsFor
? appendMimeTypeToClientFor(config.navidrome.customClientsFor.split(","))
@@ -90,7 +90,7 @@ if (config.sonos.autoRegister) {
);
}
});
} else if(config.sonos.deviceDiscovery) {
} else if(config.sonos.discovery.auto) {
sonosSystem.devices().then(devices => {
devices.forEach(d => {
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`)

View File

@@ -36,9 +36,11 @@ export default function () {
},
sonos: {
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
deviceDiscovery:
discovery: {
auto:
(process.env["BONOB_SONOS_DEVICE_DISCOVERY"] || "true") == "true",
seedHost: process.env["BONOB_SONOS_SEED_HOST"],
},
autoRegister:
(process.env["BONOB_SONOS_AUTO_REGISTER"] || "false") == "true",
sid: Number(process.env["BONOB_SONOS_SERVICE_ID"] || "246"),

View File

@@ -1,4 +1,5 @@
import registrar from "./registrar";
import readConfig from "./config";
import { URLBuilder } from "./url_builder";
const params = process.argv.slice(2);
@@ -9,7 +10,10 @@ if (params.length != 1) {
}
const bonobUrl = new URLBuilder(params[0]!);
registrar(bonobUrl)()
const config = readConfig();
registrar(bonobUrl, config.sonos.discovery)()
.then((success) => {
if (success) {
console.log(`Successfully registered bonob @ ${bonobUrl} with sonos`);

View File

@@ -1,9 +1,17 @@
import axios from "axios";
import _ from "underscore";
import logger from "./logger";
import sonos, { bonobService } from "./sonos";
import sonos, { bonobService, Discovery } from "./sonos";
import { URLBuilder } from "./url_builder";
export default (bonobUrl: URLBuilder) => async () => {
export default (
bonobUrl: URLBuilder,
sonosDiscovery: Discovery = {
auto: true,
seedHost: undefined,
}
) =>
async () => {
const about = bonobUrl.append({ pathname: "/about" });
logger.info(`Fetching bonob service about from ${about}`);
return axios
@@ -12,8 +20,19 @@ export default (bonobUrl: URLBuilder) => async () => {
if (res.status == 200) return res.data;
else throw `Unexpected response status ${res.status} from ${about}`;
})
.then((about) =>
bonobService(about.service.name, about.service.sid, bonobUrl)
)
.then((bonobService) => sonos(true).register(bonobService));
.then((res) => {
const name = _.get(res, ["service", "name"]);
const sid = _.get(res, ["service", "sid"]);
if (!name || !sid) {
throw `Unexpected response from ${about.href()}, expected service.name and service.sid`;
}
return {
name,
sid: Number.parseInt(sid),
};
})
.then(({ name, sid }: { name: string; sid: number }) =>
bonobService(name, sid, bonobUrl)
)
.then((service) => sonos(sonosDiscovery).register(service));
};

View File

@@ -9,7 +9,20 @@ import qs from "querystring";
import { URLBuilder } from "./url_builder";
import { LANG } from "./i8n";
export const SONOS_LANG: LANG[] = ["en-US", "da-DK", "de-DE", "es-ES", "fr-FR", "it-IT", "ja-JP", "nb-NO", "nl-NL", "pt-BR", "sv-SE", "zh-CN"]
export const SONOS_LANG: LANG[] = [
"en-US",
"da-DK",
"de-DE",
"es-ES",
"fr-FR",
"it-IT",
"ja-JP",
"nb-NO",
"nl-NL",
"pt-BR",
"sv-SE",
"zh-CN",
];
export const PRESENTATION_AND_STRINGS_VERSION = "21";
@@ -118,7 +131,7 @@ export const asDevice = (sonosDevice: SonosDevice): Device => ({
export const asRemoveCustomdForm = (csrfToken: string, sid: number) => ({
csrfToken,
sid: `${sid}`
sid: `${sid}`,
});
export const asCustomdForm = (csrfToken: string, service: Service) => ({
@@ -168,7 +181,10 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
});
};
const post = async (action: string, customdForm: (csrfToken: string) => any) => {
const post = async (
action: string,
customdForm: (csrfToken: string) => any
) => {
const anyDevice = await sonosDevices().then((devices) => head(devices));
if (!anyDevice) {
@@ -195,7 +211,7 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
);
return false;
}
const form = customdForm(csrfToken)
const form = customdForm(csrfToken);
logger.info(`${action} with sonos @ ${customd}`, { form });
return axios
.post(customd, new URLSearchParams(qs.stringify(form)), {
@@ -218,16 +234,22 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
)
.then((it) => it.map(asService)),
remove: async (sid: number) => post("remove", (csrfToken) => asRemoveCustomdForm(csrfToken, sid)),
remove: async (sid: number) =>
post("remove", (csrfToken) => asRemoveCustomdForm(csrfToken, sid)),
register: async (service: Service) => post("register", (csrfToken) => asCustomdForm(csrfToken, service)),
register: async (service: Service) =>
post("register", (csrfToken) => asCustomdForm(csrfToken, service)),
};
}
const sonos = (
discoveryEnabled: boolean = true,
sonosSeedHost: string | undefined = undefined
): Sonos =>
discoveryEnabled ? autoDiscoverySonos(sonosSeedHost) : SONOS_DISABLED;
export type Discovery = {
auto: boolean;
seedHost?: string;
};
export default sonos;
export default (
sonosDiscovery: Discovery = { auto: true }
): Sonos =>
sonosDiscovery.auto
? autoDiscoverySonos(sonosDiscovery.seedHost)
: SONOS_DISABLED;

View File

@@ -198,17 +198,17 @@ describe("config", () => {
"deviceDiscovery",
"BONOB_SONOS_DEVICE_DISCOVERY",
true,
(config) => config.sonos.deviceDiscovery
(config) => config.sonos.discovery.auto
);
describe("seedHost", () => {
it("should default to undefined", () => {
expect(config().sonos.seedHost).toBeUndefined();
expect(config().sonos.discovery.seedHost).toBeUndefined();
});
it("should be overridable", () => {
process.env["BONOB_SONOS_SEED_HOST"] = "123.456.789.0";
expect(config().sonos.seedHost).toEqual("123.456.789.0");
expect(config().sonos.discovery.seedHost).toEqual("123.456.789.0");
});
});

View File

@@ -20,6 +20,8 @@ import sharp from "sharp";
jest.mock("sharp");
import randomString from "../src/random_string";
jest.mock("../src/random_string");
import {
Album,
Artist,
@@ -43,8 +45,6 @@ import {
aTrack,
} from "./builders";
jest.mock("../src/random_string");
describe("t", () => {
it("should be an md5 of the password and the salt", () => {
const p = "password123";

116
tests/registrar.test.ts Normal file
View File

@@ -0,0 +1,116 @@
import axios from "axios";
jest.mock("axios");
const fakeSonos = {
register: jest.fn(),
};
import sonos, { bonobService } from "../src/sonos";
jest.mock("../src/sonos");
import registrar from "../src/registrar";
import { URLBuilder } from "../src/url_builder";
describe("registrar", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});
describe("when the bonob service can not be found", () => {
it("should fail", async () => {
const status = 409;
(axios.get as jest.Mock).mockResolvedValue({
status,
});
const bonobUrl = new URLBuilder("http://fail.example.com/bonob");
return expect(registrar(bonobUrl)()).rejects.toEqual(
`Unexpected response status ${status} from ${bonobUrl
.append({ pathname: "/about" })
.href()}`
);
});
});
describe("when the bonob service returns unexpected content", () => {
it("should fail", async () => {
(axios.get as jest.Mock).mockResolvedValue({
status: 200,
// invalid response from /about as does not have name and sid
data: {}
});
const bonobUrl = new URLBuilder("http://fail.example.com/bonob");
return expect(registrar(bonobUrl)()).rejects.toEqual(
`Unexpected response from ${bonobUrl
.append({ pathname: "/about" })
.href()}, expected service.name and service.sid`
);
});
});
describe("when the bonob service can be found", () => {
const bonobUrl = new URLBuilder("http://success.example.com/bonob");
const serviceDetails = {
name: "bob",
sid: 123,
};
const service = "service";
beforeEach(() => {
(axios.get as jest.Mock).mockResolvedValue({
status: 200,
data: {
service: serviceDetails,
},
});
(bonobService as jest.Mock).mockResolvedValue(service);
(sonos as jest.Mock).mockReturnValue(fakeSonos);
});
describe("when registration succeeds", () => {
it("should fetch the service details and register", async () => {
fakeSonos.register.mockResolvedValue(true);
const sonosDiscovery = { auto: true };
expect(await registrar(bonobUrl, sonosDiscovery)()).toEqual(
true
);
expect(bonobService).toHaveBeenCalledWith(
serviceDetails.name,
serviceDetails.sid,
bonobUrl
);
expect(sonos).toHaveBeenCalledWith(sonosDiscovery);
expect(fakeSonos.register).toHaveBeenCalledWith(service);
});
});
describe("when registration fails", () => {
it("should fetch the service details and register", async () => {
fakeSonos.register.mockResolvedValue(false);
const sonosDiscovery = { auto: false, seedHost: "192.168.1.163" };
expect(await registrar(bonobUrl, sonosDiscovery)()).toEqual(
false
);
expect(bonobService).toHaveBeenCalledWith(
serviceDetails.name,
serviceDetails.sid,
bonobUrl
);
expect(sonos).toHaveBeenCalledWith(sonosDiscovery);
expect(fakeSonos.register).toHaveBeenCalledWith(service);
});
});
});
});

View File

@@ -274,7 +274,7 @@ describe("sonos", () => {
describe("when is disabled", () => {
it("should return a disabled client", async () => {
const disabled = sonos(false);
const disabled = sonos({ auto: false });
expect(disabled).toEqual(SONOS_DISABLED);
expect(await disabled.devices()).toEqual([]);
@@ -310,7 +310,7 @@ describe("sonos", () => {
);
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
const actualDevices = await sonos(true, undefined).devices();
const actualDevices = await sonos({ auto: true }).devices();
expect(SonosManager).toHaveBeenCalledTimes(1);
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
@@ -331,7 +331,7 @@ describe("sonos", () => {
);
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
const actualDevices = await sonos(true, "").devices();
const actualDevices = await sonos({ auto: true, seedHost: "" }).devices();
expect(SonosManager).toHaveBeenCalledTimes(1);
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
@@ -354,7 +354,7 @@ describe("sonos", () => {
);
sonosManager.InitializeFromDevice.mockResolvedValue(true);
const actualDevices = await sonos(true, seedHost).devices();
const actualDevices = await sonos({ auto: true, seedHost }).devices();
expect(SonosManager).toHaveBeenCalledTimes(1);
expect(sonosManager.InitializeFromDevice).toHaveBeenCalledWith(
@@ -377,7 +377,7 @@ describe("sonos", () => {
);
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
const actualDevices = await sonos(true, undefined).devices();
const actualDevices = await sonos({ auto: true, seedHost: undefined }).devices();
expect(actualDevices).toEqual([
{
@@ -408,7 +408,7 @@ describe("sonos", () => {
);
sonosManager.InitializeWithDiscovery.mockResolvedValue(false);
expect(await sonos(true, "").devices()).toEqual([]);
expect(await sonos({ auto: true, seedHost: "" }).devices()).toEqual([]);
});
});
});