mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Compare commits
49 Commits
v0.6.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e522caeb29 | ||
|
|
5ce2e4efb7 | ||
|
|
8ef9ca80b6 | ||
|
|
a5689c3d4b | ||
|
|
b8caf90e06 | ||
|
|
9b01f07484 | ||
|
|
fb5f8e81ec | ||
|
|
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 | ||
|
|
192f65a56b | ||
|
|
9b3df4ce1a | ||
|
|
df9a6d4663 |
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
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
-
|
||||
@@ -29,37 +29,47 @@ jobs:
|
||||
|
||||
|
||||
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,linux/arm64
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.vscode
|
||||
build
|
||||
ignore
|
||||
.ignore
|
||||
node_modules
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
|
||||
147529
.yarn/releases/yarn-1.22.19.cjs
vendored
Executable file
147529
.yarn/releases/yarn-1.22.19.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
631
.yarn/releases/yarn-berry.cjs
vendored
631
.yarn/releases/yarn-berry.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-berry.cjs
|
||||
yarnPath: .yarn/releases/yarn-1.22.19.cjs
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:16-bullseye as build
|
||||
FROM node:16-bullseye-slim as build
|
||||
|
||||
WORKDIR /bonob
|
||||
|
||||
@@ -16,7 +16,7 @@ COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
|
||||
ENV JEST_TIMEOUT=30000
|
||||
ENV JEST_TIMEOUT=60000
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
@@ -29,13 +29,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/*
|
||||
|
||||
|
||||
45
README.md
45
README.md
@@ -16,7 +16,7 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
|
||||
- Search by Album, Artist, Track
|
||||
- Playlist editing through sonos app.
|
||||
- Marking of songs as favourites and with ratings through the sonos app.
|
||||
- Localization (only en-US & nl-NL supported currently, require translations for other languages). [Sonos localization and supported languages](https://developer.sonos.com/build/content-service-add-features/strings-and-localization/)
|
||||
- Localization (only en-US, da-DK & nl-NL supported currently, require translations for other languages). [Sonos localization and supported languages](https://developer.sonos.com/build/content-service-add-features/strings-and-localization/)
|
||||
- Auto discovery of sonos devices
|
||||
- Discovery of sonos devices using seed IP address
|
||||
- Auto registration with sonos on start
|
||||
@@ -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,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_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_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_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 +172,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 +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.
|
||||
|
||||
## 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 +234,16 @@ In this case you could set;
|
||||
BNB_SUBSONIC_CUSTOM_CLIENTS="audio/flac"
|
||||
```
|
||||
|
||||
This would result in 2 players in Navidrome, one called 'bonob', the other called 'bonob+audio/flac'. You could then configure a custom flac transcoder in Navidrome that re-samples the flacs to a sonos supported format, ie [Using something like this](https://stackoverflow.com/questions/41420391/ffmpeg-flac-24-bit-96khz-to-16-bit-48khz);
|
||||
This would result in 2 players in Navidrome, one called 'bonob', the other called 'bonob+audio/flac'. You could then configure a custom flac transcoder in Navidrome that re-samples the flacs to a sonos supported format, ie [Using something like this](https://stackoverflow.com/questions/41420391/ffmpeg-flac-24-bit-96khz-to-16-bit-48khz) or [this](https://stackoverflow.com/questions/52119489/ffmpeg-limit-audio-sample-rate):
|
||||
|
||||
```bash
|
||||
ffmpeg -i %s -af aresample=resampler=soxr:out_sample_fmt=s16:out_sample_rate=48000 -f flac -
|
||||
ffmpeg -i %s -af aformat=sample_fmts=s16|s32:sample_rates=8000|11025|16000|22050|24000|32000|44100|48000 -f flac -
|
||||
```
|
||||
|
||||
**Note for Sonos S1:** [24-bit depth is only supported by Sonos S2](https://support.sonos.com/s/article/79?language=en_US), so if your system is still on Sonos S1, transcoding should convert all FLACs to 16-bit:
|
||||
|
||||
```bash
|
||||
ffmpeg -i %s -af aformat=sample_fmts=s16:sample_rates=8000|11025|16000|22050|24000|32000|44100|48000 -f flac -
|
||||
```
|
||||
|
||||
### Changing Icon colors
|
||||
|
||||
@@ -5,5 +5,6 @@ module.exports = {
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/node_modules',
|
||||
'<rootDir>/build',
|
||||
],
|
||||
],
|
||||
testTimeout: Number.parseInt(process.env["JEST_TIMEOUT"] || "5000")
|
||||
};
|
||||
92
package.json
92
package.json
@@ -6,64 +6,66 @@
|
||||
"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",
|
||||
"@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",
|
||||
"@svrooij/sonos": "^2.5.0",
|
||||
"@types/express": "^4.17.19",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"@types/jws": "^3.2.6",
|
||||
"@types/morgan": "^1.9.6",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/randomstring": "^1.1.9",
|
||||
"@types/underscore": "^1.11.11",
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@types/xmldom": "0.1.32",
|
||||
"axios": "^1.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eta": "^2.0.1",
|
||||
"express": "^4.18.2",
|
||||
"fp-ts": "^2.16.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jws": "^4.0.0",
|
||||
"libxmljs2": "^0.28.0",
|
||||
"libxmljs2": "^0.32.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.10",
|
||||
"randomstring": "^1.3.0",
|
||||
"sharp": "^0.32.6",
|
||||
"soap": "^1.0.0",
|
||||
"ts-md5": "^1.3.1",
|
||||
"typescript": "^5.2.2",
|
||||
"underscore": "^1.13.6",
|
||||
"urn-lib": "^2.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0",
|
||||
"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.7",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/mocha": "^10.0.2",
|
||||
"@types/supertest": "^2.0.14",
|
||||
"@types/tmp": "^0.2.4",
|
||||
"chai": "^4.3.10",
|
||||
"get-port": "^7.0.0",
|
||||
"image-js": "^0.35.4",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"supertest": "^6.3.3",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"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",
|
||||
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
|
||||
"dev": "BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_SONOS_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" 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://${BNB_DEV_HOST_IP}:4534",
|
||||
"test": "jest",
|
||||
"gitinfo": "git describe --tags > .gitinfo"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
||||
15
src/app.ts
15
src/app.ts
@@ -88,14 +88,14 @@ const app = server(
|
||||
clock,
|
||||
iconColors: config.icons,
|
||||
applyContextPath: true,
|
||||
logRequests: true,
|
||||
logRequests: config.logRequests,
|
||||
version,
|
||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||
externalImageResolver: artistImageFetcher
|
||||
}
|
||||
);
|
||||
|
||||
app.listen(config.port, () => {
|
||||
const expressServer = app.listen(config.port, () => {
|
||||
logger.info(`Listening on ${config.port} available @ ${config.bonobUrl}`);
|
||||
});
|
||||
|
||||
@@ -113,6 +113,15 @@ if (config.sonos.autoRegister) {
|
||||
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM signal received: closing HTTP server');
|
||||
expressServer.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
});
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
export default app;
|
||||
|
||||
44
src/clock.ts
44
src/clock.ts
@@ -1,13 +1,38 @@
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
|
||||
export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() == 11 && clock.now().date() == 25;
|
||||
export const isMay4 = (clock: Clock = SystemClock) => clock.now().month() == 4 && clock.now().date() == 4;
|
||||
export const isHalloween = (clock: Clock = SystemClock) => clock.now().month() == 9 && clock.now().date() == 31
|
||||
export const isHoli = (clock: Clock = SystemClock) => ["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY = (clock: Clock = SystemClock) => ["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
|
||||
export const isCNY_2022 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2022/02/01"))
|
||||
export const isCNY_2023 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2023/01/22"))
|
||||
export const isCNY_2024 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2024/02/10"))
|
||||
function fixedDateMonthEvent(dateMonth: string) {
|
||||
const date = Number.parseInt(dateMonth.split("/")[0]!);
|
||||
const month = Number.parseInt(dateMonth.split("/")[1]!);
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return clock.now().date() == date && clock.now().month() == month - 1;
|
||||
};
|
||||
}
|
||||
|
||||
function fixedDateEvent(date: string) {
|
||||
const dayjsDate = dayjs(date);
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return clock.now().isSame(dayjsDate, "day");
|
||||
};
|
||||
}
|
||||
|
||||
function anyOf(rules: ((clock: Clock) => boolean)[]) {
|
||||
return (clock: Clock = SystemClock) => {
|
||||
return rules.find((rule) => rule(clock)) != undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export const isChristmas = fixedDateMonthEvent("25/12");
|
||||
export const isMay4 = fixedDateMonthEvent("04/05");
|
||||
export const isHalloween = fixedDateMonthEvent("31/10");
|
||||
export const isHoli = anyOf(
|
||||
["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(fixedDateEvent)
|
||||
)
|
||||
|
||||
export const isCNY_2022 = fixedDateEvent("2022/02/01");
|
||||
export const isCNY_2023 = fixedDateEvent("2023/01/22");
|
||||
export const isCNY_2024 = fixedDateEvent("2024/02/10");
|
||||
export const isCNY_2025 = fixedDateEvent("2025/02/29");
|
||||
export const isCNY = anyOf([isCNY_2022, isCNY_2023, isCNY_2024, isCNY_2025]);
|
||||
|
||||
export interface Clock {
|
||||
now(): Dayjs;
|
||||
@@ -22,7 +47,8 @@ export class FixedClock implements Clock {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
add = (t: number, unit: dayjs.UnitTypeShort) => this.time = this.time.add(t, unit)
|
||||
add = (t: number, unit: dayjs.UnitTypeShort) =>
|
||||
(this.time = this.time.add(t, unit));
|
||||
|
||||
now = () => this.time;
|
||||
}
|
||||
@@ -5,20 +5,22 @@ import url from "./url_builder";
|
||||
export const WORD = /^\w+$/;
|
||||
export const COLOR = /^#?\w+$/;
|
||||
|
||||
type EnvVarOpts = {
|
||||
default: string | undefined;
|
||||
type EnvVarOpts<T> = {
|
||||
default: T | undefined;
|
||||
legacy: string[] | undefined;
|
||||
validationPattern: RegExp | undefined;
|
||||
parser: ((value: string) => T) | undefined
|
||||
};
|
||||
|
||||
export function envVar(
|
||||
export function envVar<T>(
|
||||
name: string,
|
||||
opts: Partial<EnvVarOpts> = {
|
||||
opts: Partial<EnvVarOpts<T>> = {
|
||||
default: undefined,
|
||||
legacy: undefined,
|
||||
validationPattern: undefined,
|
||||
parser: undefined
|
||||
}
|
||||
) {
|
||||
): T {
|
||||
const result = [name, ...(opts.legacy || [])]
|
||||
.map((it) => ({ key: it, value: process.env[it] }))
|
||||
.find((it) => it.value);
|
||||
@@ -36,17 +38,28 @@ export function envVar(
|
||||
logger.warn(`Configuration key '${result.key}' is deprecated, replace with '${name}'`)
|
||||
}
|
||||
|
||||
return result?.value || opts.default;
|
||||
let value: T | undefined = undefined;
|
||||
|
||||
if(result?.value && opts.parser) {
|
||||
value = opts.parser(result?.value)
|
||||
} else if(result?.value)
|
||||
value = result?.value as any as T
|
||||
|
||||
return value == undefined ? opts.default as T : value;
|
||||
}
|
||||
|
||||
export const bnbEnvVar = (key: string, opts: Partial<EnvVarOpts> = {}) =>
|
||||
export const bnbEnvVar = <T>(key: string, opts: Partial<EnvVarOpts<T>> = {}) =>
|
||||
envVar(`BNB_${key}`, {
|
||||
...opts,
|
||||
legacy: [`BONOB_${key}`, ...(opts.legacy || [])],
|
||||
});
|
||||
|
||||
const asBoolean = (value: string) => value == "true";
|
||||
|
||||
const asInt = (value: string) => Number.parseInt(value);
|
||||
|
||||
export default function () {
|
||||
const port = +bnbEnvVar("PORT", { default: "4534" })!;
|
||||
const port = bnbEnvVar<number>("PORT", { default: 4534, parser: asInt })!;
|
||||
const bonobUrl = bnbEnvVar("URL", {
|
||||
legacy: ["BONOB_WEB_ADDRESS"],
|
||||
default: `http://${hostname()}:${port}`,
|
||||
@@ -62,34 +75,35 @@ export default function () {
|
||||
return {
|
||||
port,
|
||||
bonobUrl: url(bonobUrl),
|
||||
secret: bnbEnvVar("SECRET", { default: "bonob" })!,
|
||||
authTimeout: bnbEnvVar("AUTH_TIMEOUT", { default: "1h" })!,
|
||||
secret: bnbEnvVar<string>("SECRET", { default: "bonob" })!,
|
||||
authTimeout: bnbEnvVar<string>("AUTH_TIMEOUT", { default: "1h" })!,
|
||||
icons: {
|
||||
foregroundColor: bnbEnvVar("ICON_FOREGROUND_COLOR", {
|
||||
foregroundColor: bnbEnvVar<string>("ICON_FOREGROUND_COLOR", {
|
||||
validationPattern: COLOR,
|
||||
}),
|
||||
backgroundColor: bnbEnvVar("ICON_BACKGROUND_COLOR", {
|
||||
backgroundColor: bnbEnvVar<string>("ICON_BACKGROUND_COLOR", {
|
||||
validationPattern: COLOR,
|
||||
}),
|
||||
},
|
||||
logRequests: bnbEnvVar<boolean>("SERVER_LOG_REQUESTS", { default: false, parser: asBoolean }),
|
||||
sonos: {
|
||||
serviceName: bnbEnvVar("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||
serviceName: bnbEnvVar<string>("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||
discovery: {
|
||||
enabled:
|
||||
bnbEnvVar("SONOS_DEVICE_DISCOVERY", { default: "true" }) == "true",
|
||||
seedHost: bnbEnvVar("SONOS_SEED_HOST"),
|
||||
bnbEnvVar<boolean>("SONOS_DEVICE_DISCOVERY", { default: true, parser: asBoolean }),
|
||||
seedHost: bnbEnvVar<string>("SONOS_SEED_HOST"),
|
||||
},
|
||||
autoRegister:
|
||||
bnbEnvVar("SONOS_AUTO_REGISTER", { default: "false" }) == "true",
|
||||
sid: Number(bnbEnvVar("SONOS_SERVICE_ID", { default: "246" })),
|
||||
bnbEnvVar<boolean>("SONOS_AUTO_REGISTER", { default: false, parser: asBoolean }),
|
||||
sid: bnbEnvVar<number>("SONOS_SERVICE_ID", { default: 246, parser: asInt }),
|
||||
},
|
||||
subsonic: {
|
||||
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
|
||||
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
||||
artistImageCache: bnbEnvVar("SUBSONIC_ARTIST_IMAGE_CACHE"),
|
||||
url: url(bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!),
|
||||
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 }),
|
||||
};
|
||||
}
|
||||
|
||||
84
src/i8n.ts
84
src/i8n.ts
@@ -4,7 +4,7 @@ import { option as O } from "fp-ts";
|
||||
import _ from "underscore";
|
||||
|
||||
export type LANG = "en-US" | "da-DK" | "de-DE" | "es-ES" | "fr-FR" | "it-IT" | "ja-JP" | "nb-NO" | "nl-NL" | "pt-BR" | "sv-SE" | "zh-CN"
|
||||
export type SUPPORTED_LANG = "en-US" | "nl-NL";
|
||||
export type SUPPORTED_LANG = "en-US" | "da-DK" | "fr-FR" | "nl-NL";
|
||||
export type KEY =
|
||||
| "AppLinkMessage"
|
||||
| "artists"
|
||||
@@ -88,6 +88,88 @@ 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"
|
||||
},
|
||||
"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": {
|
||||
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artiesten",
|
||||
|
||||
@@ -6,7 +6,7 @@ export function debugIt<T>(thing: T): T {
|
||||
}
|
||||
|
||||
const logger = createLogger({
|
||||
level: 'debug',
|
||||
level: process.env["BNB_LOG_LEVEL"] || 'info',
|
||||
format: format.combine(
|
||||
format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
|
||||
@@ -307,13 +307,13 @@ function server(
|
||||
return `<Match propname="rating" value="${value}">
|
||||
<Ratings>
|
||||
<Rating Id="${ratingAsInt(
|
||||
nextLove
|
||||
)}" AutoSkip="NEVER" OnSuccessStringId="LOVE_SUCCESS" StringId="LOVE">
|
||||
nextLove
|
||||
)}" AutoSkip="NEVER" OnSuccessStringId="LOVE_SUCCESS" StringId="LOVE">
|
||||
<Icon Controller="universal" LastModified="${LastModified}" Uri="${loveRatingIcon}" />
|
||||
</Rating>
|
||||
<Rating Id="${-ratingAsInt(
|
||||
nextStar
|
||||
)}" AutoSkip="NEVER" OnSuccessStringId="STAR_SUCCESS" StringId="STAR">
|
||||
nextStar
|
||||
)}" AutoSkip="NEVER" OnSuccessStringId="STAR_SUCCESS" StringId="STAR">
|
||||
<Icon Controller="universal" LastModified="${LastModified}" Uri="${starsRatingIcon}" />
|
||||
</Rating>
|
||||
</Ratings>
|
||||
@@ -327,9 +327,9 @@ function server(
|
||||
<Match>
|
||||
<imageSizeMap>
|
||||
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
|
||||
(size) =>
|
||||
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
|
||||
).join("")}
|
||||
(size) =>
|
||||
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
|
||||
).join("")}
|
||||
</imageSizeMap>
|
||||
</Match>
|
||||
</PresentationMap>
|
||||
@@ -338,9 +338,9 @@ function server(
|
||||
<browseIconSizeMap>
|
||||
<sizeEntry size="0" substitution="/size/legacy"/>
|
||||
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
|
||||
(size) =>
|
||||
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
|
||||
).join("")}
|
||||
(size) =>
|
||||
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
|
||||
).join("")}
|
||||
</browseIconSizeMap>
|
||||
</Match>
|
||||
</PresentationMap>
|
||||
@@ -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": "*****" })}`
|
||||
@@ -406,13 +406,17 @@ function server(
|
||||
trackId: id,
|
||||
range: req.headers["range"] || undefined,
|
||||
})
|
||||
.then((stream) => {
|
||||
res.on('close', () => {
|
||||
stream.stream.destroy()
|
||||
});
|
||||
return stream;
|
||||
})
|
||||
.then((stream) => ({ musicLibrary: it, stream }))
|
||||
)
|
||||
.then(({ musicLibrary, stream }) => {
|
||||
logger.info(
|
||||
`${trace} bnb<- stream response from music service for ${id}, status=${
|
||||
stream.status
|
||||
}, headers=(${JSON.stringify(stream.headers)})`
|
||||
logger.debug(
|
||||
`${trace} bnb<- stream response from music service for ${id}, status=${stream.status}, headers=(${JSON.stringify(stream.headers)})`
|
||||
);
|
||||
|
||||
const sonosisfyContentType = (contentType: string) =>
|
||||
@@ -435,10 +439,8 @@ function server(
|
||||
sendStream: boolean;
|
||||
nowPlaying: boolean;
|
||||
}) => {
|
||||
logger.info(
|
||||
`${trace} bnb-> ${
|
||||
req.path
|
||||
}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||
logger.debug(
|
||||
`${trace} bnb-> ${req.path}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||
);
|
||||
(nowPlaying
|
||||
? musicLibrary.nowPlaying(id)
|
||||
@@ -450,8 +452,8 @@ function server(
|
||||
.forEach(([header, value]) => {
|
||||
res.setHeader(header, value!);
|
||||
});
|
||||
if (sendStream) stream.stream.pipe(filter).pipe(res);
|
||||
else res.send();
|
||||
if (sendStream) stream.stream.pipe(filter).pipe(res)
|
||||
else res.send()
|
||||
});
|
||||
};
|
||||
|
||||
@@ -513,15 +515,15 @@ function server(
|
||||
const spec =
|
||||
size == "legacy"
|
||||
? {
|
||||
mimeType: "image/png",
|
||||
responseFormatter: (svg: string): Promise<Buffer | string> =>
|
||||
sharp(Buffer.from(svg)).resize(80).png().toBuffer(),
|
||||
}
|
||||
mimeType: "image/png",
|
||||
responseFormatter: (svg: string): Promise<Buffer | string> =>
|
||||
sharp(Buffer.from(svg)).resize(80).png().toBuffer(),
|
||||
}
|
||||
: {
|
||||
mimeType: "image/svg+xml",
|
||||
responseFormatter: (svg: string): Promise<Buffer | string> =>
|
||||
Promise.resolve(svg),
|
||||
};
|
||||
mimeType: "image/svg+xml",
|
||||
responseFormatter: (svg: string): Promise<Buffer | string> =>
|
||||
Promise.resolve(svg),
|
||||
};
|
||||
|
||||
return Promise.resolve(
|
||||
icon
|
||||
|
||||
15
src/smapi.ts
15
src/smapi.ts
@@ -266,6 +266,9 @@ export const playlistAlbumArtURL = (
|
||||
bonobUrl: URLBuilder,
|
||||
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(
|
||||
playlist.entries.filter((it) => it.coverArt != undefined),
|
||||
(it) => it.album.id
|
||||
@@ -868,8 +871,13 @@ function bindSmapiSoapServiceToExpress(
|
||||
.playlists()
|
||||
.then((it) =>
|
||||
Promise.all(
|
||||
it.map((playlist) =>
|
||||
musicLibrary.playlist(playlist.id)
|
||||
it.map((playlist) => {
|
||||
return {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
entries: []
|
||||
};
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1066,8 +1074,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
|
||||
soapyService.log = (type, data) => {
|
||||
switch (type) {
|
||||
// routing all soap info messages to debug so less noisy
|
||||
case "info":
|
||||
logger.info({ level: "info", data });
|
||||
logger.debug({ level: "info", data });
|
||||
break;
|
||||
case "warn":
|
||||
logger.warn({ level: "warn", data });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { option as O, taskEither as TE } from "fp-ts";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { ordString } from "fp-ts/lib/Ord";
|
||||
import { pipe } from "fp-ts/lib/function";
|
||||
import { Md5 } from "ts-md5/dist/md5";
|
||||
import { Md5 } from "ts-md5";
|
||||
import {
|
||||
Credentials,
|
||||
MusicService,
|
||||
@@ -32,6 +32,7 @@ import { b64Encode, b64Decode } from "./b64";
|
||||
import logger from "./logger";
|
||||
import { assertSystem, BUrn } from "./burn";
|
||||
import { artist } from "./smapi";
|
||||
import { URLBuilder } from "./url_builder";
|
||||
|
||||
export const BROWSER_HEADERS = {
|
||||
accept:
|
||||
@@ -344,28 +345,28 @@ export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
|
||||
|
||||
export const cachingImageFetcher =
|
||||
(cacheDir: string, delegate: ImageFetcher) =>
|
||||
async (url: string): Promise<CoverArt | undefined> => {
|
||||
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
||||
return fse
|
||||
.readFile(filename)
|
||||
.then((data) => ({ contentType: "image/png", data }))
|
||||
.catch(() =>
|
||||
delegate(url).then((image) => {
|
||||
if (image) {
|
||||
return sharp(image.data)
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then((png) => {
|
||||
return fse
|
||||
.writeFile(filename, png)
|
||||
.then(() => ({ contentType: "image/png", data: png }));
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
async (url: string): Promise<CoverArt | undefined> => {
|
||||
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
||||
return fse
|
||||
.readFile(filename)
|
||||
.then((data) => ({ contentType: "image/png", data }))
|
||||
.catch(() =>
|
||||
delegate(url).then((image) => {
|
||||
if (image) {
|
||||
return sharp(image.data)
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then((png) => {
|
||||
return fse
|
||||
.writeFile(filename, png)
|
||||
.then(() => ({ contentType: "image/png", data: png }));
|
||||
});
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const axiosImageFetcher = (url: string): Promise<CoverArt | undefined> =>
|
||||
axios
|
||||
@@ -412,12 +413,12 @@ interface SubsonicMusicLibrary extends MusicLibrary {
|
||||
}
|
||||
|
||||
export class Subsonic implements MusicService {
|
||||
url: string;
|
||||
url: URLBuilder;
|
||||
streamClientApplication: StreamClientApplication;
|
||||
externalImageFetcher: ImageFetcher;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
url: URLBuilder,
|
||||
streamClientApplication: StreamClientApplication = DEFAULT,
|
||||
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
||||
) {
|
||||
@@ -433,7 +434,7 @@ export class Subsonic implements MusicService {
|
||||
config: AxiosRequestConfig | undefined = {}
|
||||
) =>
|
||||
axios
|
||||
.get(`${this.url}${path}`, {
|
||||
.get(this.url.append({ pathname: path }).href(), {
|
||||
params: asURLSearchParams({
|
||||
u: username,
|
||||
v: "1.16.1",
|
||||
@@ -955,6 +956,7 @@ export class Subsonic implements MusicService {
|
||||
};
|
||||
|
||||
if (credentials.type == "navidrome") {
|
||||
// todo: there does not seem to be a test for this??
|
||||
return Promise.resolve({
|
||||
...genericSubsonic,
|
||||
flavour: () => "navidrome",
|
||||
@@ -963,7 +965,7 @@ export class Subsonic implements MusicService {
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
axios.post(
|
||||
`${this.url}/auth/login`,
|
||||
this.url.append({ pathname: '/auth/login' }).href(),
|
||||
_.pick(credentials, "username", "password")
|
||||
),
|
||||
() => new AuthFailure("Failed to get bearerToken")
|
||||
|
||||
@@ -1,58 +1,85 @@
|
||||
import dayjs from "dayjs";
|
||||
import { isChristmas, isCNY, isHalloween, isHoli } from "../src/clock";
|
||||
import { randomInt } from "crypto";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
dayjs.extend(timezone);
|
||||
|
||||
describe("isChristmas", () => {
|
||||
["2000/12/25", "2022/12/25", "2030/12/25"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isChristmas({ now: () => dayjs(date) })).toEqual(true);
|
||||
import { Clock, isChristmas, isCNY, isCNY_2022, isCNY_2023, isCNY_2024, isCNY_2025, isHalloween, isHoli, isMay4 } from "../src/clock";
|
||||
|
||||
|
||||
|
||||
const randomDate = () => dayjs().subtract(randomInt(1, 1000), 'days');
|
||||
const randomDates = (count: number, exclude: string[]) => {
|
||||
const result: Dayjs[] = [];
|
||||
while(result.length < count) {
|
||||
const next = randomDate();
|
||||
if(!exclude.find(it => dayjs(it).isSame(next, 'date'))) {
|
||||
result.push(next)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function describeFixedDateMonthEvent(
|
||||
name: string,
|
||||
dateMonth: string,
|
||||
f: (clock: Clock) => boolean
|
||||
) {
|
||||
const randomYear = randomInt(2020, 3000);
|
||||
const date = dateMonth.split("/")[0];
|
||||
const month = dateMonth.split("/")[1];
|
||||
|
||||
describe(name, () => {
|
||||
it(`should return true for ${randomYear}-${month}-${date}T00:00:00 ragardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T00:00:00Z`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${randomYear}-${month}-${date}T12:00:00 regardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T12:00:00Z`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${randomYear}-${month}-${date}T23:59:00 regardless of year`, () => {
|
||||
expect(f({ now: () => dayjs(`${randomYear}-${month}-${date}T23:59:00`) })).toEqual(true);
|
||||
});
|
||||
|
||||
["2000/12/24", "2000/12/26", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date}`, () => {
|
||||
expect(f({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
["2000/12/24", "2000/12/26", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isChristmas({ now: () => dayjs(date) })).toEqual(false);
|
||||
function describeFixedDateEvent(
|
||||
name: string,
|
||||
dates: string[],
|
||||
f: (clock: Clock) => boolean
|
||||
) {
|
||||
describe(name, () => {
|
||||
dates.forEach((date) => {
|
||||
it(`should return true for ${date}T00:00:00`, () => {
|
||||
expect(f({ now: () => dayjs(`${date}T00:00:00`) })).toEqual(true);
|
||||
});
|
||||
|
||||
it(`should return true for ${date}T23:59:59`, () => {
|
||||
expect(f({ now: () => dayjs(`${date}T23:59:59`) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
randomDates(10, dates).forEach((date) => {
|
||||
it(`should return false for ${date}`, () => {
|
||||
expect(f({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("isHalloween", () => {
|
||||
["2000/10/31", "2022/10/31", "2030/10/31"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isHalloween({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
describeFixedDateMonthEvent("christmas", "25/12", isChristmas);
|
||||
describeFixedDateMonthEvent("halloween", "31/10", isHalloween);
|
||||
describeFixedDateMonthEvent("may4", "04/05", isMay4);
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isHalloween({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isHoli", () => {
|
||||
["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isHoli({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isHoli({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCNY", () => {
|
||||
["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].forEach((date) => {
|
||||
it(`should return true for ${date} regardless of year`, () => {
|
||||
expect(isCNY({ now: () => dayjs(date) })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
|
||||
it(`should return false for ${date} regardless of year`, () => {
|
||||
expect(isCNY({ now: () => dayjs(date) })).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describeFixedDateEvent("holi", ["2022-03-18", "2023-03-07", "2024-03-25", "2025-03-14"], isHoli);
|
||||
describeFixedDateEvent("cny", ["2022-02-01", "2023-01-22", "2024-02-10", "2025-02-29"], isCNY);
|
||||
describeFixedDateEvent("cny 2022", ["2022-02-01"], isCNY_2022);
|
||||
describeFixedDateEvent("cny 2023", ["2023/01/22"], isCNY_2023);
|
||||
describeFixedDateEvent("cny 2024", ["2024/02/10"], isCNY_2024);
|
||||
describeFixedDateEvent("cny 2025", ["2025/02/29"], isCNY_2025);
|
||||
|
||||
@@ -96,42 +96,35 @@ describe("config", () => {
|
||||
propertyGetter: (config: any) => any
|
||||
) {
|
||||
describe(name, () => {
|
||||
function expecting({
|
||||
value,
|
||||
expected,
|
||||
}: {
|
||||
value: string;
|
||||
expected: boolean;
|
||||
}) {
|
||||
describe(`when value is '${value}'`, () => {
|
||||
it(`should be ${expected}`, () => {
|
||||
process.env[envVar] = value;
|
||||
expect(propertyGetter(config())).toEqual(expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
expecting({ value: "", expected: expectedDefault });
|
||||
expecting({ value: "true", expected: true });
|
||||
expecting({ value: "false", expected: false });
|
||||
expecting({ value: "foo", expected: false });
|
||||
it.each([
|
||||
[expectedDefault, ""],
|
||||
[expectedDefault, undefined],
|
||||
[true, "true"],
|
||||
[false, "false"],
|
||||
[false, "foo"],
|
||||
])("should be %s when env var is '%s'", (expected, value) => {
|
||||
process.env[envVar] = value;
|
||||
expect(propertyGetter(config())).toEqual(expected);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
describe("bonobUrl", () => {
|
||||
["BNB_URL", "BONOB_URL", "BONOB_WEB_ADDRESS"].forEach((key) => {
|
||||
describe(`when ${key} is specified`, () => {
|
||||
describe.each([
|
||||
"BNB_URL",
|
||||
"BONOB_URL",
|
||||
"BONOB_WEB_ADDRESS"
|
||||
])("when %s is specified", (k) => {
|
||||
it("should be used", () => {
|
||||
const url = "http://bonob1.example.com:8877/";
|
||||
|
||||
process.env["BNB_URL"] = "";
|
||||
process.env["BONOB_URL"] = "";
|
||||
process.env["BONOB_WEB_ADDRESS"] = "";
|
||||
process.env[key] = url;
|
||||
process.env[k] = url;
|
||||
|
||||
expect(config().bonobUrl.href()).toEqual(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when none of BNB_URL, BONOB_URL, BONOB_WEB_ADDRESS are specified", () => {
|
||||
@@ -165,87 +158,89 @@ describe("config", () => {
|
||||
|
||||
describe("icons", () => {
|
||||
describe("foregroundColor", () => {
|
||||
["BNB_ICON_FOREGROUND_COLOR", "BONOB_ICON_FOREGROUND_COLOR"].forEach(
|
||||
(k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_ICON_FOREGROUND_COLOR",
|
||||
"BONOB_ICON_FOREGROUND_COLOR",
|
||||
])("%s", (k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "pink";
|
||||
expect(config().icons.foregroundColor).toEqual("pink");
|
||||
});
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "pink";
|
||||
expect(config().icons.foregroundColor).toEqual("pink");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.foregroundColor).toEqual("#1db954");
|
||||
});
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.foregroundColor).toEqual("#1db954");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!dfasd";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!dfasd";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("backgroundColor", () => {
|
||||
["BNB_ICON_BACKGROUND_COLOR", "BONOB_ICON_BACKGROUND_COLOR"].forEach(
|
||||
(k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_ICON_BACKGROUND_COLOR",
|
||||
"BONOB_ICON_BACKGROUND_COLOR",
|
||||
])("%s", (k) => {
|
||||
describe(`when ${k} is not specified`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
describe(`when ${k} is ''`, () => {
|
||||
it(`should default to undefined`, () => {
|
||||
process.env[k] = "";
|
||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "blue";
|
||||
expect(config().icons.backgroundColor).toEqual("blue");
|
||||
});
|
||||
describe(`when ${k} is specified as a color`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "blue";
|
||||
expect(config().icons.backgroundColor).toEqual("blue");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.backgroundColor).toEqual("#1db954");
|
||||
});
|
||||
describe(`when ${k} is specified as hex`, () => {
|
||||
it(`should use it`, () => {
|
||||
process.env[k] = "#1db954";
|
||||
expect(config().icons.backgroundColor).toEqual("#1db954");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!red";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
describe(`when ${k} is an invalid string`, () => {
|
||||
it(`should blow up`, () => {
|
||||
process.env[k] = "!red";
|
||||
expect(() => config()).toThrow(
|
||||
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${COLOR}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,9 +249,12 @@ describe("config", () => {
|
||||
expect(config().secret).toEqual("bonob");
|
||||
});
|
||||
|
||||
["BNB_SECRET", "BONOB_SECRET"].forEach((key) => {
|
||||
it(`should be overridable using ${key}`, () => {
|
||||
process.env[key] = "new secret";
|
||||
describe.each([
|
||||
"BNB_SECRET",
|
||||
"BONOB_SECRET"
|
||||
])("%s", (k) => {
|
||||
it(`should be overridable using ${k}`, () => {
|
||||
process.env[k] = "new secret";
|
||||
expect(config().secret).toEqual("new secret");
|
||||
});
|
||||
});
|
||||
@@ -271,7 +269,16 @@ describe("config", () => {
|
||||
process.env["BNB_AUTH_TIMEOUT"] = "33s";
|
||||
expect(config().authTimeout).toEqual("33s");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("logRequests", () => {
|
||||
describeBooleanConfigValue(
|
||||
"logRequests",
|
||||
"BNB_SERVER_LOG_REQUESTS",
|
||||
false,
|
||||
(config) => config.logRequests
|
||||
);
|
||||
});
|
||||
|
||||
describe("sonos", () => {
|
||||
describe("serviceName", () => {
|
||||
@@ -279,91 +286,122 @@ 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.href()).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.href()).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/some-context-path";
|
||||
process.env[k] = url;
|
||||
expect(config().subsonic.url.href()).toEqual(url);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe(`when ${k} is specified with trailing slash`, () => {
|
||||
it(`should maintain the trailing slash as URLBuilder will remove it when required ${k}`, () => {
|
||||
const url = "http://navidrome.example.com:1234/";
|
||||
process.env[k] = url;
|
||||
expect(config().subsonic.url.href()).toEqual(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("customClientsFor", () => {
|
||||
@@ -371,11 +409,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 +433,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 +445,18 @@ describe("config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
["BNB_REPORT_NOW_PLAYING", "BONOB_REPORT_NOW_PLAYING"].forEach((k) => {
|
||||
describeBooleanConfigValue(
|
||||
"reportNowPlaying",
|
||||
k,
|
||||
true,
|
||||
(config) => config.reportNowPlaying
|
||||
);
|
||||
});
|
||||
describe.each([
|
||||
"BNB_REPORT_NOW_PLAYING",
|
||||
"BONOB_REPORT_NOW_PLAYING"
|
||||
])(
|
||||
"%s",
|
||||
(k) => {
|
||||
describeBooleanConfigValue(
|
||||
"reportNowPlaying",
|
||||
k,
|
||||
true,
|
||||
(config) => config.reportNowPlaying
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("i8n", () => {
|
||||
|
||||
describe("langs", () => {
|
||||
it("should be all langs that are explicitly defined", () => {
|
||||
expect(langs()).toEqual(["en-US", "nl-NL"]);
|
||||
expect(langs()).toEqual(["en-US", "da-DK", "fr-FR", "nl-NL"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -167,15 +167,13 @@ describe("RangeBytesFromFilter", () => {
|
||||
|
||||
|
||||
describe("server", () => {
|
||||
jest.setTimeout(Number.parseInt(process.env["JEST_TIMEOUT"] || "2000"));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const bonobUrlWithNoContextPath = url("http://bonob.localhost:1234");
|
||||
const bonobUrlWithContextPath = url("http://bonob.localhost:1234/aContext");
|
||||
const bonobUrlWithNoContextPath = url("http://localhost:1234");
|
||||
const bonobUrlWithContextPath = url("http://localhost:1234/aContext");
|
||||
|
||||
const langName = randomLang();
|
||||
const acceptLanguage = `le-ET,${langName};q=0.9,en;q=0.8`;
|
||||
@@ -757,15 +755,22 @@ describe("server", () => {
|
||||
const trackId = `t-${uuid()}`;
|
||||
const smapiAuthToken: SmapiToken = { token: `token-${uuid()}`, key: `key-${uuid()}` };
|
||||
|
||||
const streamContent = (content: string) => ({
|
||||
pipe: (_: Transform) => {
|
||||
return {
|
||||
pipe: (res: Response) => {
|
||||
res.send(content);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const streamContent = (content: string) => {
|
||||
const self = {
|
||||
destroyed: false,
|
||||
pipe: (_: Transform) => {
|
||||
return {
|
||||
pipe: (res: Response) => {
|
||||
res.send(content);
|
||||
}
|
||||
};
|
||||
},
|
||||
destroy: () => {
|
||||
self.destroyed = true;
|
||||
}
|
||||
};
|
||||
return self;
|
||||
};
|
||||
|
||||
describe("HEAD requests", () => {
|
||||
describe("when there is no Bearer token", () => {
|
||||
@@ -831,6 +836,8 @@ describe("server", () => {
|
||||
);
|
||||
expect(res.headers["content-length"]).toEqual("123");
|
||||
expect(res.body).toEqual({});
|
||||
|
||||
expect(trackStream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -856,6 +863,8 @@ describe("server", () => {
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
expect(res.body).toEqual({});
|
||||
|
||||
expect(trackStream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -918,6 +927,8 @@ describe("server", () => {
|
||||
|
||||
expect(musicLibrary.nowPlaying).not.toHaveBeenCalled();
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -961,6 +972,8 @@ describe("server", () => {
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1002,6 +1015,8 @@ describe("server", () => {
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1042,6 +1057,8 @@ describe("server", () => {
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1085,6 +1102,8 @@ describe("server", () => {
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1133,6 +1152,8 @@ describe("server", () => {
|
||||
trackId,
|
||||
range: requestedRange,
|
||||
});
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1180,6 +1201,8 @@ describe("server", () => {
|
||||
trackId,
|
||||
range: "4000-5000",
|
||||
});
|
||||
|
||||
expect(stream.stream.destroyed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
@@ -135,8 +135,8 @@ describe("service config", () => {
|
||||
"Sonos koppelen aan music land"
|
||||
);
|
||||
|
||||
// no fr-FR translation, so use en-US
|
||||
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
|
||||
// no pt-BR translation, so use en-US
|
||||
expect(sonosString("AppLinkMessage", "pt-BR")).toEqual(
|
||||
"Linking sonos with music land"
|
||||
);
|
||||
});
|
||||
@@ -519,30 +519,30 @@ describe("playlistAlbumArtURL", () => {
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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(
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(
|
||||
formatForURL(externalArt1)
|
||||
@@ -551,6 +551,26 @@ describe("playlistAlbumArtURL", () => {
|
||||
)}/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`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 4 tracks from 2 different albums, including some tracks that are missing coverArt urns", () => {
|
||||
@@ -1651,10 +1671,10 @@ describe("wsdl api", () => {
|
||||
});
|
||||
|
||||
describe("asking for playlists", () => {
|
||||
const playlist1 = aPlaylist({ id: "1", name: "pl1" });
|
||||
const playlist2 = aPlaylist({ id: "2", name: "pl2" });
|
||||
const playlist3 = aPlaylist({ id: "3", name: "pl3" });
|
||||
const playlist4 = aPlaylist({ id: "4", name: "pl4" });
|
||||
const playlist1 = aPlaylist({ id: "1", name: "pl1", entries: []});
|
||||
const playlist2 = aPlaylist({ id: "2", name: "pl2", entries: []});
|
||||
const playlist3 = aPlaylist({ id: "3", name: "pl3", entries: []});
|
||||
const playlist4 = aPlaylist({ id: "4", name: "pl4", entries: []});
|
||||
|
||||
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 tmp from "tmp";
|
||||
import fse from "fs-extra";
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from "./builders";
|
||||
import { b64Encode } from "../src/b64";
|
||||
import { BUrn } from "../src/burn";
|
||||
import { URLBuilder } from "../src/url_builder";
|
||||
|
||||
describe("t", () => {
|
||||
it("should be an md5 of the password and the salt", () => {
|
||||
@@ -688,7 +689,7 @@ describe("asTrack", () => {
|
||||
});
|
||||
|
||||
describe("Subsonic", () => {
|
||||
const url = "http://127.0.0.22:4567";
|
||||
const url = new URLBuilder("http://127.0.0.22:4567/some-context-path");
|
||||
const username = `user1-${uuid()}`;
|
||||
const password = `pass1-${uuid()}`;
|
||||
const salt = "saltysalty";
|
||||
@@ -756,7 +757,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(parseToken(token.serviceToken)).toEqual({ username, password, type: PING_OK["subsonic-response"].type })
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/ping.view' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -777,7 +778,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(parseToken(token.serviceToken)).toEqual({ username, password, type })
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/ping.view' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -802,11 +803,11 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(parseToken(token.serviceToken)).toEqual({ username, password, type: "navidrome", bearer: navidromeToken })
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/ping.view' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
expect(axios.post).toHaveBeenCalledWith(`${url}/auth/login`, {
|
||||
expect(axios.post).toHaveBeenCalledWith(url.append({ pathname: '/auth/login' }).href(), {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
@@ -848,7 +849,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(parseToken(refreshedToken.serviceToken)).toEqual({ username, password, type })
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/ping.view' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -876,11 +877,11 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(parseToken(refreshedToken.serviceToken)).toEqual({ username, password, type: "navidrome", bearer: navidromeToken })
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/ping.view' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
expect(axios.post).toHaveBeenCalledWith(`${url}/auth/login`, {
|
||||
expect(axios.post).toHaveBeenCalledWith(url.append({ pathname: '/auth/login' }).href(), {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
@@ -927,6 +928,34 @@ describe("Subsonic", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("bearerToken", () => {
|
||||
describe("when flavour is generic subsonic", () => {
|
||||
it("should return undefined", async () => {
|
||||
const credentials = { username: "foo", password: "bar" };
|
||||
const token = { ...credentials, type: "subsonic", bearer: undefined }
|
||||
const client = await subsonic.login(asToken(token));
|
||||
|
||||
const bearerToken = await pipe(client.bearerToken(credentials))();
|
||||
expect(bearerToken).toStrictEqual(E.right(undefined));
|
||||
});
|
||||
});
|
||||
|
||||
describe("when flavour is navidrome", () => {
|
||||
it("should get a bearerToken from navidrome", async () => {
|
||||
const credentials = { username: "foo", password: "bar" };
|
||||
const token = { ...credentials, type: "navidrome", bearer: undefined }
|
||||
const client = await subsonic.login(asToken(token));
|
||||
|
||||
mockPOST.mockImplementationOnce(() => Promise.resolve(ok({ token: 'theBearerToken' })))
|
||||
|
||||
const bearerToken = await pipe(client.bearerToken(credentials))();
|
||||
expect(bearerToken).toStrictEqual(E.right('theBearerToken'));
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(url.append({ pathname: '/auth/login' }).href(), credentials)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getting genres", () => {
|
||||
describe("when there are none", () => {
|
||||
beforeEach(() => {
|
||||
@@ -941,7 +970,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getGenres' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -968,7 +997,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getGenres' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -1003,7 +1032,7 @@ describe("Subsonic", () => {
|
||||
{ id: b64Encode("g4"), name: "g4" },
|
||||
]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getGenres' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -1059,7 +1088,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: artist.similarArtists,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1067,7 +1096,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1118,7 +1147,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: artist.similarArtists,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1126,7 +1155,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1171,7 +1200,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: artist.similarArtists,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1179,7 +1208,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1225,7 +1254,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1233,7 +1262,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1276,7 +1305,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1284,7 +1313,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1327,7 +1356,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1335,7 +1364,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1379,7 +1408,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1387,7 +1416,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1432,7 +1461,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1440,7 +1469,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1483,7 +1512,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1491,7 +1520,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1532,7 +1561,7 @@ describe("Subsonic", () => {
|
||||
similarArtists: [],
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1540,7 +1569,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtistInfo2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: artist.id,
|
||||
@@ -1661,7 +1690,7 @@ describe("Subsonic", () => {
|
||||
total: 1,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -1702,7 +1731,7 @@ describe("Subsonic", () => {
|
||||
total: 4,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -1730,7 +1759,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(artists).toEqual({ results: expectedResults, total: 4 });
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -1786,12 +1815,12 @@ describe("Subsonic", () => {
|
||||
total: 2,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "byGenre",
|
||||
@@ -1838,12 +1867,12 @@ describe("Subsonic", () => {
|
||||
total: 3,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "newest",
|
||||
@@ -1889,12 +1918,12 @@ describe("Subsonic", () => {
|
||||
total: 2,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "recent",
|
||||
@@ -1931,12 +1960,12 @@ describe("Subsonic", () => {
|
||||
total: 1,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "frequent",
|
||||
@@ -1973,12 +2002,12 @@ describe("Subsonic", () => {
|
||||
total: 1,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "highest",
|
||||
@@ -2024,12 +2053,12 @@ describe("Subsonic", () => {
|
||||
total: 1,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -2074,12 +2103,12 @@ describe("Subsonic", () => {
|
||||
total: 0,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -2139,12 +2168,12 @@ describe("Subsonic", () => {
|
||||
total: 6,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -2190,12 +2219,12 @@ describe("Subsonic", () => {
|
||||
total: 6,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbumList2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -2263,13 +2292,13 @@ describe("Subsonic", () => {
|
||||
total: 4,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2320,13 +2349,13 @@ describe("Subsonic", () => {
|
||||
total: 4,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2376,13 +2405,13 @@ describe("Subsonic", () => {
|
||||
total: 4,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2442,13 +2471,13 @@ describe("Subsonic", () => {
|
||||
total: 5,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2506,13 +2535,13 @@ describe("Subsonic", () => {
|
||||
total: 5,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2568,13 +2597,13 @@ describe("Subsonic", () => {
|
||||
total: 5,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getArtists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
url.append({ pathname: '/rest/getAlbumList2' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
@@ -2620,7 +2649,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(album);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -2698,7 +2727,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1, track2, track3, track4]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -2745,7 +2774,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -2780,7 +2809,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -2831,7 +2860,7 @@ describe("Subsonic", () => {
|
||||
rating: { love: true, stars: 4 },
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getSong`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getSong' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: track.id,
|
||||
@@ -2839,7 +2868,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -2878,7 +2907,7 @@ describe("Subsonic", () => {
|
||||
rating: { love: false, stars: 0 },
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getSong`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getSong' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: track.id,
|
||||
@@ -2886,7 +2915,7 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getAlbum' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: album.id,
|
||||
@@ -3033,7 +3062,7 @@ describe("Subsonic", () => {
|
||||
});
|
||||
expect(result.stream).toEqual(stream);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/stream' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: trackId,
|
||||
@@ -3139,7 +3168,7 @@ describe("Subsonic", () => {
|
||||
});
|
||||
expect(result.stream).toEqual(stream);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/stream' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: trackId,
|
||||
@@ -3182,7 +3211,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(streamClientApplication).toHaveBeenCalledWith(track);
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/stream' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: trackId,
|
||||
@@ -3224,7 +3253,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.stream({ trackId, range }));
|
||||
|
||||
expect(streamClientApplication).toHaveBeenCalledWith(track);
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/stream' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: trackId,
|
||||
@@ -3267,7 +3296,7 @@ describe("Subsonic", () => {
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getCoverArt`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getCoverArt' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: coverArtId,
|
||||
@@ -3303,7 +3332,7 @@ describe("Subsonic", () => {
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getCoverArt`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(url.append({ pathname: '/rest/getCoverArt' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: coverArtId,
|
||||
@@ -3429,7 +3458,7 @@ describe("Subsonic", () => {
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getCoverArt`,
|
||||
url.append({ pathname: '/rest/getCoverArt' }).href(),
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
@@ -3495,7 +3524,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/star`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/star' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: trackId,
|
||||
@@ -3528,7 +3557,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/unstar`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/unstar' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: trackId,
|
||||
@@ -3587,7 +3616,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/setRating`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/setRating' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: trackId,
|
||||
@@ -3648,14 +3677,14 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/unstar`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/unstar' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: trackId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/setRating`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/setRating' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: trackId,
|
||||
@@ -3715,7 +3744,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/scrobble' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -3744,7 +3773,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(false);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/scrobble' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -3770,7 +3799,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/scrobble' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -3799,7 +3828,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(false);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/scrobble' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -3827,7 +3856,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([artistToArtistSummary(artist1)]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 20,
|
||||
@@ -3861,7 +3890,7 @@ describe("Subsonic", () => {
|
||||
artistToArtistSummary(artist2),
|
||||
]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 20,
|
||||
@@ -3887,7 +3916,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 20,
|
||||
@@ -3923,7 +3952,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([albumToAlbumSummary(album)]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -3973,7 +4002,7 @@ describe("Subsonic", () => {
|
||||
albumToAlbumSummary(album2),
|
||||
]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -3999,7 +4028,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -4045,7 +4074,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -4117,7 +4146,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1, track2]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -4143,7 +4172,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/search3`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/search3' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
artistCount: 0,
|
||||
@@ -4174,7 +4203,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([playlist]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getPlaylists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -4199,7 +4228,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(playlists);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getPlaylists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -4219,7 +4248,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylists`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getPlaylists' }).href(), {
|
||||
params: asURLSearchParams(authParamsPlusJson),
|
||||
headers,
|
||||
});
|
||||
@@ -4304,7 +4333,7 @@ describe("Subsonic", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getPlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -4331,7 +4360,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(playlist);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getPlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getPlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id: playlist.id,
|
||||
@@ -4359,7 +4388,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual({ id, name });
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/createPlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/createPlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
f: "json",
|
||||
@@ -4383,7 +4412,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/deletePlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/deletePlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
id,
|
||||
@@ -4408,7 +4437,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/updatePlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/updatePlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
playlistId,
|
||||
@@ -4433,7 +4462,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual(true);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/updatePlaylist`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/updatePlaylist' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParamsPlusJson,
|
||||
playlistId,
|
||||
@@ -4480,7 +4509,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getSimilarSongs2`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getSimilarSongs2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
@@ -4550,7 +4579,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1, track2, track3]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getSimilarSongs2`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getSimilarSongs2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
@@ -4577,7 +4606,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getSimilarSongs2`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getSimilarSongs2' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
@@ -4644,7 +4673,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getTopSongs`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getTopSongs' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
@@ -4711,7 +4740,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([track1, track2, track3]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getTopSongs`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getTopSongs' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
@@ -4751,7 +4780,7 @@ describe("Subsonic", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(`${url}/rest/getTopSongs`, {
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getTopSongs' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json",
|
||||
|
||||
@@ -50,11 +50,11 @@
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": [
|
||||
"./typings",
|
||||
"node_modules/@types"
|
||||
"./node_modules/@types"
|
||||
]
|
||||
/* List of folders to include type definitions from. */,
|
||||
// "types": ["src/customTypes/scale-that-svg.d.ts"], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
Reference in New Issue
Block a user