mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f743aaa7e | ||
|
|
d4bed77c54 | ||
|
|
29531a6e01 | ||
|
|
e78b6c4fbc | ||
|
|
2941f6f595 | ||
|
|
2c48d08b0e | ||
|
|
de48ee0fca | ||
|
|
cefdf5e2d5 | ||
|
|
f86a78b338 | ||
|
|
4d23885d7c | ||
|
|
8c80c00089 | ||
|
|
ebf385e918 | ||
|
|
a20fdcbc5f | ||
|
|
f763dbd8b9 | ||
|
|
2d3e5dc635 | ||
|
|
6091308266 | ||
|
|
fed6e9663d | ||
|
|
03b5b04c73 | ||
|
|
4a529b46e1 | ||
|
|
5c9fbede7a | ||
|
|
94e25e03ea | ||
|
|
d9c3a3edcb | ||
|
|
f22b094d83 | ||
|
|
4ae71675e8 | ||
|
|
84866dfd60 | ||
|
|
719fd998b1 | ||
|
|
91995678a4 | ||
|
|
67d6c4a730 | ||
|
|
3df4f4daa7 | ||
|
|
bd63408ec3 | ||
|
|
da5491b474 | ||
|
|
bbd676b5b8 | ||
|
|
d01c747c96 | ||
|
|
192f65a56b | ||
|
|
9b3df4ce1a | ||
|
|
df9a6d4663 | ||
|
|
d0c80b2f20 |
15
.devcontainer/Dockerfile
Normal file
15
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:16-bullseye
|
||||
|
||||
LABEL maintainer=simojenki
|
||||
|
||||
ENV JEST_TIMEOUT=60000
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install --no-install-recommends \
|
||||
libvips-dev \
|
||||
python3 \
|
||||
make \
|
||||
git \
|
||||
g++ \
|
||||
vim
|
||||
13
.devcontainer/devcontainer.json
Normal file
13
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "bonob",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {
|
||||
"version": "latest",
|
||||
"moby": true
|
||||
}
|
||||
}
|
||||
}
|
||||
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.devcontainer
|
||||
.github
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
build
|
||||
node_modules
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
147529
.yarn/releases/yarn-1.22.19.cjs
vendored
Executable file
147529
.yarn/releases/yarn-1.22.19.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
631
.yarn/releases/yarn-berry.cjs
vendored
631
.yarn/releases/yarn-berry.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
||||
yarnPath: .yarn/releases/yarn-1.22.19.cjs
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:16-bullseye as build
|
||||
FROM node:16-bullseye-slim as build
|
||||
|
||||
WORKDIR /bonob
|
||||
|
||||
@@ -16,7 +16,7 @@ COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
|
||||
ENV JEST_TIMEOUT=30000
|
||||
ENV JEST_TIMEOUT=60000
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
@@ -29,13 +29,26 @@ RUN apt-get update && \
|
||||
g++ && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
yarn install --immutable && \
|
||||
yarn gitinfo && \
|
||||
yarn config set network-timeout 600000 -g && \
|
||||
yarn install \
|
||||
--prefer-offline \
|
||||
--frozen-lockfile \
|
||||
--non-interactive \
|
||||
--production=false && \
|
||||
yarn test --no-cache && \
|
||||
yarn build
|
||||
yarn gitinfo && \
|
||||
yarn build && \
|
||||
rm -Rf node_modules && \
|
||||
NODE_ENV=production yarn install \
|
||||
--prefer-offline \
|
||||
--pure-lockfile \
|
||||
--non-interactive \
|
||||
--production=true
|
||||
|
||||
|
||||
FROM node:16-bullseye
|
||||
FROM node:16-bullseye-slim
|
||||
|
||||
LABEL maintainer=simojenki
|
||||
|
||||
ENV BNB_PORT=4534
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
@@ -56,7 +69,10 @@ COPY src/Sonoswsdl-1.19.4-20190411.142401-3.wsdl ./src/Sonoswsdl-1.19.4-20190411
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install --no-install-recommends libvips tzdata && \
|
||||
apt-get -y install --no-install-recommends \
|
||||
libvips \
|
||||
tzdata \
|
||||
wget && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
26
README.md
26
README.md
@@ -16,7 +16,7 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
|
||||
- Search by Album, Artist, Track
|
||||
- Playlist editing through sonos app.
|
||||
- Marking of songs as favourites and with ratings through the sonos app.
|
||||
- Localization (only en-US & nl-NL supported currently, require translations for other languages). [Sonos localization and supported languages](https://developer.sonos.com/build/content-service-add-features/strings-and-localization/)
|
||||
- Localization (only en-US, da-DK & nl-NL supported currently, require translations for other languages). [Sonos localization and supported languages](https://developer.sonos.com/build/content-service-add-features/strings-and-localization/)
|
||||
- Auto discovery of sonos devices
|
||||
- Discovery of sonos devices using seed IP address
|
||||
- Auto registration with sonos on start
|
||||
@@ -126,8 +126,8 @@ services:
|
||||
# ip address of your machine running bonob
|
||||
BNB_URL: http://192.168.1.111:4534
|
||||
BNB_SECRET: changeme
|
||||
BNB_SONOS_AUTO_REGISTER: true
|
||||
BNB_SONOS_DEVICE_DISCOVERY: true
|
||||
BNB_SONOS_AUTO_REGISTER: "true"
|
||||
BNB_SONOS_DEVICE_DISCOVERY: "true"
|
||||
BNB_SONOS_SERVICE_ID: 246
|
||||
# ip address of one of your sonos devices
|
||||
BNB_SONOS_SEED_HOST: 192.168.1.121
|
||||
@@ -146,6 +146,8 @@ BNB_PORT | 4534 | Default http port for bonob to listen on
|
||||
BNB_URL | http://$(hostname):4534 | URL (including path) for bonob so that sonos devices can communicate. **This must be either the public IP or DNS entry of the bonob instance so that the sonos devices can communicate with it.**
|
||||
BNB_SECRET | bonob | secret used for encrypting credentials
|
||||
BNB_AUTH_TIMEOUT | 1h | Timeout for the sonos auth token, described in the format [ms](https://github.com/vercel/ms), ie. '5s' == 5 seconds, '11h' == 11 hours. In the case of using Navidrome this should be less than the value for ND_SESSIONTIMEOUT
|
||||
BNB_LOG_LEVEL | info | Log level. One of ['debug', 'info', 'warn', 'error']
|
||||
BNB_SERVER_LOG_REQUESTS | false | Whether or not to log http requests
|
||||
BNB_SONOS_AUTO_REGISTER | false | Whether or not to try and auto-register on startup
|
||||
BNB_SONOS_DEVICE_DISCOVERY | true | Enable/Disable sonos device discovery entirely. Setting this to 'false' will disable sonos device search, regardless of whether a seed host is specified.
|
||||
BNB_SONOS_SEED_HOST | undefined | sonos device seed host for discovery, or ommitted for for auto-discovery
|
||||
@@ -153,7 +155,7 @@ BNB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
||||
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
||||
BNB_SUBSONIC_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom subsonic clients when streaming. ie. "audio/flac,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs.
|
||||
BNB_SUBSONIC_ARTIST_IMAGE_CACHE | undefined | Path for caching of artist images as are sourced externally. ie. Navidrome provides spotify URLs
|
||||
BNB_SUBSONIC_ARTIST_IMAGE_CACHE | undefined | Path for caching of artist images that are sourced externally. ie. Navidrome provides spotify URLs. Remember to provide a volume-mapping for Docker, when enabling this cache.
|
||||
BNB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
|
||||
BNB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
|
||||
BNB_ICON_FOREGROUND_COLOR | undefined | Icon foreground color in sonos app, must be a valid [svg color](https://www.december.com/html/spec/colorsvg.html)
|
||||
@@ -188,6 +190,12 @@ Generally speaking you will not need to do this very often. However on occassio
|
||||
|
||||
Service should now be registered and everything should work as expected.
|
||||
|
||||
## Multiple registrations within a single household.
|
||||
|
||||
It's possible to register multiple Subsonic clone users for the bonob service in Sonos.
|
||||
Basically this consist of repeating the Sonos app ["Add a service"](#initialising-service-within-sonos-app) steps for each additional user.
|
||||
Afterwards the Sonos app displays a dropdown underneath the service, allowing to switch between users.
|
||||
|
||||
## Implementing a different music source other than a subsonic clone
|
||||
|
||||
- Implement the MusicService/MusicLibrary interface
|
||||
@@ -209,10 +217,16 @@ In this case you could set;
|
||||
BNB_SUBSONIC_CUSTOM_CLIENTS="audio/flac"
|
||||
```
|
||||
|
||||
This would result in 2 players in Navidrome, one called 'bonob', the other called 'bonob+audio/flac'. You could then configure a custom flac transcoder in Navidrome that re-samples the flacs to a sonos supported format, ie [Using something like this](https://stackoverflow.com/questions/41420391/ffmpeg-flac-24-bit-96khz-to-16-bit-48khz);
|
||||
This would result in 2 players in Navidrome, one called 'bonob', the other called 'bonob+audio/flac'. You could then configure a custom flac transcoder in Navidrome that re-samples the flacs to a sonos supported format, ie [Using something like this](https://stackoverflow.com/questions/41420391/ffmpeg-flac-24-bit-96khz-to-16-bit-48khz) or [this](https://stackoverflow.com/questions/52119489/ffmpeg-limit-audio-sample-rate):
|
||||
|
||||
```bash
|
||||
ffmpeg -i %s -af aresample=resampler=soxr:out_sample_fmt=s16:out_sample_rate=48000 -f flac -
|
||||
ffmpeg -i %s -af aformat=sample_fmts=s16|s32:sample_rates=8000|11025|16000|22050|24000|32000|44100|48000 -f flac -
|
||||
```
|
||||
|
||||
**Note for Sonos S1:** [24-bit depth is only supported by Sonos S2](https://support.sonos.com/s/article/79?language=en_US), so if your system is still on Sonos S1, transcoding should convert all FLACs to 16-bit:
|
||||
|
||||
```bash
|
||||
ffmpeg -i %s -af aformat=sample_fmts=s16:sample_rates=8000|11025|16000|22050|24000|32000|44100|48000 -f flac -
|
||||
```
|
||||
|
||||
### Changing Icon colors
|
||||
|
||||
@@ -5,5 +5,6 @@ module.exports = {
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/node_modules',
|
||||
'<rootDir>/build',
|
||||
],
|
||||
],
|
||||
testTimeout: Number.parseInt(process.env["JEST_TIMEOUT"] || "5000")
|
||||
};
|
||||
89
package.json
89
package.json
@@ -6,64 +6,67 @@
|
||||
"author": "simojenki <simojenki@users.noreply.github.com>",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@svrooij/sonos": "^2.4.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/jws": "^3.2.4",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/node": "^16.7.13",
|
||||
"@svrooij/sonos": "^2.5.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/jws": "^3.2.5",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/randomstring": "^1.1.8",
|
||||
"@types/sharp": "^0.28.6",
|
||||
"@types/underscore": "^1.11.3",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"axios": "^0.21.4",
|
||||
"dayjs": "^1.10.6",
|
||||
"eta": "^1.12.3",
|
||||
"express": "^4.17.1",
|
||||
"fp-ts": "^2.11.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/underscore": "^1.11.4",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/xmldom": "0.1.31",
|
||||
"axios": "^1.3.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"eta": "^2.0.1",
|
||||
"express": "^4.18.2",
|
||||
"fp-ts": "^2.13.1",
|
||||
"fs-extra": "^11.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"libxmljs2": "^0.28.0",
|
||||
"libxmljs2": "^0.31.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-html-parser": "^4.1.4",
|
||||
"randomstring": "^1.2.1",
|
||||
"sharp": "^0.29.1",
|
||||
"soap": "^0.42.0",
|
||||
"ts-md5": "^1.2.9",
|
||||
"typescript": "^4.4.2",
|
||||
"underscore": "^1.13.1",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"randomstring": "^1.2.3",
|
||||
"sharp": "^0.31.3",
|
||||
"soap": "^1.0.0",
|
||||
"ts-md5": "^1.3.1",
|
||||
"typescript": "^4.9.5",
|
||||
"underscore": "^1.13.6",
|
||||
"urn-lib": "^2.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.8.2",
|
||||
"xmldom-ts": "^0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/tmp": "^0.2.1",
|
||||
"chai": "^4.3.4",
|
||||
"get-port": "^5.1.1",
|
||||
"image-js": "^0.33.0",
|
||||
"jest": "^27.1.0",
|
||||
"nodemon": "^2.0.12",
|
||||
"supertest": "^6.1.6",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"chai": "^4.3.7",
|
||||
"get-port": "^6.1.2",
|
||||
"image-js": "^0.35.3",
|
||||
"jest": "^29.4.3",
|
||||
"nodemon": "^2.0.21",
|
||||
"supertest": "^6.3.3",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"xmldom-ts": "^0.3.1",
|
||||
"xpath-ts": "^1.3.13"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -Rf build node_modules",
|
||||
"build": "tsc",
|
||||
"dev": "BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_ICON_FOREGROUND_COLOR=white BNB_ICON_BACKGROUND_COLOR=darkgrey BNB_SONOS_SERVICE_NAME=bonobDev BNB_SONOS_DEVICE_DISCOVERY=true nodemon -V ./src/app.ts",
|
||||
"devr": "BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_ICON_FOREGROUND_COLOR=white BNB_ICON_BACKGROUND_COLOR=darkgrey BNB_SONOS_SERVICE_NAME=bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_SONOS_AUTO_REGISTER=true nodemon -V ./src/app.ts",
|
||||
"dev": "BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_ICON_FOREGROUND_COLOR=white BNB_ICON_BACKGROUND_COLOR=darkgrey BNB_SONOS_SERVICE_NAME=bonobDev BNB_SONOS_DEVICE_DISCOVERY=true nodemon -V ./src/app.ts",
|
||||
"devr": "BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_ICON_FOREGROUND_COLOR=white BNB_ICON_BACKGROUND_COLOR=darkgrey BNB_SONOS_SERVICE_NAME=bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_SONOS_AUTO_REGISTER=true nodemon -V ./src/app.ts",
|
||||
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
|
||||
"test": "jest",
|
||||
"gitinfo": "git describe --tags > .gitinfo"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
||||
15
src/app.ts
15
src/app.ts
@@ -88,14 +88,14 @@ const app = server(
|
||||
clock,
|
||||
iconColors: config.icons,
|
||||
applyContextPath: true,
|
||||
logRequests: true,
|
||||
logRequests: config.logRequests,
|
||||
version,
|
||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||
externalImageResolver: artistImageFetcher
|
||||
}
|
||||
);
|
||||
|
||||
app.listen(config.port, () => {
|
||||
const expressServer = app.listen(config.port, () => {
|
||||
logger.info(`Listening on ${config.port} available @ ${config.bonobUrl}`);
|
||||
});
|
||||
|
||||
@@ -113,6 +113,15 @@ if (config.sonos.autoRegister) {
|
||||
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM signal received: closing HTTP server');
|
||||
expressServer.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
});
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
export default app;
|
||||
|
||||
48
src/clock.ts
48
src/clock.ts
@@ -1,13 +1,38 @@
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() == 11 && clock.now().date() == 25;
|
||||
export const isMay4 = (clock: Clock = SystemClock) => clock.now().month() == 4 && clock.now().date() == 4;
|
||||
export const isHalloween = (clock: Clock = SystemClock) => clock.now().month() == 9 && clock.now().date() == 31
|
||||
export const isHoli = (clock: Clock = SystemClock) => ["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY = (clock: Clock = SystemClock) => ["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY_2022 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2022/02/01"))
|
||||
export const isCNY_2023 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2023/01/22"))
|
||||
export const isCNY_2024 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2024/02/10"))
|
||||
function fixedDateMonthEvent(dateMonth: string) {
|
||||
const date = Number.parseInt(dateMonth.split("/")[0]!);
|
||||
const month = Number.parseInt(dateMonth.split("/")[1]!);
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return clock.now().date() == date && clock.now().month() == month - 1;
|
||||
};
|
||||
}
|
||||
|
||||
function fixedDateEvent(date: string) {
|
||||
const dayjsDate = dayjs(date);
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return clock.now().isSame(dayjsDate, "day");
|
||||
};
|
||||
}
|
||||
|
||||
function anyOf(rules: ((clock: Clock) => boolean)[]) {
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return rules.find((rule) => rule(clock)) != undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export const isChristmas = fixedDateMonthEvent("25/12");
|
||||
export const isMay4 = fixedDateMonthEvent("04/05");
|
||||
export const isHalloween = fixedDateMonthEvent("31/10");
|
||||
export const isHoli = anyOf(
|
||||
["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(fixedDateEvent)
|
||||
)
|
||||
|
||||
export const isCNY_2022 = fixedDateEvent("2022/02/01");
|
||||
export const isCNY_2023 = fixedDateEvent("2023/01/22");
|
||||
export const isCNY_2024 = fixedDateEvent("2024/02/10");
|
||||
export const isCNY_2025 = fixedDateEvent("2025/02/29");
|
||||
export const isCNY = anyOf([isCNY_2022, isCNY_2023, isCNY_2024, isCNY_2025]);
|
||||
|
||||
export interface Clock {
|
||||
now(): Dayjs;
|
||||
@@ -17,12 +42,13 @@ export const SystemClock = { now: () => dayjs() };
|
||||
|
||||
export class FixedClock implements Clock {
|
||||
time: Dayjs;
|
||||
|
||||
|
||||
constructor(time: Dayjs = dayjs()) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
add = (t: number, unit: dayjs.UnitTypeShort) => this.time = this.time.add(t, unit)
|
||||
add = (t: number, unit: dayjs.UnitTypeShort) =>
|
||||
(this.time = this.time.add(t, unit));
|
||||
|
||||
now = () => this.time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,22 @@ import url from "./url_builder";
|
||||
export const WORD = /^\w+$/;
|
||||
export const COLOR = /^#?\w+$/;
|
||||
|
||||
type EnvVarOpts = {
|
||||
default: string | undefined;
|
||||
type EnvVarOpts<T> = {
|
||||
default: T | undefined;
|
||||
legacy: string[] | undefined;
|
||||
validationPattern: RegExp | undefined;
|
||||
parser: ((value: string) => T) | undefined
|
||||
};
|
||||
|
||||
export function envVar(
|
||||
export function envVar<T>(
|
||||
name: string,
|
||||
opts: Partial<EnvVarOpts> = {
|
||||
opts: Partial<EnvVarOpts<T>> = {
|
||||
default: undefined,
|
||||
legacy: undefined,
|
||||
validationPattern: undefined,
|
||||
parser: undefined
|
||||
}
|
||||
) {
|
||||
): T {
|
||||
const result = [name, ...(opts.legacy || [])]
|
||||
.map((it) => ({ key: it, value: process.env[it] }))
|
||||
.find((it) => it.value);
|
||||
@@ -36,17 +38,28 @@ export function envVar(
|
||||
logger.warn(`Configuration key '${result.key}' is deprecated, replace with '${name}'`)
|
||||
}
|
||||
|
||||
return result?.value || opts.default;
|
||||
let value: T | undefined = undefined;
|
||||
|
||||
if(result?.value && opts.parser) {
|
||||
value = opts.parser(result?.value)
|
||||
} else if(result?.value)
|
||||
value = result?.value as any as T
|
||||
|
||||
return value == undefined ? opts.default as T : value;
|
||||
}
|
||||
|
||||
export const bnbEnvVar = (key: string, opts: Partial<EnvVarOpts> = {}) =>
|
||||
export const bnbEnvVar = <T>(key: string, opts: Partial<EnvVarOpts<T>> = {}) =>
|
||||
envVar(`BNB_${key}`, {
|
||||
...opts,
|
||||
legacy: [`BONOB_${key}`, ...(opts.legacy || [])],
|
||||
});
|
||||
|
||||
const asBoolean = (value: string) => value == "true";
|
||||
|
||||
const asInt = (value: string) => Number.parseInt(value);
|
||||
|
||||
export default function () {
|
||||
const port = +bnbEnvVar("PORT", { default: "4534" })!;
|
||||
const port = bnbEnvVar<number>("PORT", { default: 4534, parser: asInt })!;
|
||||
const bonobUrl = bnbEnvVar("URL", {
|
||||
legacy: ["BONOB_WEB_ADDRESS"],
|
||||
default: `http://${hostname()}:${port}`,
|
||||
@@ -62,34 +75,35 @@ export default function () {
|
||||
return {
|
||||
port,
|
||||
bonobUrl: url(bonobUrl),
|
||||
secret: bnbEnvVar("SECRET", { default: "bonob" })!,
|
||||
authTimeout: bnbEnvVar("AUTH_TIMEOUT", { default: "1h" })!,
|
||||
secret: bnbEnvVar<string>("SECRET", { default: "bonob" })!,
|
||||
authTimeout: bnbEnvVar<string>("AUTH_TIMEOUT", { default: "1h" })!,
|
||||
icons: {
|
||||
foregroundColor: bnbEnvVar("ICON_FOREGROUND_COLOR", {
|
||||
foregroundColor: bnbEnvVar<string>("ICON_FOREGROUND_COLOR", {
|
||||
validationPattern: COLOR,
|
||||
}),
|
||||
backgroundColor: bnbEnvVar("ICON_BACKGROUND_COLOR", {
|
||||
backgroundColor: bnbEnvVar<string>("ICON_BACKGROUND_COLOR", {
|
||||
validationPattern: COLOR,
|
||||
}),
|
||||
},
|
||||
logRequests: bnbEnvVar<boolean>("SERVER_LOG_REQUESTS", { default: false, parser: asBoolean }),
|
||||
sonos: {
|
||||
serviceName: bnbEnvVar("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||
serviceName: bnbEnvVar<string>("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||
discovery: {
|
||||
enabled:
|
||||
bnbEnvVar("SONOS_DEVICE_DISCOVERY", { default: "true" }) == "true",
|
||||
seedHost: bnbEnvVar("SONOS_SEED_HOST"),
|
||||
bnbEnvVar<boolean>("SONOS_DEVICE_DISCOVERY", { default: true, parser: asBoolean }),
|
||||
seedHost: bnbEnvVar<string>("SONOS_SEED_HOST"),
|
||||
},
|
||||
autoRegister:
|
||||
bnbEnvVar("SONOS_AUTO_REGISTER", { default: "false" }) == "true",
|
||||
sid: Number(bnbEnvVar("SONOS_SERVICE_ID", { default: "246" })),
|
||||
bnbEnvVar<boolean>("SONOS_AUTO_REGISTER", { default: false, parser: asBoolean }),
|
||||
sid: bnbEnvVar<number>("SONOS_SERVICE_ID", { default: 246, parser: asInt }),
|
||||
},
|
||||
subsonic: {
|
||||
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
|
||||
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
||||
artistImageCache: bnbEnvVar("SUBSONIC_ARTIST_IMAGE_CACHE"),
|
||||
customClientsFor: bnbEnvVar<string>("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
||||
artistImageCache: bnbEnvVar<string>("SUBSONIC_ARTIST_IMAGE_CACHE"),
|
||||
},
|
||||
scrobbleTracks: bnbEnvVar("SCROBBLE_TRACKS", { default: "true" }) == "true",
|
||||
scrobbleTracks: bnbEnvVar<boolean>("SCROBBLE_TRACKS", { default: true, parser: asBoolean }),
|
||||
reportNowPlaying:
|
||||
bnbEnvVar("REPORT_NOW_PLAYING", { default: "true" }) == "true",
|
||||
bnbEnvVar<boolean>("REPORT_NOW_PLAYING", { default: true, parser: asBoolean }),
|
||||
};
|
||||
}
|
||||
|
||||
43
src/i8n.ts
43
src/i8n.ts
@@ -4,7 +4,7 @@ import { option as O } from "fp-ts";
|
||||
import _ from "underscore";
|
||||
|
||||
export type 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 type SUPPORTED_LANG = "en-US" | "nl-NL";
|
||||
export type SUPPORTED_LANG = "en-US" | "da-DK" | "nl-NL";
|
||||
export type KEY =
|
||||
| "AppLinkMessage"
|
||||
| "artists"
|
||||
@@ -88,6 +88,47 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
LOVE: "Love",
|
||||
LOVE_SUCCESS: "Track loved"
|
||||
},
|
||||
"da-DK": {
|
||||
AppLinkMessage: "Forbinder Sonos med $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Kunstnere",
|
||||
albums: "Album",
|
||||
tracks: "Numre",
|
||||
playlists: "Afspilningslister",
|
||||
genres: "Genre",
|
||||
random: "Tilfældig",
|
||||
topRated: "Højst vurderet",
|
||||
recentlyAdded: "Senest tilføjet",
|
||||
recentlyPlayed: "Senest afspillet",
|
||||
mostPlayed: "Flest afspilninger",
|
||||
success: "Succes",
|
||||
failure: "Fejl",
|
||||
expectedConfig: "Forventet konfiguration",
|
||||
existingServiceConfig: "Eksisterende tjeneste konfiguration",
|
||||
noExistingServiceRegistration: "Ingen eksisterende tjeneste registrering",
|
||||
register: "Registrer",
|
||||
removeRegistration: "Fjern registrering",
|
||||
devices: "Enheder",
|
||||
services: "Tjenester",
|
||||
login: "Log på",
|
||||
logInToBonob: "Log på $BNB_SONOS_SERVICE_NAME",
|
||||
username: "Brugernavn",
|
||||
password: "Adgangskode",
|
||||
successfullyRegistered: "Registreret med succes",
|
||||
registrationFailed: "Registrering fejlede!",
|
||||
successfullyRemovedRegistration: "Registrering fjernet med succes",
|
||||
failedToRemoveRegistration: "FJernelse af registrering fejlede!",
|
||||
invalidLinkCode: "Ugyldig linkCode!",
|
||||
loginSuccessful: "Log på succes!",
|
||||
loginFailed: "Log på fejlede!",
|
||||
noSonosDevices: "Ingen Sonos enheder",
|
||||
favourites: "Favoritter",
|
||||
STAR: "Tilføj stjerne",
|
||||
UNSTAR: "Fjern stjerne",
|
||||
STAR_SUCCESS: "Stjerne tilføjet",
|
||||
UNSTAR_SUCCESS: "Stjerne fjernet",
|
||||
LOVE: "Synes godt om",
|
||||
LOVE_SUCCESS: "Syntes godt om"
|
||||
},
|
||||
"nl-NL": {
|
||||
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artiesten",
|
||||
|
||||
@@ -6,7 +6,7 @@ export function debugIt<T>(thing: T): T {
|
||||
}
|
||||
|
||||
const logger = createLogger({
|
||||
level: 'debug',
|
||||
level: process.env["BNB_LOG_LEVEL"] || 'info',
|
||||
format: format.combine(
|
||||
format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
|
||||
@@ -374,7 +374,7 @@ function server(
|
||||
const id = req.params["id"]!;
|
||||
const trace = uuid();
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
||||
req.query
|
||||
)}, headers=${JSON.stringify({ ...req.headers, "bnbt": "*****", "bnbk": "*****" })}`
|
||||
@@ -409,7 +409,7 @@ function server(
|
||||
.then((stream) => ({ musicLibrary: it, stream }))
|
||||
)
|
||||
.then(({ musicLibrary, stream }) => {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`${trace} bnb<- stream response from music service for ${id}, status=${
|
||||
stream.status
|
||||
}, headers=(${JSON.stringify(stream.headers)})`
|
||||
@@ -435,7 +435,7 @@ function server(
|
||||
sendStream: boolean;
|
||||
nowPlaying: boolean;
|
||||
}) => {
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`${trace} bnb-> ${
|
||||
req.path
|
||||
}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||
|
||||
@@ -1066,8 +1066,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
|
||||
soapyService.log = (type, data) => {
|
||||
switch (type) {
|
||||
// routing all soap info messages to debug so less noisy
|
||||
case "info":
|
||||
logger.info({ level: "info", data });
|
||||
logger.debug({ level: "info", data });
|
||||
break;
|
||||
case "warn":
|
||||
logger.warn({ level: "warn", data });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { option as O, taskEither as TE } from "fp-ts";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { ordString } from "fp-ts/lib/Ord";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { Md5 } from "ts-md5/dist/md5";
|
||||
import { Md5 } from "ts-md5";
|
||||
import {
|
||||
Credentials,
|
||||
MusicService,
|
||||
|
||||
@@ -1,58 +1,85 @@
|
||||
import dayjs from "dayjs";
|
||||
import { isChristmas, isCNY, isHalloween, isHoli } from "../src/clock";
|
||||
import { randomInt } from "crypto";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(timezone);
|
||||
|
||||
describe("isChristmas", () => {
|
||||
["2000/12/25", "2022/12/25", "2030/12/25"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isChristmas({ now: () => dayjs(date) })).toEqual(true);
|
||||
import { Clock, isChristmas, isCNY, isCNY_2022, isCNY_2023, isCNY_2024, isCNY_2025, isHalloween, isHoli, isMay4 } from "../src/clock";
|
||||
|
||||
|
||||
|
||||
const randomDate = () => dayjs().subtract(randomInt(1, 1000), 'days');
|
||||
const randomDates = (count: number, exclude: string[]) => {
|
||||
const result: Dayjs[] = [];
|
||||
while(result.length < count) {
|
||||
const next = randomDate();
|
||||
if(!exclude.find(it => dayjs(it).isSame(next, 'date'))) {
|
||||
result.push(next)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function describeFixedDateMonthEvent(
|
||||
name: string,
|
||||
dateMonth: string,
|
||||
f: (clock: Clock) => boolean
|
||||
) {
|
||||
const randomYear = randomInt(2020, 3000);
|
||||
const date = dateMonth.split("/")[0];
|
||||
const month = dateMonth.split("/")[1];
|
||||
|
||||
describe(name, () => {
|
||||
it(`should return true for ${randomYear}-${month}-${date}T00:00:00 ragardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T00:00:00Z`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${randomYear}-${month}-${date}T12:00:00 regardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T12:00:00Z`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${randomYear}-${month}-${date}T23:59:00 regardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T23:59:00`) })).toEqual(true);
|
||||
});
|
||||
|
||||
["2000/12/24", "2000/12/26", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date}`, () => {
|
||||
expect(f({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
["2000/12/24", "2000/12/26", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isChristmas({ now: () => dayjs(date) })).toEqual(false);
|
||||
function describeFixedDateEvent(
|
||||
name: string,
|
||||
dates: string[],
|
||||
f: (clock: Clock) => boolean
|
||||
) {
|
||||
describe(name, () => {
|
||||
dates.forEach((date) => {
|
||||
it(`should return true for ${date}T00:00:00`, () => {
|
||||
expect(f({ now: () => dayjs(`${date}T00:00:00`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${date}T23:59:59`, () => {
|
||||
expect(f({ now: () => dayjs(`${date}T23:59:59`) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
randomDates(10, dates).forEach((date) => {
|
||||
it(`should return false for ${date}`, () => {
|
||||
expect(f({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("isHalloween", () => {
|
||||
["2000/10/31", "2022/10/31", "2030/10/31"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isHalloween({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
describeFixedDateMonthEvent("christmas", "25/12", isChristmas);
|
||||
describeFixedDateMonthEvent("halloween", "31/10", isHalloween);
|
||||
describeFixedDateMonthEvent("may4", "04/05", isMay4);
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isHalloween({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isHoli", () => {
|
||||
["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isHoli({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isHoli({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCNY", () => {
|
||||
["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isCNY({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isCNY({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describeFixedDateEvent("holi", ["2022-03-18", "2023-03-07", "2024-03-25", "2025-03-14"], isHoli);
|
||||
describeFixedDateEvent("cny", ["2022-02-01", "2023-01-22", "2024-02-10", "2025-02-29"], isCNY);
|
||||
describeFixedDateEvent("cny 2022", ["2022-02-01"], isCNY_2022);
|
||||
describeFixedDateEvent("cny 2023", ["2023/01/22"], isCNY_2023);
|
||||
describeFixedDateEvent("cny 2024", ["2024/02/10"], isCNY_2024);
|
||||
describeFixedDateEvent("cny 2025", ["2025/02/29"], isCNY_2025);
|
||||
|
||||
@@ -96,42 +96,35 @@ describe("config", () => {
|
||||
propertyGetter: (config: any) => any
|
||||
) {
|
||||
describe(name, () => {
|
||||
function expecting({
|
||||
value,
|
||||
expected,
|
||||
}: {
|
||||
value: string;
|
||||
expected: boolean;
|
||||
}) {
|
||||
describe(`when value is '${value}'`, () => {
|
||||
it(`should be ${expected}`, () => {
|
||||
process.env[envVar] = value;
|
||||
expect(propertyGetter(config())).toEqual(expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expecting({ value: "", expected: expectedDefault });
|
||||
expecting({ value: "true", expected: true });
|
||||
expecting({ value: "false", expected: false });
|
||||
expecting({ value: "foo", expected: false });
|
||||
it.each([
|
||||
[expectedDefault, ""],
|
||||
[expectedDefault, undefined],
|
||||
[true, "true"],
|
||||
[false, "false"],
|
||||
[false, "foo"],
|
||||
])("should be %s when env var is '%s'", (expected, value) => {
|
||||
process.env[envVar] = value;
|
||||
expect(propertyGetter(config())).toEqual(expected);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
describe("bonobUrl", () => {
|
||||
["BNB_URL", "BONOB_URL", "BONOB_WEB_ADDRESS"].forEach((key) => {
|
||||
describe(`when ${key} is specified`, () => {
|
||||
describe.each([
|
||||
"BNB_URL",
|
||||
"BONOB_URL",
|
||||
"BONOB_WEB_ADDRESS"
|
||||
])("when %s is specified", (k) => {
|
||||
it("should be used", () => {
|
||||
const url = "http://bonob1.example.com:8877/";
|
||||
|
||||
process.env["BNB_URL"] = "";
|
||||
process.env["BONOB_URL"] = "";
|
||||
process.env["BONOB_WEB_ADDRESS"] = "";
|
||||
process.env[key] = url;
|
||||
process.env[k] = url;
|
||||
|
||||
expect(config().bonobUrl.href()).toEqual(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when none of BNB_URL, BONOB_URL, BONOB_WEB_ADDRESS are specified", () => {
|
||||
@@ -165,87 +158,89 @@ describe("config", () => {
|
||||
|
||||
describe("icons", () => {
|
||||
describe("foregroundColor", () => {
|
||||
["BNB_ICON_FOREGROUND_COLOR", "BONOB_ICON_FOREGROUND_COLOR"].forEach(
|
||||
(k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_ICON_FOREGROUND_COLOR",
|
||||
"BONOB_ICON_FOREGROUND_COLOR",
|
||||
])("%s", (k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "pink";
|
||||
expect(config().icons.foregroundColor).toEqual("pink");
|
||||
});
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "pink";
|
||||
expect(config().icons.foregroundColor).toEqual("pink");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.foregroundColor).toEqual("#1db954");
|
||||
});
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.foregroundColor).toEqual("#1db954");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!dfasd";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!dfasd";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("backgroundColor", () => {
|
||||
["BNB_ICON_BACKGROUND_COLOR", "BONOB_ICON_BACKGROUND_COLOR"].forEach(
|
||||
(k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_ICON_BACKGROUND_COLOR",
|
||||
"BONOB_ICON_BACKGROUND_COLOR",
|
||||
])("%s", (k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "blue";
|
||||
expect(config().icons.backgroundColor).toEqual("blue");
|
||||
});
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "blue";
|
||||
expect(config().icons.backgroundColor).toEqual("blue");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.backgroundColor).toEqual("#1db954");
|
||||
});
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.backgroundColor).toEqual("#1db954");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!red";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!red";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,9 +249,12 @@ describe("config", () => {
|
||||
expect(config().secret).toEqual("bonob");
|
||||
});
|
||||
|
||||
["BNB_SECRET", "BONOB_SECRET"].forEach((key) => {
|
||||
it(`should be overridable using ${key}`, () => {
|
||||
process.env[key] = "new secret";
|
||||
describe.each([
|
||||
"BNB_SECRET",
|
||||
"BONOB_SECRET"
|
||||
])("%s", (k) => {
|
||||
it(`should be overridable using ${k}`, () => {
|
||||
process.env[k] = "new secret";
|
||||
expect(config().secret).toEqual("new secret");
|
||||
});
|
||||
});
|
||||
@@ -271,7 +269,16 @@ describe("config", () => {
|
||||
process.env["BNB_AUTH_TIMEOUT"] = "33s";
|
||||
expect(config().authTimeout).toEqual("33s");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRequests", () => {
|
||||
describeBooleanConfigValue(
|
||||
"logRequests",
|
||||
"BNB_SERVER_LOG_REQUESTS",
|
||||
false,
|
||||
(config) => config.logRequests
|
||||
);
|
||||
});
|
||||
|
||||
describe("sonos", () => {
|
||||
describe("serviceName", () => {
|
||||
@@ -279,91 +286,114 @@ describe("config", () => {
|
||||
expect(config().sonos.serviceName).toEqual("bonob");
|
||||
});
|
||||
|
||||
["BNB_SONOS_SERVICE_NAME", "BONOB_SONOS_SERVICE_NAME"].forEach((k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "foobar1000";
|
||||
expect(config().sonos.serviceName).toEqual("foobar1000");
|
||||
});
|
||||
});
|
||||
describe.each([
|
||||
"BNB_SONOS_SERVICE_NAME",
|
||||
"BONOB_SONOS_SERVICE_NAME"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "foobar1000";
|
||||
expect(config().sonos.serviceName).toEqual("foobar1000");
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
["BNB_SONOS_DEVICE_DISCOVERY", "BONOB_SONOS_DEVICE_DISCOVERY"].forEach(
|
||||
(k) => {
|
||||
describeBooleanConfigValue(
|
||||
"deviceDiscovery",
|
||||
k,
|
||||
true,
|
||||
(config) => config.sonos.discovery.enabled
|
||||
);
|
||||
}
|
||||
);
|
||||
describe.each([
|
||||
"BNB_SONOS_DEVICE_DISCOVERY",
|
||||
"BONOB_SONOS_DEVICE_DISCOVERY",
|
||||
])("%s", (k) => {
|
||||
describeBooleanConfigValue(
|
||||
"deviceDiscovery",
|
||||
k,
|
||||
true,
|
||||
(config) => config.sonos.discovery.enabled
|
||||
);
|
||||
});
|
||||
|
||||
describe("seedHost", () => {
|
||||
it("should default to undefined", () => {
|
||||
expect(config().sonos.discovery.seedHost).toBeUndefined();
|
||||
});
|
||||
|
||||
["BNB_SONOS_SEED_HOST", "BONOB_SONOS_SEED_HOST"].forEach((k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "123.456.789.0";
|
||||
expect(config().sonos.discovery.seedHost).toEqual("123.456.789.0");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
["BNB_SONOS_AUTO_REGISTER", "BONOB_SONOS_AUTO_REGISTER"].forEach((k) => {
|
||||
describeBooleanConfigValue(
|
||||
"autoRegister",
|
||||
k,
|
||||
false,
|
||||
(config) => config.sonos.autoRegister
|
||||
describe.each([
|
||||
"BNB_SONOS_SEED_HOST",
|
||||
"BONOB_SONOS_SEED_HOST"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "123.456.789.0";
|
||||
expect(config().sonos.discovery.seedHost).toEqual("123.456.789.0");
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
"BNB_SONOS_AUTO_REGISTER",
|
||||
"BONOB_SONOS_AUTO_REGISTER"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
describeBooleanConfigValue(
|
||||
"autoRegister",
|
||||
k,
|
||||
false,
|
||||
(config) => config.sonos.autoRegister
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
describe("sid", () => {
|
||||
it("should default to 246", () => {
|
||||
expect(config().sonos.sid).toEqual(246);
|
||||
});
|
||||
|
||||
["BNB_SONOS_SERVICE_ID", "BONOB_SONOS_SERVICE_ID"].forEach((k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "786";
|
||||
expect(config().sonos.sid).toEqual(786);
|
||||
});
|
||||
});
|
||||
describe.each([
|
||||
"BNB_SONOS_SERVICE_ID",
|
||||
"BONOB_SONOS_SERVICE_ID"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
it("should be overridable", () => {
|
||||
process.env[k] = "786";
|
||||
expect(config().sonos.sid).toEqual(786);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("subsonic", () => {
|
||||
describe("url", () => {
|
||||
["BNB_SUBSONIC_URL", "BONOB_SUBSONIC_URL", "BONOB_NAVIDROME_URL"].forEach(
|
||||
(k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to http://${hostname()}:4533`, () => {
|
||||
expect(config().subsonic.url).toEqual(
|
||||
`http://${hostname()}:4533`
|
||||
);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_SUBSONIC_URL",
|
||||
"BONOB_SUBSONIC_URL",
|
||||
"BONOB_NAVIDROME_URL",
|
||||
])("%s", (k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to http://${hostname()}:4533`, () => {
|
||||
expect(config().subsonic.url).toEqual(`http://${hostname()}:4533`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to http://${hostname()}:4533`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().subsonic.url).toEqual(
|
||||
`http://${hostname()}:4533`
|
||||
);
|
||||
});
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to http://${hostname()}:4533`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().subsonic.url).toEqual(`http://${hostname()}:4533`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified`, () => {
|
||||
it(`should use it for ${k}`, () => {
|
||||
const url = "http://navidrome.example.com:1234";
|
||||
process.env[k] = url;
|
||||
expect(config().subsonic.url).toEqual(url);
|
||||
});
|
||||
describe(`when ${k} is specified`, () => {
|
||||
it(`should use it for ${k}`, () => {
|
||||
const url = "http://navidrome.example.com:1234";
|
||||
process.env[k] = url;
|
||||
expect(config().subsonic.url).toEqual(url);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("customClientsFor", () => {
|
||||
@@ -371,11 +401,11 @@ describe("config", () => {
|
||||
expect(config().subsonic.customClientsFor).toBeUndefined();
|
||||
});
|
||||
|
||||
[
|
||||
describe.each([
|
||||
"BNB_SUBSONIC_CUSTOM_CLIENTS",
|
||||
"BONOB_SUBSONIC_CUSTOM_CLIENTS",
|
||||
"BONOB_NAVIDROME_CUSTOM_CLIENTS",
|
||||
].forEach((k) => {
|
||||
])("%s", (k) => {
|
||||
it(`should be overridable for ${k}`, () => {
|
||||
process.env[k] = "whoop/whoop";
|
||||
expect(config().subsonic.customClientsFor).toEqual("whoop/whoop");
|
||||
@@ -395,7 +425,10 @@ describe("config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
["BNB_SCROBBLE_TRACKS", "BONOB_SCROBBLE_TRACKS"].forEach((k) => {
|
||||
describe.each([
|
||||
"BNB_SCROBBLE_TRACKS",
|
||||
"BONOB_SCROBBLE_TRACKS"
|
||||
])("%s", (k) => {
|
||||
describeBooleanConfigValue(
|
||||
"scrobbleTracks",
|
||||
k,
|
||||
@@ -404,12 +437,18 @@ describe("config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
["BNB_REPORT_NOW_PLAYING", "BONOB_REPORT_NOW_PLAYING"].forEach((k) => {
|
||||
describeBooleanConfigValue(
|
||||
"reportNowPlaying",
|
||||
k,
|
||||
true,
|
||||
(config) => config.reportNowPlaying
|
||||
);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_REPORT_NOW_PLAYING",
|
||||
"BONOB_REPORT_NOW_PLAYING"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
describeBooleanConfigValue(
|
||||
"reportNowPlaying",
|
||||
k,
|
||||
true,
|
||||
(config) => config.reportNowPlaying
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("i8n", () => {
|
||||
|
||||
describe("langs", () => {
|
||||
it("should be all langs that are explicitly defined", () => {
|
||||
expect(langs()).toEqual(["en-US", "nl-NL"]);
|
||||
expect(langs()).toEqual(["en-US", "da-DK", "nl-NL"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -167,15 +167,13 @@ describe("RangeBytesFromFilter", () => {
|
||||
|
||||
|
||||
describe("server", () => {
|
||||
jest.setTimeout(Number.parseInt(process.env["JEST_TIMEOUT"] || "2000"));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const bonobUrlWithNoContextPath = url("http://bonob.localhost:1234");
|
||||
const bonobUrlWithContextPath = url("http://bonob.localhost:1234/aContext");
|
||||
const bonobUrlWithNoContextPath = url("http://localhost:1234");
|
||||
const bonobUrlWithContextPath = url("http://localhost:1234/aContext");
|
||||
|
||||
const langName = randomLang();
|
||||
const acceptLanguage = `le-ET,${langName};q=0.9,en;q=0.8`;
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("service config", () => {
|
||||
|
||||
describe(STRINGS_ROUTE, () => {
|
||||
it("should return xml for the strings", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
const xml: Document = await fetchStringsXml();
|
||||
|
||||
const sonosString = (id: string, lang: string) =>
|
||||
xpath.select(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Md5 } from "ts-md5/dist/md5";
|
||||
import { Md5 } from "ts-md5";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import tmp from "tmp";
|
||||
import fse from "fs-extra";
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
]
|
||||
/* List of folders to include type definitions from. */,
|
||||
// "types": ["src/customTypes/scale-that-svg.d.ts"], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
Reference in New Issue
Block a user