Compare commits

...

40 Commits

Author SHA1 Message Date
simojenki
8111041551 Additional documentation around where to pull image from 2023-03-18 08:47:16 +11:00
simojenki
df2ef9b152 Add some labels to docker image 2023-03-17 10:37:42 +11:00
Bᴇʀɴᴅ Sᴄʜᴏʀɢᴇʀs
33473cd387 ci: Push image to GHCR (#153)
* ci: Push image to GHCR

* ci: Update build actions
2023-03-17 10:26:43 +11:00
Simon J
7f743aaa7e Change some messages from info to debug, route all soap info to debug (#151) 2023-03-13 08:47:32 +11:00
simojenki
d4bed77c54 Set default log level to info 2023-03-12 07:52:43 +00:00
Simon J
29531a6e01 Ability to configure log level, default to 'warn' (#150) 2023-03-12 13:52:38 +11:00
Simon J
e78b6c4fbc Ability to configure whether to log http requests (#149) 2023-03-12 09:21:49 +11:00
Simon J
2941f6f595 Add wget to image 2023-03-09 06:55:58 +11:00
Daniel Hammer
2c48d08b0e Expanded BNB_SUBSONIC_ARTIST_IMAGE_CACHE description to reflect maintainer insights. (#146)
@see https://github.com/simojenki/bonob/issues/138#issuecomment-1455001557

Co-authored-by: Daniel Hammer <daniel.hammer+oss@gmail.com>
2023-03-08 16:16:03 +11:00
Daniel Hammer
de48ee0fca Added initial da-DK i18n. (#140) 2023-03-06 18:40:08 +11:00
Simon J
cefdf5e2d5 Switch to node:16-bullsys-slim images to reduce final image size (#144) 2023-03-06 18:39:40 +11:00
Daniel Hammer
f86a78b338 Added initial documentation for multiple registrations. (#141)
@see https://github.com/simojenki/bonob/issues/139

Co-authored-by: Daniel Hammer <daniel.hammer+oss@gmail.com>
2023-03-06 10:41:55 +11:00
dependabot[bot]
4d23885d7c Bump @xmldom/xmldom from 0.7.6 to 0.7.9 (#143)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.7.6 to 0.7.9.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.7.6...0.7.9)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 10:41:19 +11:00
dependabot[bot]
8c80c00089 Bump qs from 6.10.1 to 6.11.0 (#142)
Bumps [qs](https://github.com/ljharb/qs) from 6.10.1 to 6.11.0.
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.10.1...v6.11.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 10:41:08 +11:00
dependabot[bot]
ebf385e918 Bump http-cache-semantics from 4.1.0 to 4.1.1 (#133)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 10:40:52 +11:00
dependabot[bot]
a20fdcbc5f Bump eta from 1.12.3 to 2.0.0 (#131)
Bumps [eta](https://github.com/eta-dev/eta) from 1.12.3 to 2.0.0.
- [Release notes](https://github.com/eta-dev/eta/releases)
- [Commits](https://github.com/eta-dev/eta/compare/v1.12.3...v2.0.0)

---
updated-dependencies:
- dependency-name: eta
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 10:40:40 +11:00
dependabot[bot]
f763dbd8b9 Bump cookiejar from 2.1.2 to 2.1.4 (#130)
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.2 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 10:40:25 +11:00
dependabot[bot]
2d3e5dc635 Bump json5 from 2.2.0 to 2.2.3 (#126) 2023-03-06 08:10:21 +11:00
dependabot[bot]
6091308266 Bump jsonwebtoken from 8.5.1 to 9.0.0 (#125) 2023-03-06 08:10:14 +11:00
dependabot[bot]
fed6e9663d Bump express from 4.17.1 to 4.17.3 (#124) 2023-03-06 08:10:01 +11:00
dependabot[bot]
03b5b04c73 Bump minimatch from 3.0.4 to 3.1.2 (#120) 2023-03-06 07:54:15 +11:00
simojenki
4a529b46e1 Fix incorrect boolean usage with docker-compose, had to quote the "true" 2022-11-14 00:34:04 +00:00
simojenki
5c9fbede7a Add devcontainer for building bonob 2022-11-14 00:33:35 +00:00
dependabot[bot]
94e25e03ea Bump follow-redirects from 1.14.3 to 1.15.2 (#119)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.15.2.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.15.2)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-20 19:34:27 +11:00
dependabot[bot]
d9c3a3edcb Bump jpeg-js from 0.4.3 to 0.4.4 (#118)
Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-20 19:34:17 +11:00
dependabot[bot]
f22b094d83 Bump minimist from 1.2.5 to 1.2.7 (#117)
Bumps [minimist](https://github.com/minimistjs/minimist) from 1.2.5 to 1.2.7.
- [Release notes](https://github.com/minimistjs/minimist/releases)
- [Changelog](https://github.com/minimistjs/minimist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/minimistjs/minimist/compare/v1.2.5...v1.2.7)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-20 19:34:06 +11:00
dependabot[bot]
4ae71675e8 Bump async from 3.2.0 to 3.2.4 (#116)
Bumps [async](https://github.com/caolan/async) from 3.2.0 to 3.2.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/master/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v3.2.0...v3.2.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-20 19:33:53 +11:00
simojenki
84866dfd60 Trying to make server tests more stable 2022-10-20 16:35:18 +11:00
simojenki
719fd998b1 Set jest timeout globally for all tests as tests break in GH actions due to timeout 2022-10-20 13:29:37 +11:00
dependabot[bot]
91995678a4 Bump tar from 6.1.8 to 6.1.11 (#115) 2022-10-18 09:06:15 +11:00
dependabot[bot]
67d6c4a730 Bump node-fetch from 2.6.1 to 2.6.7 (#114) 2022-10-18 09:05:58 +11:00
dependabot[bot]
3df4f4daa7 Bump nth-check from 2.0.0 to 2.1.1 (#113) 2022-10-18 09:05:19 +11:00
dependabot[bot]
bd63408ec3 Bump tmpl from 1.0.4 to 1.0.5 (#112) 2022-10-18 09:04:50 +11:00
dependabot[bot]
da5491b474 Bump sharp from 0.29.1 to 0.30.5 (#106) 2022-10-18 09:04:27 +11:00
dependabot[bot]
bbd676b5b8 Bump @xmldom/xmldom from 0.7.4 to 0.7.6 (#111)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.7.4 to 0.7.6.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.7.4...0.7.6)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 08:03:05 +11:00
simojenki
d01c747c96 handling SIGTERM 2022-07-30 17:28:30 +10:00
Laurent le Beau-Martin
192f65a56b Improve ffmpeg command to transcode flac (#99)
* Improve ffmpeg command to transcode flac

The command previously suggested forced the output sample rate to 48 kHz, even if the input was lower, at 44.1 kHz. 
This new command lets `ffmpeg` select the appropriate output sample rate to minimize conversion. 
Documentation: https://www.ffmpeg.org/ffmpeg-filters.html#aformat-1

* Update transcoding command

- Support more sample rates and bit depths.
- Add note about S1
2022-03-10 15:06:56 +11:00
Simon J
9b3df4ce1a Support for using boolean values when using yaml docker-compose files rather than strings for booleans (#98) 2022-02-28 22:07:17 +11:00
Simon J
df9a6d4663 Improve date handling (#94) 2022-02-02 13:26:01 +11:00
simojenki
d0c80b2f20 Add linux/arm64 to platforms supported 2021-12-30 09:30:49 +11:00
27 changed files with 152667 additions and 8576 deletions

15
.devcontainer/Dockerfile Normal file
View 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

View 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
View File

@@ -0,0 +1,6 @@
.devcontainer
.github
.yarn/cache
.yarn/install-state.gz
build
node_modules

View File

@@ -15,54 +15,64 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
-
-
name: Check out the repo
uses: actions/checkout@v2
-
uses: actions/setup-node@v1
uses: actions/checkout@v3
-
uses: actions/setup-node@v3
with:
node-version: '16'
-
-
run: yarn install
-
-
run: yarn test
push_to_registry:
name: Push Docker image to Docker Hub
name: Push Docker image to Docker registries
needs: build_and_test
runs-on: ubuntu-latest
steps:
-
-
name: Check out the repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: simojenki/bonob
images: |
simojenki/bonob
ghcr.io/simojenki/bonob
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Push to Docker Hub
uses: docker/build-push-action@v2
-
name: Log in to GitHub Container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Push image
uses: docker/build-push-action@v4
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 }}
labels: ${{ steps.meta.outputs.labels }}

147529
.yarn/releases/yarn-1.22.19.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-berry.cjs
yarnPath: .yarn/releases/yarn-1.22.19.cjs

View File

@@ -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,29 @@ 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" \
org.opencontainers.image.source="https://github.com/simojenki/bonob" \
org.opencontainers.image.description="bonob SONOS SMAPI implementation" \
org.opencontainers.image.licenses="GPLv3"
ENV BNB_PORT=4534
ENV DEBIAN_FRONTEND=noninteractive
@@ -56,7 +72,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/*

View File

@@ -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
@@ -25,7 +25,23 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
## Running
bonob is distributed via docker and can be run in a number of ways
bonob is packaged as an OCI image to both the docker hub registry and github registry.
ie.
```bash
docker pull docker.io/simojenki/bonob
```
or
```bash
docker pull ghcr.io/simojenki/bonob
```
tag | description
--- | ---
latest | Latest release, intended to be stable
master | Laster build from master, probably works, however is currently under test in
vX.Y.Z | Fixed release versions from tags, for those that want to pin to specific release
### Full sonos device auto-discovery and auto-registration using docker --network host
@@ -126,8 +142,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 +162,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 +171,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 +206,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 +233,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

View File

@@ -5,5 +5,6 @@ module.exports = {
modulePathIgnorePatterns: [
'<rootDir>/node_modules',
'<rootDir>/build',
],
],
testTimeout: Number.parseInt(process.env["JEST_TIMEOUT"] || "5000")
};

View File

@@ -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"
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 }),
};
}

View File

@@ -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",

View File

@@ -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'

View File

@@ -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)}`

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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);

View File

@@ -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
);
}
);
});

View File

@@ -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"]);
});
});

View File

@@ -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`;

View File

@@ -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(

View File

@@ -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";

View File

@@ -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. */

12161
yarn.lock

File diff suppressed because it is too large Load Diff