mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Compare commits
39 Commits
feature/nd
...
v0.6.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9786d9f1dd | ||
|
|
a9d88bd9eb | ||
|
|
f6fc7ab920 | ||
|
|
8111041551 | ||
|
|
df2ef9b152 | ||
|
|
33473cd387 | ||
|
|
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 |
16
.devcontainer/Dockerfile
Normal file
16
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:16-bullseye
|
||||||
|
|
||||||
|
LABEL maintainer=simojenki
|
||||||
|
|
||||||
|
ENV JEST_TIMEOUT=60000
|
||||||
|
EXPOSE 4534
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get -y upgrade && \
|
||||||
|
apt-get -y install --no-install-recommends \
|
||||||
|
libvips-dev \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
vim
|
||||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "bonob",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
|
},
|
||||||
|
"containerEnv": {
|
||||||
|
// these env vars need to be configured appropriately for your local dev env
|
||||||
|
"BNB_DEV_SONOS_DEVICE_IP": "${localEnv:BNB_DEV_SONOS_DEVICE_IP}",
|
||||||
|
"BNB_DEV_HOST_IP": "${localEnv:BNB_DEV_HOST_IP}",
|
||||||
|
"BNB_DEV_SUBSONIC_URL": "${localEnv:BNB_DEV_SUBSONIC_URL}"
|
||||||
|
},
|
||||||
|
"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
|
||||||
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -15,54 +15,64 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Check out the repo
|
name: Check out the repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
-
|
-
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
-
|
-
|
||||||
run: yarn install
|
run: yarn install
|
||||||
-
|
-
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
|
|
||||||
push_to_registry:
|
push_to_registry:
|
||||||
name: Push Docker image to Docker Hub
|
name: Push Docker image to Docker registries
|
||||||
needs: build_and_test
|
needs: build_and_test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Check out the repo
|
name: Check out the repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: simojenki/bonob
|
images: |
|
||||||
|
simojenki/bonob
|
||||||
|
ghcr.io/simojenki/bonob
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Push to Docker Hub
|
name: Log in to GitHub Container registry
|
||||||
uses: docker/build-push-action@v2
|
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:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
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
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
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
yarnPath: .yarn/releases/yarn-1.22.19.cjs
|
||||||
|
|||||||
33
Dockerfile
33
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16-bullseye as build
|
FROM node:16-bullseye-slim as build
|
||||||
|
|
||||||
WORKDIR /bonob
|
WORKDIR /bonob
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ COPY yarn.lock .
|
|||||||
COPY .yarnrc.yml .
|
COPY .yarnrc.yml .
|
||||||
COPY .yarn/releases ./.yarn/releases
|
COPY .yarn/releases ./.yarn/releases
|
||||||
|
|
||||||
ENV JEST_TIMEOUT=30000
|
ENV JEST_TIMEOUT=60000
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
@@ -29,13 +29,29 @@ RUN apt-get update && \
|
|||||||
g++ && \
|
g++ && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
yarn install --immutable && \
|
yarn config set network-timeout 600000 -g && \
|
||||||
yarn gitinfo && \
|
yarn install \
|
||||||
|
--prefer-offline \
|
||||||
|
--frozen-lockfile \
|
||||||
|
--non-interactive \
|
||||||
|
--production=false && \
|
||||||
yarn test --no-cache && \
|
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 BNB_PORT=4534
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
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 && \
|
RUN apt-get update && \
|
||||||
apt-get -y upgrade && \
|
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 && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -16,7 +16,7 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
|
|||||||
- Search by Album, Artist, Track
|
- Search by Album, Artist, Track
|
||||||
- Playlist editing through sonos app.
|
- Playlist editing through sonos app.
|
||||||
- Marking of songs as favourites and with ratings through the 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
|
- Auto discovery of sonos devices
|
||||||
- Discovery of sonos devices using seed IP address
|
- Discovery of sonos devices using seed IP address
|
||||||
- Auto registration with sonos on start
|
- Auto registration with sonos on start
|
||||||
@@ -25,7 +25,23 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
|
|||||||
|
|
||||||
## Running
|
## 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
|
### Full sonos device auto-discovery and auto-registration using docker --network host
|
||||||
|
|
||||||
@@ -126,8 +142,8 @@ services:
|
|||||||
# ip address of your machine running bonob
|
# ip address of your machine running bonob
|
||||||
BNB_URL: http://192.168.1.111:4534
|
BNB_URL: http://192.168.1.111:4534
|
||||||
BNB_SECRET: changeme
|
BNB_SECRET: changeme
|
||||||
BNB_SONOS_AUTO_REGISTER: true
|
BNB_SONOS_AUTO_REGISTER: "true"
|
||||||
BNB_SONOS_DEVICE_DISCOVERY: true
|
BNB_SONOS_DEVICE_DISCOVERY: "true"
|
||||||
BNB_SONOS_SERVICE_ID: 246
|
BNB_SONOS_SERVICE_ID: 246
|
||||||
# ip address of one of your sonos devices
|
# ip address of one of your sonos devices
|
||||||
BNB_SONOS_SEED_HOST: 192.168.1.121
|
BNB_SONOS_SEED_HOST: 192.168.1.121
|
||||||
@@ -146,6 +162,9 @@ 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_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_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_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_DISABLE_PLAYLIST_ART | undefined | Disables playlist art generation, ie. when there are many playlists and art generation takes too long
|
||||||
|
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_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_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
|
BNB_SONOS_SEED_HOST | undefined | sonos device seed host for discovery, or ommitted for for auto-discovery
|
||||||
@@ -153,7 +172,7 @@ BNB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
|||||||
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||||
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
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_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_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_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)
|
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 +207,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.
|
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
|
## Implementing a different music source other than a subsonic clone
|
||||||
|
|
||||||
- Implement the MusicService/MusicLibrary interface
|
- Implement the MusicService/MusicLibrary interface
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ services:
|
|||||||
BNB_URL: http://192.168.1.111:4534
|
BNB_URL: http://192.168.1.111:4534
|
||||||
BNB_SECRET: changeme
|
BNB_SECRET: changeme
|
||||||
BNB_SONOS_SERVICE_ID: 246
|
BNB_SONOS_SERVICE_ID: 246
|
||||||
BNB_SONOS_AUTO_REGISTER: true
|
BNB_SONOS_AUTO_REGISTER: "true"
|
||||||
BNB_SONOS_DEVICE_DISCOVERY: true
|
BNB_SONOS_DEVICE_DISCOVERY: "true"
|
||||||
# ip address of one of your sonos devices
|
# ip address of one of your sonos devices
|
||||||
BNB_SONOS_SEED_HOST: 192.168.1.121
|
BNB_SONOS_SEED_HOST: 192.168.1.121
|
||||||
BNB_SUBSONIC_URL: http://navidrome:4533
|
BNB_SUBSONIC_URL: http://navidrome:4533
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ module.exports = {
|
|||||||
modulePathIgnorePatterns: [
|
modulePathIgnorePatterns: [
|
||||||
'<rootDir>/node_modules',
|
'<rootDir>/node_modules',
|
||||||
'<rootDir>/build',
|
'<rootDir>/build',
|
||||||
],
|
],
|
||||||
|
testTimeout: Number.parseInt(process.env["JEST_TIMEOUT"] || "5000")
|
||||||
};
|
};
|
||||||
91
package.json
91
package.json
@@ -6,64 +6,67 @@
|
|||||||
"author": "simojenki <simojenki@users.noreply.github.com>",
|
"author": "simojenki <simojenki@users.noreply.github.com>",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svrooij/sonos": "^2.4.0",
|
"@svrooij/sonos": "^2.5.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/jws": "^3.2.4",
|
"@types/jws": "^3.2.5",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.4",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.11.7",
|
||||||
"@types/randomstring": "^1.1.8",
|
"@types/randomstring": "^1.1.8",
|
||||||
"@types/sharp": "^0.28.6",
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/underscore": "^1.11.3",
|
"@types/underscore": "^1.11.4",
|
||||||
"@types/uuid": "^8.3.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^0.21.4",
|
"@types/xmldom": "0.1.31",
|
||||||
"dayjs": "^1.10.6",
|
"axios": "^1.3.4",
|
||||||
"eta": "^1.12.3",
|
"dayjs": "^1.11.7",
|
||||||
"express": "^4.17.1",
|
"eta": "^2.0.1",
|
||||||
"fp-ts": "^2.11.1",
|
"express": "^4.18.2",
|
||||||
"fs-extra": "^10.0.0",
|
"fp-ts": "^2.13.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"fs-extra": "^11.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jws": "^4.0.0",
|
"jws": "^4.0.0",
|
||||||
"libxmljs2": "^0.28.0",
|
"libxmljs2": "^0.31.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-html-parser": "^4.1.4",
|
"node-html-parser": "^6.1.5",
|
||||||
"randomstring": "^1.2.1",
|
"randomstring": "^1.2.3",
|
||||||
"sharp": "^0.29.1",
|
"sharp": "^0.31.3",
|
||||||
"soap": "^0.42.0",
|
"soap": "^1.0.0",
|
||||||
"ts-md5": "^1.2.9",
|
"ts-md5": "^1.3.1",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.9.5",
|
||||||
"underscore": "^1.13.1",
|
"underscore": "^1.13.6",
|
||||||
"urn-lib": "^2.0.0",
|
"urn-lib": "^2.0.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^9.0.0",
|
||||||
"winston": "^3.3.3"
|
"winston": "^3.8.2",
|
||||||
|
"xmldom-ts": "^0.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.21",
|
"@types/chai": "^4.3.4",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^10.0.1",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/tmp": "^0.2.1",
|
"@types/tmp": "^0.2.3",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.7",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^6.1.2",
|
||||||
"image-js": "^0.33.0",
|
"image-js": "^0.35.3",
|
||||||
"jest": "^27.1.0",
|
"jest": "^29.4.3",
|
||||||
"nodemon": "^2.0.12",
|
"nodemon": "^2.0.21",
|
||||||
"supertest": "^6.1.6",
|
"supertest": "^6.3.3",
|
||||||
"tmp": "^0.2.1",
|
"tmp": "^0.2.1",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"ts-mockito": "^2.6.1",
|
"ts-mockito": "^2.6.1",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.9.1",
|
||||||
"xmldom-ts": "^0.3.1",
|
"xmldom-ts": "^0.3.1",
|
||||||
"xpath-ts": "^1.3.13"
|
"xpath-ts": "^1.3.13"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -Rf build node_modules",
|
"clean": "rm -Rf build node_modules",
|
||||||
"build": "tsc",
|
"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",
|
"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_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" 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",
|
"devr": "BNB_DISABLE_PLAYLIST_ART=true 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_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_SONOS_AUTO_REGISTER=true BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" nodemon -V ./src/app.ts",
|
||||||
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
|
"register-dev": "ts-node ./src/register.ts http://${BNB_DEV_HOST_IP}:4534",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"gitinfo": "git describe --tags > .gitinfo"
|
"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,
|
clock,
|
||||||
iconColors: config.icons,
|
iconColors: config.icons,
|
||||||
applyContextPath: true,
|
applyContextPath: true,
|
||||||
logRequests: true,
|
logRequests: config.logRequests,
|
||||||
version,
|
version,
|
||||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||||
externalImageResolver: artistImageFetcher
|
externalImageResolver: artistImageFetcher
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.listen(config.port, () => {
|
const expressServer = app.listen(config.port, () => {
|
||||||
logger.info(`Listening on ${config.port} available @ ${config.bonobUrl}`);
|
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}`);
|
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;
|
export default app;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default function () {
|
|||||||
validationPattern: COLOR,
|
validationPattern: COLOR,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
logRequests: bnbEnvVar<boolean>("SERVER_LOG_REQUESTS", { default: false, parser: asBoolean }),
|
||||||
sonos: {
|
sonos: {
|
||||||
serviceName: bnbEnvVar<string>("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
serviceName: bnbEnvVar<string>("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||||
discovery: {
|
discovery: {
|
||||||
|
|||||||
84
src/i8n.ts
84
src/i8n.ts
@@ -4,7 +4,7 @@ import { option as O } from "fp-ts";
|
|||||||
import _ from "underscore";
|
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 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" | "fr-FR" | "nl-NL";
|
||||||
export type KEY =
|
export type KEY =
|
||||||
| "AppLinkMessage"
|
| "AppLinkMessage"
|
||||||
| "artists"
|
| "artists"
|
||||||
@@ -88,6 +88,88 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
|||||||
LOVE: "Love",
|
LOVE: "Love",
|
||||||
LOVE_SUCCESS: "Track loved"
|
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"
|
||||||
|
},
|
||||||
|
"fr-FR": {
|
||||||
|
AppLinkMessage: "Associer Sonos à $BNB_SONOS_SERVICE_NAME",
|
||||||
|
artists: "Artistes",
|
||||||
|
albums: "Albums",
|
||||||
|
tracks: "Pistes",
|
||||||
|
playlists: "Playlists",
|
||||||
|
genres: "Genres",
|
||||||
|
random: "Aléatoire",
|
||||||
|
topRated: "Les mieux notés",
|
||||||
|
recentlyAdded: "Récemment ajouté",
|
||||||
|
recentlyPlayed: "Récemment joué",
|
||||||
|
mostPlayed: "Les plus joué",
|
||||||
|
success: "Succès",
|
||||||
|
failure: "Échec",
|
||||||
|
expectedConfig: "Configuration attendue",
|
||||||
|
existingServiceConfig: "La configuration de service existe",
|
||||||
|
noExistingServiceRegistration: "Aucun enregistrement de service existant",
|
||||||
|
register: "Inscription",
|
||||||
|
removeRegistration: "Supprimer l'inscription",
|
||||||
|
devices: "Appareils",
|
||||||
|
services: "Services",
|
||||||
|
login: "Se connecter",
|
||||||
|
logInToBonob: "Se connecter à $BNB_SONOS_SERVICE_NAME",
|
||||||
|
username: "Nom d'utilisateur",
|
||||||
|
password: "Mot de passe",
|
||||||
|
successfullyRegistered: "Connecté avec succès",
|
||||||
|
registrationFailed: "Échec de la connexion !",
|
||||||
|
successfullyRemovedRegistration: "Inscription supprimée avec succès",
|
||||||
|
failedToRemoveRegistration: "Échec de la suppression de l'inscription !",
|
||||||
|
invalidLinkCode: "Code non valide !",
|
||||||
|
loginSuccessful: "Connexion réussie !",
|
||||||
|
loginFailed: "La connexion a échoué !",
|
||||||
|
noSonosDevices: "Aucun appareil Sonos",
|
||||||
|
favourites: "Favoris",
|
||||||
|
STAR: "Suivre",
|
||||||
|
UNSTAR: "Ne plus suivre",
|
||||||
|
STAR_SUCCESS: "Piste suivie",
|
||||||
|
UNSTAR_SUCCESS: "Piste non suivie",
|
||||||
|
LOVE: "Aimer",
|
||||||
|
LOVE_SUCCESS: "Pistes aimée"
|
||||||
|
},
|
||||||
"nl-NL": {
|
"nl-NL": {
|
||||||
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
||||||
artists: "Artiesten",
|
artists: "Artiesten",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function debugIt<T>(thing: T): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
level: 'debug',
|
level: process.env["BNB_LOG_LEVEL"] || 'info',
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
format.timestamp({
|
format.timestamp({
|
||||||
format: 'YYYY-MM-DD HH:mm:ss'
|
format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ function server(
|
|||||||
const id = req.params["id"]!;
|
const id = req.params["id"]!;
|
||||||
const trace = uuid();
|
const trace = uuid();
|
||||||
|
|
||||||
logger.info(
|
logger.debug(
|
||||||
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
||||||
req.query
|
req.query
|
||||||
)}, headers=${JSON.stringify({ ...req.headers, "bnbt": "*****", "bnbk": "*****" })}`
|
)}, headers=${JSON.stringify({ ...req.headers, "bnbt": "*****", "bnbk": "*****" })}`
|
||||||
@@ -409,7 +409,7 @@ function server(
|
|||||||
.then((stream) => ({ musicLibrary: it, stream }))
|
.then((stream) => ({ musicLibrary: it, stream }))
|
||||||
)
|
)
|
||||||
.then(({ musicLibrary, stream }) => {
|
.then(({ musicLibrary, stream }) => {
|
||||||
logger.info(
|
logger.debug(
|
||||||
`${trace} bnb<- stream response from music service for ${id}, status=${
|
`${trace} bnb<- stream response from music service for ${id}, status=${
|
||||||
stream.status
|
stream.status
|
||||||
}, headers=(${JSON.stringify(stream.headers)})`
|
}, headers=(${JSON.stringify(stream.headers)})`
|
||||||
@@ -435,7 +435,7 @@ function server(
|
|||||||
sendStream: boolean;
|
sendStream: boolean;
|
||||||
nowPlaying: boolean;
|
nowPlaying: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
logger.info(
|
logger.debug(
|
||||||
`${trace} bnb-> ${
|
`${trace} bnb-> ${
|
||||||
req.path
|
req.path
|
||||||
}, status=${status}, headers=${JSON.stringify(headers)}`
|
}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||||
|
|||||||
15
src/smapi.ts
15
src/smapi.ts
@@ -266,6 +266,9 @@ export const playlistAlbumArtURL = (
|
|||||||
bonobUrl: URLBuilder,
|
bonobUrl: URLBuilder,
|
||||||
playlist: Playlist
|
playlist: Playlist
|
||||||
) => {
|
) => {
|
||||||
|
// todo: this should be put into config, or even just removed for the ND music source
|
||||||
|
if(process.env["BNB_DISABLE_PLAYLIST_ART"]) return iconArtURI(bonobUrl, "music");
|
||||||
|
|
||||||
const burns: BUrn[] = uniq(
|
const burns: BUrn[] = uniq(
|
||||||
playlist.entries.filter((it) => it.coverArt != undefined),
|
playlist.entries.filter((it) => it.coverArt != undefined),
|
||||||
(it) => it.album.id
|
(it) => it.album.id
|
||||||
@@ -868,8 +871,13 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.playlists()
|
.playlists()
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
it.map((playlist) =>
|
it.map((playlist) => {
|
||||||
musicLibrary.playlist(playlist.id)
|
return {
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
entries: []
|
||||||
|
};
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1066,8 +1074,9 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
|
|
||||||
soapyService.log = (type, data) => {
|
soapyService.log = (type, data) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
// routing all soap info messages to debug so less noisy
|
||||||
case "info":
|
case "info":
|
||||||
logger.info({ level: "info", data });
|
logger.debug({ level: "info", data });
|
||||||
break;
|
break;
|
||||||
case "warn":
|
case "warn":
|
||||||
logger.warn({ level: "warn", data });
|
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 * as A from "fp-ts/Array";
|
||||||
import { ordString } from "fp-ts/lib/Ord";
|
import { ordString } from "fp-ts/lib/Ord";
|
||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { Md5 } from "ts-md5/dist/md5";
|
import { Md5 } from "ts-md5";
|
||||||
import {
|
import {
|
||||||
Credentials,
|
Credentials,
|
||||||
MusicService,
|
MusicService,
|
||||||
|
|||||||
@@ -270,6 +270,15 @@ describe("config", () => {
|
|||||||
expect(config().authTimeout).toEqual("33s");
|
expect(config().authTimeout).toEqual("33s");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("logRequests", () => {
|
||||||
|
describeBooleanConfigValue(
|
||||||
|
"logRequests",
|
||||||
|
"BNB_SERVER_LOG_REQUESTS",
|
||||||
|
false,
|
||||||
|
(config) => config.logRequests
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("sonos", () => {
|
describe("sonos", () => {
|
||||||
describe("serviceName", () => {
|
describe("serviceName", () => {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe("i8n", () => {
|
|||||||
|
|
||||||
describe("langs", () => {
|
describe("langs", () => {
|
||||||
it("should be all langs that are explicitly defined", () => {
|
it("should be all langs that are explicitly defined", () => {
|
||||||
expect(langs()).toEqual(["en-US", "nl-NL"]);
|
expect(langs()).toEqual(["en-US", "da-DK", "fr-FR", "nl-NL"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -167,15 +167,13 @@ describe("RangeBytesFromFilter", () => {
|
|||||||
|
|
||||||
|
|
||||||
describe("server", () => {
|
describe("server", () => {
|
||||||
jest.setTimeout(Number.parseInt(process.env["JEST_TIMEOUT"] || "2000"));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const bonobUrlWithNoContextPath = url("http://bonob.localhost:1234");
|
const bonobUrlWithNoContextPath = url("http://localhost:1234");
|
||||||
const bonobUrlWithContextPath = url("http://bonob.localhost:1234/aContext");
|
const bonobUrlWithContextPath = url("http://localhost:1234/aContext");
|
||||||
|
|
||||||
const langName = randomLang();
|
const langName = randomLang();
|
||||||
const acceptLanguage = `le-ET,${langName};q=0.9,en;q=0.8`;
|
const acceptLanguage = `le-ET,${langName};q=0.9,en;q=0.8`;
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ describe("rating to and from ints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("service config", () => {
|
describe("service config", () => {
|
||||||
jest.setTimeout(Number.parseInt(process.env["JEST_TIMEOUT"] || "2000"));
|
|
||||||
|
|
||||||
const bonobWithNoContextPath = url("http://localhost:1234");
|
const bonobWithNoContextPath = url("http://localhost:1234");
|
||||||
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
|
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ describe("service config", () => {
|
|||||||
|
|
||||||
describe(STRINGS_ROUTE, () => {
|
describe(STRINGS_ROUTE, () => {
|
||||||
it("should return xml for the strings", async () => {
|
it("should return xml for the strings", async () => {
|
||||||
const xml = await fetchStringsXml();
|
const xml: Document = await fetchStringsXml();
|
||||||
|
|
||||||
const sonosString = (id: string, lang: string) =>
|
const sonosString = (id: string, lang: string) =>
|
||||||
xpath.select(
|
xpath.select(
|
||||||
@@ -137,8 +135,8 @@ describe("service config", () => {
|
|||||||
"Sonos koppelen aan music land"
|
"Sonos koppelen aan music land"
|
||||||
);
|
);
|
||||||
|
|
||||||
// no fr-FR translation, so use en-US
|
// no pt-BR translation, so use en-US
|
||||||
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
|
expect(sonosString("AppLinkMessage", "pt-BR")).toEqual(
|
||||||
"Linking sonos with music land"
|
"Linking sonos with music land"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -521,30 +519,30 @@ describe("playlistAlbumArtURL", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when the playlist has external ids", () => {
|
describe("when the playlist has external ids", () => {
|
||||||
|
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||||
|
const externalArt1 = {
|
||||||
|
system: "external",
|
||||||
|
resource: "http://example.com/image1.jpg",
|
||||||
|
};
|
||||||
|
const externalArt2 = {
|
||||||
|
system: "external",
|
||||||
|
resource: "http://example.com/image2.jpg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const playlist = aPlaylist({
|
||||||
|
entries: [
|
||||||
|
aTrack({
|
||||||
|
coverArt: externalArt1,
|
||||||
|
album: anAlbumSummary({ id: "album1" }),
|
||||||
|
}),
|
||||||
|
aTrack({
|
||||||
|
coverArt: externalArt2,
|
||||||
|
album: anAlbumSummary({ id: "album2" }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
it("should format the url with encrypted urn", () => {
|
it("should format the url with encrypted urn", () => {
|
||||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
|
||||||
const externalArt1 = {
|
|
||||||
system: "external",
|
|
||||||
resource: "http://example.com/image1.jpg",
|
|
||||||
};
|
|
||||||
const externalArt2 = {
|
|
||||||
system: "external",
|
|
||||||
resource: "http://example.com/image2.jpg",
|
|
||||||
};
|
|
||||||
|
|
||||||
const playlist = aPlaylist({
|
|
||||||
entries: [
|
|
||||||
aTrack({
|
|
||||||
coverArt: externalArt1,
|
|
||||||
album: anAlbumSummary({ id: "album1" }),
|
|
||||||
}),
|
|
||||||
aTrack({
|
|
||||||
coverArt: externalArt2,
|
|
||||||
album: anAlbumSummary({ id: "album2" }),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||||
`http://localhost:1234/context-path/art/${encodeURIComponent(
|
`http://localhost:1234/context-path/art/${encodeURIComponent(
|
||||||
formatForURL(externalArt1)
|
formatForURL(externalArt1)
|
||||||
@@ -552,6 +550,26 @@ describe("playlistAlbumArtURL", () => {
|
|||||||
formatForURL(externalArt2)
|
formatForURL(externalArt2)
|
||||||
)}/size/180?search=yes`
|
)}/size/180?search=yes`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when BNB_NO_PLAYLIST_ART is set", () => {
|
||||||
|
const OLD_ENV = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...OLD_ENV };
|
||||||
|
|
||||||
|
process.env["BNB_DISABLE_PLAYLIST_ART"] = "true";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = OLD_ENV;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an icon", () => {
|
||||||
|
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||||
|
`http://localhost:1234/context-path/icon/music/size/legacy?search=yes`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1653,10 +1671,10 @@ describe("wsdl api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("asking for playlists", () => {
|
describe("asking for playlists", () => {
|
||||||
const playlist1 = aPlaylist({ id: "1", name: "pl1" });
|
const playlist1 = aPlaylist({ id: "1", name: "pl1", entries: []});
|
||||||
const playlist2 = aPlaylist({ id: "2", name: "pl2" });
|
const playlist2 = aPlaylist({ id: "2", name: "pl2", entries: []});
|
||||||
const playlist3 = aPlaylist({ id: "3", name: "pl3" });
|
const playlist3 = aPlaylist({ id: "3", name: "pl3", entries: []});
|
||||||
const playlist4 = aPlaylist({ id: "4", name: "pl4" });
|
const playlist4 = aPlaylist({ id: "4", name: "pl4", entries: []});
|
||||||
|
|
||||||
const playlists = [playlist1, playlist2, playlist3, playlist4];
|
const playlists = [playlist1, playlist2, playlist3, playlist4];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Md5 } from "ts-md5/dist/md5";
|
import { Md5 } from "ts-md5";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import tmp from "tmp";
|
import tmp from "tmp";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
|||||||
Reference in New Issue
Block a user