Compare commits

..

1 Commits

Author SHA1 Message Date
simojenki
0b381b4ab1 ar 2021-08-15 07:52:50 +10:00
35 changed files with 389 additions and 1523 deletions

View File

@@ -1,57 +0,0 @@
name: ci
on:
push:
branches:
- 'master'
tags:
- 'v*'
pull_request:
branches:
- 'master'
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
-
uses: actions/checkout@v2
-
uses: actions/setup-node@v1
with:
node-version: 16.x
-
run: yarn install
-
run: yarn test
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v2
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: simojenki/bonob
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Push to Docker Hub
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

34
.github/workflows/master.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Build
on:
push:
branches: [ master ]
# pull_request:
# branches: [ master ]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn install
- run: yarn test
push_to_registry:
needs: build_and_test
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: simojenki/bonob
tag_with_ref: true

15
.github/workflows/pr.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Test PR
on: pull_request
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn install
- run: yarn test

View File

@@ -3,7 +3,6 @@ FROM node:16.6-alpine as build
WORKDIR /bonob WORKDIR /bonob
COPY src ./src COPY src ./src
COPY typings ./typings
COPY web ./web COPY web ./web
COPY tests ./tests COPY tests ./tests
COPY jest.config.js . COPY jest.config.js .

View File

@@ -23,33 +23,29 @@ Currently only a single integration allowing Navidrome to be registered with son
- Ability to play a playlist - Ability to play a playlist
- Ability to add/remove playlists - Ability to add/remove playlists
- Ability to add/remove tracks from a playlist - Ability to add/remove tracks from a playlist
- 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/)
## Running ## Running
bonob is ditributed via docker and can be run in a number of ways bonob is ditributed via docker and can be run in a number of ways
### Full sonos device auto-discovery and auto-registration using docker --network host ### Full sonos device auto-discovery by using docker --network host
```bash ```bash
docker run \ docker run \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
-p 4534:4534 \ -p 4534:4534 \
--network host \ --network host \
simojenki/bonob simojenki/bonob
``` ```
Now open http://localhost:4534 in your browser, you should see sonos devices, and service configuration. Bonob will auto-register itself with your sonos system on startup. Now open http://localhost:4534 in your browser, you should see sonos devices, and service configuration. By pressing 'Re-register' bonob will register itself in your sonos system, and should then show up in the "Services" list.
### Full sonos device auto-discovery and auto-registration on custom port by using a sonos seed device, without requiring docker host networking ### Full sonos device auto-discovery and auto-registration on custom port by using a sonos seed device, without requiring docker host networking
```bash ```bash
docker run \ docker run \
-e BONOB_PORT=3000 \ -e BONOB_PORT=3000 \
-e BONOB_SONOS_SEED_HOST=192.168.1.123 \
-e BONOB_SONOS_AUTO_REGISTER=true \ -e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \ -e BONOB_SONOS_SEED_HOST=192.168.1.123 \
-p 3000:3000 \ -p 3000:3000 \
simojenki/bonob simojenki/bonob
``` ```
@@ -117,9 +113,9 @@ services:
# ip address of your machine running bonob # ip address of your machine running bonob
BONOB_URL: http://192.168.1.111:4534 BONOB_URL: http://192.168.1.111:4534
BONOB_SECRET: changeme BONOB_SECRET: changeme
BONOB_SONOS_AUTO_REGISTER: true
BONOB_SONOS_DEVICE_DISCOVERY: true
BONOB_SONOS_SERVICE_ID: 246 BONOB_SONOS_SERVICE_ID: 246
BONOB_SONOS_AUTO_REGISTER: "true"
BONOB_SONOS_DEVICE_DISCOVERY: "true"
# ip address of one of your sonos devices # ip address of one of your sonos devices
BONOB_SONOS_SEED_HOST: 192.168.1.121 BONOB_SONOS_SEED_HOST: 192.168.1.121
BONOB_NAVIDROME_URL: http://navidrome:4533 BONOB_NAVIDROME_URL: http://navidrome:4533
@@ -159,10 +155,6 @@ BONOB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
- Implement the MusicService/MusicLibrary interface - Implement the MusicService/MusicLibrary interface
- Startup bonob with your new implementation. - Startup bonob with your new implementation.
## Credits
- Icons courtesy of: ![Navidrome](https://www.navidrome.org/), ![Vectornator](https://www.vectornator.io/), and @jicho
## TODO ## TODO
- Artist Radio - Artist Radio

View File

@@ -20,7 +20,6 @@
"fp-ts": "^2.9.5", "fp-ts": "^2.9.5",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"node-html-parser": "^2.1.0", "node-html-parser": "^2.1.0",
"scale-that-svg": "^1.0.5",
"sharp": "^0.27.2", "sharp": "^0.27.2",
"soap": "^0.37.0", "soap": "^0.37.0",
"ts-md5": "^1.2.7", "ts-md5": "^1.2.7",
@@ -37,7 +36,6 @@
"@types/supertest": "^2.0.10", "@types/supertest": "^2.0.10",
"chai": "^4.2.0", "chai": "^4.2.0",
"get-port": "^5.1.1", "get-port": "^5.1.1",
"image-js": "^0.32.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"supertest": "^6.1.3", "supertest": "^6.1.3",
@@ -50,8 +48,7 @@
"scripts": { "scripts": {
"clean": "rm -Rf build", "clean": "rm -Rf build",
"build": "tsc", "build": "tsc",
"dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true nodemon ./src/app.ts", "dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=true nodemon ./src/app.ts",
"devr": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=true nodemon ./src/app.ts",
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534", "register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
"test": "jest --testPathIgnorePatterns=build" "test": "jest --testPathIgnorePatterns=build"
} }

View File

@@ -88,7 +88,7 @@ const translations: Record<LANG, Record<KEY, string>> = {
expectedConfig: "Verwachte configuratie", expectedConfig: "Verwachte configuratie",
existingServiceConfig: "Bestaande serviceconfiguratie", existingServiceConfig: "Bestaande serviceconfiguratie",
noExistingServiceRegistration: "Geen bestaande serviceregistratie", noExistingServiceRegistration: "Geen bestaande serviceregistratie",
register: "Registreren", register: "Register",
removeRegistration: "Verwijder registratie", removeRegistration: "Verwijder registratie",
devices: "Apparaten", devices: "Apparaten",
services: "Services", services: "Services",
@@ -106,13 +106,6 @@ const translations: Record<LANG, Record<KEY, string>> = {
}, },
}; };
const translationsLookup = Object.keys(translations).reduce((lookups, lang) => {
lookups.set(lang, translations[lang as LANG]);
lookups.set(lang.toLocaleLowerCase(), translations[lang as LANG]);
lookups.set(lang.toLocaleLowerCase().split("-")[0]!, translations[lang as LANG]);
return lookups;
}, new Map<string, Record<KEY, string>>())
export const randomLang = () => _.shuffle(["en-US", "nl-NL"])[0]!; export const randomLang = () => _.shuffle(["en-US", "nl-NL"])[0]!;
export const asLANGs = (acceptLanguageHeader: string | undefined) => export const asLANGs = (acceptLanguageHeader: string | undefined) =>
@@ -142,7 +135,7 @@ export const keys = (lang: LANG = "en-US") => Object.keys(translations[lang]);
export default (serviceName: string): I8N => export default (serviceName: string): I8N =>
(...langs: string[]): Lang => { (...langs: string[]): Lang => {
const langToUse = const langToUse =
langs.map((l) => translationsLookup.get(l as LANG)).find((it) => it) || langs.map((l) => translations[l as LANG]).find((it) => it) ||
translations["en-US"]; translations["en-US"];
return (key: KEY) => { return (key: KEY) => {
const value = langToUse[key]?.replace( const value = langToUse[key]?.replace(

View File

@@ -99,7 +99,7 @@ export const asResult = <T>([results, total]: [T[], number]) => ({
export type ArtistQuery = Paging; export type ArtistQuery = Paging;
export type AlbumQueryType = 'alphabeticalByArtist' | 'alphabeticalByName' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred'; export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred';
export type AlbumQuery = Paging & { export type AlbumQuery = Paging & {
type: AlbumQueryType; type: AlbumQueryType;

View File

@@ -367,7 +367,7 @@ export class Navidrome implements MusicService {
) )
.then((json) => json["subsonic-response"]) .then((json) => json["subsonic-response"])
.then((json) => { .then((json) => {
if (isError(json)) throw `Navidrome error:${json.error._message}`; if (isError(json)) throw json.error._message;
else return json as unknown as T; else return json as unknown as T;
}); });

View File

@@ -2,10 +2,6 @@ import { option as O } from "fp-ts";
import express, { Express, Request } from "express"; import express, { Express, Request } from "express";
import * as Eta from "eta"; import * as Eta from "eta";
import morgan from "morgan"; import morgan from "morgan";
import path from "path";
import scale from "scale-that-svg";
import sharp from "sharp";
import fs from "fs";
import { PassThrough, Transform, TransformCallback } from "stream"; import { PassThrough, Transform, TransformCallback } from "stream";
@@ -17,8 +13,7 @@ import {
SONOS_RECOMMENDED_IMAGE_SIZES, SONOS_RECOMMENDED_IMAGE_SIZES,
LOGIN_ROUTE, LOGIN_ROUTE,
CREATE_REGISTRATION_ROUTE, CREATE_REGISTRATION_ROUTE,
REMOVE_REGISTRATION_ROUTE, REMOVE_REGISTRATION_ROUTE
ICON,
} from "./smapi"; } from "./smapi";
import { LinkCodes, InMemoryLinkCodes } from "./link_codes"; import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
import { MusicService, isSuccess } from "./music_service"; import { MusicService, isSuccess } from "./music_service";
@@ -32,26 +27,6 @@ import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n";
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token"; export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
const icon = (name: string) =>
fs
.readFileSync(path.resolve(".", "web", "icons", name))
.toString();
export type Icon = { svg: string; size: number };
export const ICONS: Record<ICON, Icon> = {
artists: { svg: icon("navidrome-artists.svg"), size: 24 },
albums: { svg: icon("navidrome-all.svg"), size: 24 },
playlists: { svg: icon("navidrome-playlists.svg"), size: 24 },
genres: { svg: icon("Theatre-Mask-111172.svg"), size: 128 },
random: { svg: icon("navidrome-random.svg"), size: 24 },
starred: { svg: icon("navidrome-topRated.svg"), size: 24 },
recentlyAdded: { svg: icon("navidrome-recentlyAdded.svg"), size: 24 },
recentlyPlayed: { svg: icon("navidrome-recentlyPlayed.svg"), size: 24 },
mostPlayed: { svg: icon("navidrome-mostPlayed.svg"), size: 24 },
discover: { svg: icon("Binoculars-14310.svg"), size: 32 },
};
interface RangeFilter extends Transform { interface RangeFilter extends Transform {
range: (length: number) => string; range: (length: number) => string;
} }
@@ -112,12 +87,7 @@ function server(
app.set("view engine", "eta"); app.set("view engine", "eta");
app.set("views", "./web/views"); app.set("views", "./web/views");
const langFor = (req: Request) => { const langFor = (req: Request) => i8n(...asLANGs(req.headers["accept-language"]))
logger.debug(
`${req.path} (req[accept-language]=${req.headers["accept-language"]})`
);
return i8n(...asLANGs(req.headers["accept-language"]));
};
app.get("/", (req, res) => { app.get("/", (req, res) => {
const lang = langFor(req); const lang = langFor(req);
@@ -132,12 +102,8 @@ function server(
services, services,
bonobService: service, bonobService: service,
registeredBonobService, registeredBonobService,
createRegistrationRoute: bonobUrl createRegistrationRoute: bonobUrl.append({ pathname: CREATE_REGISTRATION_ROUTE }).pathname(),
.append({ pathname: CREATE_REGISTRATION_ROUTE }) removeRegistrationRoute: bonobUrl.append({ pathname: REMOVE_REGISTRATION_ROUTE }).pathname(),
.pathname(),
removeRegistrationRoute: bonobUrl
.append({ pathname: REMOVE_REGISTRATION_ROUTE })
.pathname(),
}); });
} }
); );
@@ -147,8 +113,8 @@ function server(
return res.send({ return res.send({
service: { service: {
name: service.name, name: service.name,
sid: service.sid, sid: service.sid
}, }
}); });
}); });
@@ -218,19 +184,15 @@ function server(
res.status(403).render("failure", { res.status(403).render("failure", {
lang, lang,
message: lang("loginFailed"), message: lang("loginFailed"),
cause: authResult.message, cause: authResult.message
}); });
} }
} }
}); });
app.get(STRINGS_ROUTE, (_, res) => { app.get(STRINGS_ROUTE, (_, res) => {
const stringNode = (id: string, value: string) => const stringNode = (id: string, value: string) => `<string stringId="${id}"><![CDATA[${value}]]></string>`
`<string stringId="${id}"><![CDATA[${value}]]></string>`; const stringtableNode = (langName: string) => `<stringtable rev="1" xml:lang="${langName}">${i8nKeys().map(key => stringNode(key, i8n(langName as LANG)(key as KEY))).join("")}</stringtable>`
const stringtableNode = (langName: string) =>
`<stringtable rev="1" xml:lang="${langName}">${i8nKeys()
.map((key) => stringNode(key, i8n(langName as LANG)(key as KEY)))
.join("")}</stringtable>`;
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?> res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
<stringtables xmlns="http://sonos.com/sonosapi"> <stringtables xmlns="http://sonos.com/sonosapi">
@@ -246,23 +208,12 @@ function server(
<Match> <Match>
<imageSizeMap> <imageSizeMap>
${SONOS_RECOMMENDED_IMAGE_SIZES.map( ${SONOS_RECOMMENDED_IMAGE_SIZES.map(
(size) => (size) =>
`<sizeEntry size="${size}" substitution="/size/${size}"/>` `<sizeEntry size="${size}" substitution="/art/size/${size}"/>`
).join("")} ).join("")}
</imageSizeMap> </imageSizeMap>
</Match> </Match>
</PresentationMap> </PresentationMap>
<PresentationMap type="BrowseIconSizeMap">
<Match>
<browseIconSizeMap>
<sizeEntry size="0" substitution="/size/legacy"/>
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
(size) =>
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
).join("")}
</browseIconSizeMap>
</Match>
</PresentationMap>
<PresentationMap type="Search"> <PresentationMap type="Search">
<Match> <Match>
<SearchCategories> <SearchCategories>
@@ -301,8 +252,7 @@ function server(
) )
.then(({ musicLibrary, stream }) => { .then(({ musicLibrary, stream }) => {
logger.info( logger.info(
`stream response from music service for ${id}, status=${ `stream response from music service for ${id}, status=${stream.status
stream.status
}, headers=(${JSON.stringify(stream.headers)})` }, headers=(${JSON.stringify(stream.headers)})`
); );
@@ -375,72 +325,42 @@ function server(
} }
}); });
app.get("/icon/:type/size/:size", (req, res) => { app.get("/stream/artistRadio/:id", async (req, res) => {
const type = req.params["type"]!; const id = req.params["id"]!;
const size = req.params["size"]!; console.log(`----------> Streaming artist radio!! ${id}`)
res.status(404).send()
if (!Object.keys(ICONS).includes(type)) {
return res.status(404).send();
} else if (
size != "legacy" &&
!SONOS_RECOMMENDED_IMAGE_SIZES.includes(size)
) {
return res.status(400).send();
} else {
const icon = (ICONS as any)[type]! as Icon;
const spec =
size == "legacy"
? {
outputSize: 80,
mimeType: "image/png",
responseFormatter: (svg: string): Promise<Buffer | string> =>
sharp(Buffer.from(svg)).png().toBuffer(),
}
: {
outputSize: Number.parseInt(size),
mimeType: "image/svg+xml",
responseFormatter: (svg: string): Promise<Buffer | string> =>
Promise.resolve(svg),
};
return Promise.resolve(icon.svg)
.then((svg) => scale(svg, { scale: spec.outputSize / icon.size }))
.then(spec.responseFormatter)
.then((data) => res.status(200).type(spec.mimeType).send(data));
}
}); });
app.get("/art/:type/:id/size/:size", (req, res) => { app.get("/:type/:id/art/size/:size", (req, res) => {
const authToken = accessTokens.authTokenFor( const authToken = accessTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string req.query[BONOB_ACCESS_TOKEN_HEADER] as string
); );
const type = req.params["type"]!; const type = req.params["type"]!;
const id = req.params["id"]!; const id = req.params["id"]!;
const size = req.params["size"]!; const size = Number.parseInt(req.params["size"]!);
if (!authToken) { if (!authToken) {
return res.status(401).send(); return res.status(401).send();
} else if (type != "artist" && type != "album") { } else if (type != "artist" && type != "album") {
return res.status(400).send(); return res.status(400).send();
} else if (!(size.match(/^\d+$/) && Number.parseInt(size) > 0)) {
return res.status(400).send();
} else { } else {
return musicService return musicService
.login(authToken) .login(authToken)
.then((it) => it.coverArt(id, type, Number.parseInt(size))) .then((it) => it.coverArt(id, type, size))
.then((coverArt) => { .then((coverArt) => {
if (coverArt) { if (coverArt) {
res.status(200); res.status(200);
res.setHeader("content-type", coverArt.contentType); res.setHeader("content-type", coverArt.contentType);
return res.send(coverArt.data); res.send(coverArt.data);
} else { } else {
return res.status(404).send(); res.status(404).send();
} }
}) })
.catch((e: Error) => { .catch((e: Error) => {
logger.error( logger.error(
`Failed fetching image ${type}/${id}/size/${size}`, { cause: e } `Failed fetching image ${type}/${id}/size/${size}: ${e.message}`,
e
); );
return res.status(500).send(); res.status(500).send();
}); });
} }
}); });

View File

@@ -22,7 +22,7 @@ import { AccessTokens } from "./access_tokens";
import { BONOB_ACCESS_TOKEN_HEADER } from "./server"; import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
import { Clock } from "./clock"; import { Clock } from "./clock";
import { URLBuilder } from "./url_builder"; import { URLBuilder } from "./url_builder";
import { asLANGs, I8N } from "./i8n"; import { I8N, LANG } from "./i8n";
export const LOGIN_ROUTE = "/login"; export const LOGIN_ROUTE = "/login";
export const CREATE_REGISTRATION_ROUTE = "/registration/add"; export const CREATE_REGISTRATION_ROUTE = "/registration/add";
@@ -46,8 +46,6 @@ export const SONOS_RECOMMENDED_IMAGE_SIZES = [
"1500", "1500",
]; ];
export type ICON = "artists" | "albums" | "playlists" | "genres" | "random" | "starred" | "recentlyAdded" | "recentlyPlayed" | "mostPlayed" | "discover"
const WSDL_FILE = path.resolve( const WSDL_FILE = path.resolve(
__dirname, __dirname,
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl" "Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
@@ -228,15 +226,12 @@ const playlist = (playlist: PlaylistSummary) => ({
}); });
export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) => export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) =>
bonobUrl.append({ pathname: `/art/album/${album.id}/size/180` }); bonobUrl.append({ pathname: `/album/${album.id}/art/size/180` });
export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON) =>
bonobUrl.append({ pathname: `/icon/${icon}/size/legacy` });
export const defaultArtistArtURI = ( export const defaultArtistArtURI = (
bonobUrl: URLBuilder, bonobUrl: URLBuilder,
artist: ArtistSummary artist: ArtistSummary
) => bonobUrl.append({ pathname: `/art/artist/${artist.id}/size/180` }); ) => bonobUrl.append({ pathname: `/artist/${artist.id}/art/size/180` });
export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({ export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
itemType: "album", itemType: "album",
@@ -390,13 +385,30 @@ function bindSmapiSoapServiceToExpress(
) => ) =>
auth(musicService, accessTokens, soapyHeaders?.credentials) auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id)) .then(splitId(id))
.then(async ({ musicLibrary, accessToken, typeId }) => .then(async ({ musicLibrary, accessToken, type, typeId }) => {
musicLibrary.track(typeId!).then((it) => ({ console.log(`!!! getMediaMetadata->${id}`)
getMediaMetadataResult: track( switch (type) {
urlWithToken(accessToken), case "track": return musicLibrary.track(typeId!).then((it) => ({
it getMediaMetadataResult: track(
), urlWithToken(accessToken),
})) it,
),
}));
case "artistRadio": return {
getMediaMetadataResult: {
id,
itemType: "stream",
title: "Foobar100",
mimeType: 'audio/x-scpls',
// streamMetadata: {
// logo: "??"
// }
}
}
default:
throw `Unsupported search by:${id}`;
}
}
), ),
search: async ( search: async (
{ id, term }: { id: string; term: string }, { id, term }: { id: string; term: string },
@@ -444,7 +456,7 @@ function bindSmapiSoapServiceToExpress(
index, index,
count, count,
}: // recursive, }: // recursive,
{ id: string; index: number; count: number; recursive: boolean }, { id: string; index: number; count: number; recursive: boolean },
_, _,
soapyHeaders: SoapyHeaders, soapyHeaders: SoapyHeaders,
) => ) =>
@@ -469,12 +481,18 @@ function bindSmapiSoapServiceToExpress(
relatedBrowse: relatedBrowse:
artist.similarArtists.filter(it => it.inLibrary).length > 0 artist.similarArtists.filter(it => it.inLibrary).length > 0
? [ ? [
{ {
id: `relatedArtists:${artist.id}`, id: `relatedArtists:${artist.id}`,
type: "RELATED_ARTISTS", type: "RELATED_ARTISTS",
}, },
] ]
: [], : [],
relatedPlay: {
id: `artistRadio:${artist.id}`,
itemType: "stream",
title: "Foobar radio",
canPlay: true
}
}, },
}; };
}); });
@@ -532,7 +550,7 @@ function bindSmapiSoapServiceToExpress(
index, index,
count, count,
}: // recursive, }: // recursive,
{ id: string; index: number; count: number; recursive: boolean }, { id: string; index: number; count: number; recursive: boolean },
_, _,
soapyHeaders: SoapyHeaders, soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, 'headers'> { headers }: Pick<Request, 'headers'>
@@ -541,11 +559,10 @@ function bindSmapiSoapServiceToExpress(
.then(splitId(id)) .then(splitId(id))
.then(({ musicLibrary, accessToken, type, typeId }) => { .then(({ musicLibrary, accessToken, type, typeId }) => {
const paging = { _index: index, _count: count }; const paging = { _index: index, _count: count };
const acceptLanguage = headers["accept-language"]; const lang = i8n((headers["accept-language"] || "en-US") as LANG);
logger.debug( logger.debug(
`Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}` `Fetching metadata type=${type}, typeId=${typeId}`
); );
const lang = i8n(...asLANGs(acceptLanguage));
const albums = (q: AlbumQuery): Promise<GetMetadataResponse> => const albums = (q: AlbumQuery): Promise<GetMetadataResponse> =>
musicLibrary.albums(q).then((result) => { musicLibrary.albums(q).then((result) => {
@@ -563,22 +580,19 @@ function bindSmapiSoapServiceToExpress(
return getMetadataResult({ return getMetadataResult({
mediaCollection: [ mediaCollection: [
{ {
itemType: "container",
id: "artists", id: "artists",
title: lang("artists"), title: lang("artists"),
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
}, },
{ {
itemType: "albumList",
id: "albums", id: "albums",
title: lang("albums"), title: lang("albums"),
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
}, },
{ {
itemType: "playlist",
id: "playlists", id: "playlists",
title: lang("playlists"), title: lang("playlists"),
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: { attributes: {
readOnly: false, readOnly: false,
userContent: true, userContent: true,
@@ -586,40 +600,34 @@ function bindSmapiSoapServiceToExpress(
}, },
}, },
{ {
itemType: "container",
id: "genres", id: "genres",
title: lang("genres"), title: lang("genres"),
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
}, },
{ {
itemType: "albumList",
id: "randomAlbums", id: "randomAlbums",
title: lang("random"), title: lang("random"),
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "starredAlbums", id: "starredAlbums",
title: lang("starred"), title: lang("starred"),
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyAdded", id: "recentlyAdded",
title: lang("recentlyAdded"), title: lang("recentlyAdded"),
albumArtURI: iconArtURI(bonobUrl, "recentlyAdded").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyPlayed", id: "recentlyPlayed",
title: lang("recentlyPlayed"), title: lang("recentlyPlayed"),
albumArtURI: iconArtURI(bonobUrl, "recentlyPlayed").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "mostPlayed", id: "mostPlayed",
title: lang("mostPlayed"), title: lang("mostPlayed"),
albumArtURI: iconArtURI(bonobUrl, "mostPlayed").href(),
itemType: "albumList",
}, },
], ],
index: 0, index: 0,
@@ -647,7 +655,7 @@ function bindSmapiSoapServiceToExpress(
}); });
case "albums": { case "albums": {
return albums({ return albums({
type: "alphabeticalByName", type: "alphabeticalByArtist",
...paging, ...paging,
}); });
} }

View File

@@ -10,7 +10,7 @@ import { URLBuilder } from "./url_builder";
export const SONOS_LANG = ["en-US", "da-DK", "de-DE", "es-ES", "fr-FR", "it-IT", "ja-JP", "nb-NO", "nl-NL", "pt-BR", "sv-SE", "zh-CN"] export const SONOS_LANG = ["en-US", "da-DK", "de-DE", "es-ES", "fr-FR", "it-IT", "ja-JP", "nb-NO", "nl-NL", "pt-BR", "sv-SE", "zh-CN"]
export const PRESENTATION_AND_STRINGS_VERSION = "21"; export const PRESENTATION_AND_STRINGS_VERSION = "20";
// NOTE: manifest requires https for the URL, // NOTE: manifest requires https for the URL,
// otherwise you will get an error trying to register // otherwise you will get an error trying to register
@@ -20,6 +20,7 @@ export type Capability =
| "alFavorites" | "alFavorites"
| "ucPlaylists" | "ucPlaylists"
| "extendedMD" | "extendedMD"
| "radioExtendedMD"
| "contextHeaders" | "contextHeaders"
| "authorizationHeader" | "authorizationHeader"
| "logging" | "logging"
@@ -32,6 +33,7 @@ export const BONOB_CAPABILITIES: Capability[] = [
"ucPlaylists", "ucPlaylists",
"extendedMD", "extendedMD",
"logging", "logging",
"radioExtendedMD"
]; ];
export type Device = { export type Device = {
@@ -162,7 +164,7 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
} }
}) })
.catch((e) => { .catch((e) => {
logger.error(`Failed looking for sonos devices`, { cause: e }); logger.error(`Failed looking for sonos devices ${e}`);
return []; return [];
}); });
}; };

View File

@@ -54,129 +54,79 @@ describe("i8n", () => {
describe("fetching translations", () => { describe("fetching translations", () => {
describe("with a single lang", () => { describe("with a single lang", () => {
describe("and the lang is not represented", () => { describe("and there is no templating", () => {
describe("and there is no templating", () => { it("should return the value", () => {
it("should return the en-US value", () => { expect(i8n("foo")("en-US")("artists")).toEqual("Artists");
expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists"); expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten");
});
});
describe("and there is templating of the service name", () => {
it("should return the en-US value templated", () => {
expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
}); });
}); });
describe("and the lang is represented", () => { describe("and there is templating of the service name", () => {
describe("and there is no templating", () => { it("should return the value", () => {
it("should return the value", () => { expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual(
expect(i8n("foo")("en-US")("artists")).toEqual("Artists"); "Linking sonos with service123"
expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten"); );
}); expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual(
}); "Sonos koppelen aan service456"
);
describe("and there is templating of the service name", () => {
it("should return the value", () => {
expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
}); });
}); });
}); });
describe("with multiple langs", () => { describe("with multiple langs", () => {
function itShouldReturn(serviceName: string, langs: string[], key: KEY, expected: string) { describe("and the first lang is a match", () => {
it(`should return '${expected}' for the serviceName=${serviceName}, langs=${langs}`, () => {
expect(i8n(serviceName)(...langs)(key)).toEqual(expected);
});
};
describe("and the first lang is an exact match", () => {
describe("and there is no templating", () => { describe("and there is no templating", () => {
itShouldReturn("foo", ["en-US", "nl-NL"], "artists", "Artists"); it("should return the value for the first lang", () => {
itShouldReturn("foo", ["nl-NL", "en-US"], "artists", "Artiesten"); expect(i8n("foo")("en-US", "nl-NL")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL", "en-US")("artists")).toEqual("Artiesten");
});
}); });
describe("and there is templating of the service name", () => { describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123"); it("should return the value for the firt lang", () => {
itShouldReturn("service456", ["nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456"); expect(i8n("service123")("en-US", "nl-NL")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("nl-NL", "en-US")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
}); });
}); });
describe("and the first lang is a case insensitive match", () => { describe("and the first lang is not a match, however there is a match in the provided langs", () => {
describe("and there is no templating", () => { describe("and there is no templating", () => {
itShouldReturn("foo", ["en-us", "nl-NL"], "artists", "Artists"); it("should return the value for the first lang", () => {
itShouldReturn("foo", ["nl-nl", "en-US"], "artists", "Artiesten"); expect(i8n("foo")("something", "en-US", "nl-NL")("artists")).toEqual("Artists");
expect(i8n("foo")("something", "nl-NL", "en-US")("artists")).toEqual("Artiesten");
});
}); });
describe("and there is templating of the service name", () => { describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["en-us", "nl-NL"], "AppLinkMessage", "Linking sonos with service123"); it("should return the value for the firt lang", () => {
itShouldReturn("service456", ["nl-nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456"); expect(i8n("service123")("something", "en-US", "nl-NL")("AppLinkMessage")).toEqual(
}); "Linking sonos with service123"
}); );
expect(i8n("service456")("something", "nl-NL", "en-US")("AppLinkMessage")).toEqual(
describe("and the first lang is a lang match without region", () => { "Sonos koppelen aan service456"
describe("and there is no templating", () => { );
itShouldReturn("foo", ["en", "nl-NL"], "artists", "Artists"); });
itShouldReturn("foo", ["nl", "en-US"], "artists", "Artiesten");
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["en", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
});
});
describe("and the first lang is not a match, however there is an exact match in the provided langs", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en-US", "nl-NL"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl-NL", "en-US"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456")
});
});
describe("and the first lang is not a match, however there is a case insensitive match in the provided langs", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en-us", "nl-nl"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl-nl", "en-us"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en-us", "nl-nl"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl-nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456")
});
});
describe("and the first lang is not a match, however there is a lang match without region", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en", "nl-nl"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl", "en-us"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en", "nl-nl"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456")
}); });
}); });
describe("and no lang is a match", () => { describe("and no lang is a match", () => {
describe("and there is no templating", () => { describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "something2"], "artists", "Artists") it("should return the value for the first lang", () => {
expect(i8n("foo")("something", "something2")("artists")).toEqual("Artists");
});
}); });
describe("and there is templating of the service name", () => { describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "something2"], "AppLinkMessage", "Linking sonos with service123") it("should return the value for the firt lang", () => {
expect(i8n("service123")("something", "something2")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
}); });
}); });
}); });
@@ -189,5 +139,20 @@ describe("i8n", () => {
}); });
}); });
describe("when the lang is not represented", () => {
describe("and there is no templating", () => {
it("should return the en-US value", () => {
expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists");
});
});
describe("and there is templating of the service name", () => {
it("should return the en-US value templated", () => {
expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
});
});
}); });
}); });

View File

@@ -16,7 +16,6 @@ import {
HIP_HOP, HIP_HOP,
SKA, SKA,
} from "./builders"; } from "./builders";
import _ from "underscore";
describe("InMemoryMusicService", () => { describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService(); const service = new InMemoryMusicService();
@@ -211,7 +210,6 @@ describe("InMemoryMusicService", () => {
const artist3_album2 = anAlbum({ genre: POP }); const artist3_album2 = anAlbum({ genre: POP });
const artist1 = anArtist({ const artist1 = anArtist({
name: "artist1",
albums: [ albums: [
artist1_album1, artist1_album1,
artist1_album2, artist1_album2,
@@ -220,8 +218,8 @@ describe("InMemoryMusicService", () => {
artist1_album5, artist1_album5,
], ],
}); });
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] }); const artist2 = anArtist({ albums: [artist2_album1] });
const artist3 = anArtist({ name: "artist3", albums: [artist3_album1, artist3_album2] }); const artist3 = anArtist({ albums: [artist3_album1, artist3_album2] });
const artistWithNoAlbums = anArtist({ albums: [] }); const artistWithNoAlbums = anArtist({ albums: [] });
const allAlbums = [artist1, artist2, artist3, artistWithNoAlbums].flatMap( const allAlbums = [artist1, artist2, artist3, artistWithNoAlbums].flatMap(
@@ -267,48 +265,29 @@ describe("InMemoryMusicService", () => {
describe("fetching multiple albums", () => { describe("fetching multiple albums", () => {
describe("with no filtering", () => { describe("with no filtering", () => {
describe("fetching all on one page", () => { describe("fetching all on one page", () => {
describe("alphabeticalByArtist", () => { it("should return all the albums for all the artists", async () => {
it("should return all the albums for all the artists", async () => { expect(
expect( await musicLibrary.albums({
await musicLibrary.albums({ _index: 0,
_index: 0, _count: 100,
_count: 100, type: "alphabeticalByArtist",
type: "alphabeticalByArtist", })
}) ).toEqual({
).toEqual({ results: [
results: [ albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album1), albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album2), albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album3), albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album4), albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1), albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1), albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2), albumToAlbumSummary(artist3_album2),
], ],
total: totalAlbumCount, total: totalAlbumCount,
});
}); });
}); });
describe("alphabeticalByName", () => {
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByName",
})
).toEqual({
results:
_.sortBy(allAlbums, 'name').map(albumToAlbumSummary),
total: totalAlbumCount,
});
});
});
}); });
describe("fetching a page", () => { describe("fetching a page", () => {

View File

@@ -77,9 +77,7 @@ export class InMemoryMusicService implements MusicService {
switch (q.type) { switch (q.type) {
case "alphabeticalByArtist": case "alphabeticalByArtist":
return artist2Album; return artist2Album;
case "alphabeticalByName": case "byGenre":
return artist2Album.sort((a, b) => a.album.name.localeCompare(b.album.name));
case "byGenre":
return artist2Album.filter( return artist2Album.filter(
(it) => it.album.genre?.id === q.genre (it) => it.album.genre?.id === q.genre
); );

View File

@@ -447,7 +447,7 @@ describe("Navidrome", () => {
}); });
const token = await navidrome.generateToken({ username, password }); const token = await navidrome.generateToken({ username, password });
expect(token).toEqual({ message: "Navidrome error:Wrong username or password" }); expect(token).toEqual({ message: "Wrong username or password" });
}); });
}); });
}); });
@@ -3387,7 +3387,7 @@ describe("Navidrome", () => {
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken)) .then((it) => navidrome.login(it.authToken))
.then((it) => it.playlist(id)) .then((it) => it.playlist(id))
).rejects.toEqual("Navidrome error:data not found"); ).rejects.toEqual("data not found");
}); });
}); });
@@ -3717,6 +3717,7 @@ describe("Navidrome", () => {
const id = "idWithNoTracks"; const id = "idWithNoTracks";
const xml = similarSongsXml([]); const xml = similarSongsXml([]);
console.log(`xml = ${xml}`)
mockGET mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() => .mockImplementationOnce(() =>
@@ -3742,7 +3743,7 @@ describe("Navidrome", () => {
}); });
}); });
describe("when the id doesnt exist", () => { describe("when there id doesnt exist", () => {
it("should fail", async () => { it("should fail", async () => {
const id = "idThatHasAnError"; const id = "idThatHasAnError";
@@ -3756,7 +3757,7 @@ describe("Navidrome", () => {
.generateToken({ username, password }) .generateToken({ username, password })
.then((it) => it as AuthSuccess) .then((it) => it as AuthSuccess)
.then((it) => navidrome.login(it.authToken)) .then((it) => navidrome.login(it.authToken))
.then((it) => it.similarSongs(id))).rejects.toEqual("Navidrome error:data not found"); .then((it) => it.similarSongs(id))).rejects.toEqual("data not found");
}); });
}); });
}); });

View File

@@ -138,6 +138,8 @@ class SonosDriver {
return m![1]!; return m![1]!;
}); });
console.log(`posting to action ${action}`);
return request(this.server) return request(this.server)
.post(action) .post(action)
.type("form") .type("form")
@@ -243,7 +245,7 @@ describe("scenarios", () => {
...BLONDIE.albums, ...BLONDIE.albums,
...BOB_MARLEY.albums, ...BOB_MARLEY.albums,
...MADONNA.albums, ...MADONNA.albums,
].map((it) => it.name).sort() ].map((it) => it.name)
) )
); );
}); });

View File

@@ -1,16 +1,12 @@
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import dayjs from "dayjs"; import dayjs from "dayjs";
import request from "supertest"; import request from "supertest";
import Image from "image-js";
import { MusicService } from "../src/music_service"; import { MusicService } from "../src/music_service";
import makeServer, { import makeServer, {
BONOB_ACCESS_TOKEN_HEADER, BONOB_ACCESS_TOKEN_HEADER,
ICONS,
RangeBytesFromFilter, RangeBytesFromFilter,
rangeFilterFor, rangeFilterFor,
} from "../src/server"; } from "../src/server";
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos"; import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
import { aDevice, aService } from "./builders"; import { aDevice, aService } from "./builders";
@@ -21,9 +17,6 @@ import { Response } from "express";
import { Transform } from "stream"; import { Transform } from "stream";
import url from "../src/url_builder"; import url from "../src/url_builder";
import i8n, { randomLang } from "../src/i8n"; import i8n, { randomLang } from "../src/i8n";
import {
SONOS_RECOMMENDED_IMAGE_SIZES,
} from "../src/smapi";
describe("rangeFilterFor", () => { describe("rangeFilterFor", () => {
describe("invalid range header string", () => { describe("invalid range header string", () => {
@@ -285,16 +278,10 @@ describe("server", () => {
.set("accept-language", acceptLanguage) .set("accept-language", acceptLanguage)
.send(); .send();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.text).toMatch( expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
`<input type="submit" value="${lang("register")}">`
);
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`); expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
expect(res.text).toMatch( expect(res.text).toMatch(`<h3>${lang("noExistingServiceRegistration")}</h3>`);
`<h3>${lang("noExistingServiceRegistration")}</h3>` expect(res.text).not.toMatch(`<input type="submit" value="${lang("removeRegistration")}">`);
);
expect(res.text).not.toMatch(
`<input type="submit" value="${lang("removeRegistration")}">`
);
}); });
}); });
}); });
@@ -330,16 +317,10 @@ describe("server", () => {
.set("accept-language", acceptLanguage) .set("accept-language", acceptLanguage)
.send(); .send();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.text).toMatch( expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
`<input type="submit" value="${lang("register")}">`
);
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`); expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
expect(res.text).toMatch( expect(res.text).toMatch(`<h3>${lang("existingServiceConfig")}</h3>`);
`<h3>${lang("existingServiceConfig")}</h3>` expect(res.text).toMatch(`<input type="submit" value="${lang("removeRegistration")}">`);
);
expect(res.text).toMatch(
`<input type="submit" value="${lang("removeRegistration")}">`
);
}); });
}); });
}); });
@@ -362,16 +343,15 @@ describe("server", () => {
it("should report some information about the service", async () => { it("should report some information about the service", async () => {
const res = await request(server) const res = await request(server)
.get(bonobUrl.append({ pathname: "/about" }).path()) .get(bonobUrl.append({ pathname: "/about" }).path())
.send(); .send();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.body).toEqual({ expect(res.body).toEqual({
service: { service: {
name: theService.name, name: theService.name,
sid: theService.sid, sid: theService.sid
}, }});
});
}); });
}); });
@@ -435,9 +415,7 @@ describe("server", () => {
sonos.remove.mockResolvedValue(true); sonos.remove.mockResolvedValue(true);
const res = await request(server) const res = await request(server)
.post( .post(bonobUrl.append({ pathname: "/registration/remove" }).path())
bonobUrl.append({ pathname: "/registration/remove" }).path()
)
.set("accept-language", acceptLanguage) .set("accept-language", acceptLanguage)
.send(); .send();
@@ -455,9 +433,7 @@ describe("server", () => {
sonos.remove.mockResolvedValue(false); sonos.remove.mockResolvedValue(false);
const res = await request(server) const res = await request(server)
.post( .post(bonobUrl.append({ pathname: "/registration/remove" }).path())
bonobUrl.append({ pathname: "/registration/remove" }).path()
)
.set("accept-language", acceptLanguage) .set("accept-language", acceptLanguage)
.send(); .send();
@@ -478,7 +454,7 @@ describe("server", () => {
remove: jest.fn(), remove: jest.fn(),
}; };
const theService = aService({ const theService = aService({
name: serviceNameForLang, name: serviceNameForLang
}); });
const musicService = { const musicService = {
@@ -499,6 +475,7 @@ describe("server", () => {
now: jest.fn(), now: jest.fn(),
}; };
const server = makeServer( const server = makeServer(
sonos as unknown as Sonos, sonos as unknown as Sonos,
theService, theService,
@@ -519,18 +496,10 @@ describe("server", () => {
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.text).toMatch(`<title>${lang("login")}</title>`); expect(res.text).toMatch(`<title>${lang("login")}</title>`);
expect(res.text).toMatch( expect(res.text).toMatch(`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>`);
`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>` expect(res.text).toMatch(`<label for="username">${lang("username")}:</label>`);
); expect(res.text).toMatch(`<label for="password">${lang("password")}:</label>`);
expect(res.text).toMatch( expect(res.text).toMatch(`<input type="submit" value="${lang("login")}" id="submit">`);
`<label for="username">${lang("username")}:</label>`
);
expect(res.text).toMatch(
`<label for="password">${lang("password")}:</label>`
);
expect(res.text).toMatch(
`<input type="submit" value="${lang("login")}" id="submit">`
);
}); });
describe("when the credentials are valid", () => { describe("when the credentials are valid", () => {
@@ -1042,7 +1011,7 @@ describe("server", () => {
}); });
}); });
describe("/art", () => { describe("art", () => {
const musicService = { const musicService = {
login: jest.fn(), login: jest.fn(),
}; };
@@ -1071,7 +1040,7 @@ describe("server", () => {
describe("when there is no access-token", () => { describe("when there is no access-token", () => {
it("should return a 401", async () => { it("should return a 401", async () => {
const res = await request(server).get(`/art/album/123/size/180`); const res = await request(server).get(`/album/123/art/size/180`);
expect(res.status).toEqual(401); expect(res.status).toEqual(401);
}); });
@@ -1082,7 +1051,7 @@ describe("server", () => {
now = now.add(1, "day"); now = now.add(1, "day");
const res = await request(server).get( const res = await request(server).get(
`/art/album/123/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/album/123/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
); );
expect(res.status).toEqual(401); expect(res.status).toEqual(401);
@@ -1094,7 +1063,7 @@ describe("server", () => {
it("should return a 400", async () => { it("should return a 400", async () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/foo/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/foo/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1103,21 +1072,6 @@ describe("server", () => {
}); });
describe("artist art", () => { describe("artist art", () => {
["0", "-1", "foo"].forEach((size) => {
describe(`when the size is ${size}`, () => {
it(`should return a 400`, async () => {
musicService.login.mockResolvedValue(musicLibrary);
const res = await request(server)
.get(
`/art/artist/${albumId}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(400);
});
});
});
describe("when there is some", () => { describe("when there is some", () => {
it("should return the image and a 200", async () => { it("should return the image and a 200", async () => {
const coverArt = { const coverArt = {
@@ -1132,7 +1086,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1158,7 +1112,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1174,7 +1128,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1184,21 +1138,6 @@ describe("server", () => {
}); });
describe("album art", () => { describe("album art", () => {
["0", "-1", "foo"].forEach((size) => {
describe(`when the size is ${size}`, () => {
it(`should return a 400`, async () => {
musicService.login.mockResolvedValue(musicLibrary);
const res = await request(server)
.get(
`/art/album/${albumId}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
)
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
expect(res.status).toEqual(400);
});
});
});
describe("when there is some", () => { describe("when there is some", () => {
it("should return the image and a 200", async () => { it("should return the image and a 200", async () => {
const coverArt = { const coverArt = {
@@ -1212,7 +1151,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1237,7 +1176,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1252,7 +1191,7 @@ describe("server", () => {
const res = await request(server) const res = await request(server)
.get( .get(
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}` `/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
) )
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken); .set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
@@ -1262,94 +1201,6 @@ describe("server", () => {
}); });
}); });
}); });
describe("/icon", () => {
const server = makeServer(
jest.fn() as unknown as Sonos,
aService(),
url("http://localhost:1234"),
jest.fn() as unknown as MusicService,
new InMemoryLinkCodes(),
jest.fn() as unknown as AccessTokens
);
describe("invalid icon names", () => {
[
"..%2F..%2Ffoo",
"%2Fetc%2Fpasswd",
".%2Fbob.js",
".",
"..",
"1",
"%23%24",
"notAValidIcon",
].forEach((type) => {
describe(`trying to retrieve an icon with name ${type}`, () => {
it(`should fail`, async () => {
const response = await request(server).get(
`/icon/${type}/size/legacy`
);
expect(response.status).toEqual(404);
});
});
});
});
describe("invalid size", () => {
["-1", "0", "59", "foo"].forEach((size) => {
describe(`trying to retrieve an icon with size ${size}`, () => {
it(`should fail`, async () => {
const response = await request(server).get(
`/icon/artists/size/${size}`
);
expect(response.status).toEqual(400);
});
});
});
});
describe("fetching", () => {
Object.keys(ICONS).forEach((type) => {
describe(`type=${type}`, () => {
describe(`legacy icon`, () => {
it("should return the png image", async () => {
const response = await request(server).get(
`/icon/${type}/size/legacy`
);
expect(response.status).toEqual(200);
expect(response.header["content-type"]).toEqual("image/png");
const image = await Image.load(response.body);
expect(image.width).toEqual(80);
expect(image.height).toEqual(80);
});
});
describe("svg icon", () => {
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
it(`should return an svg image for size = ${size}`, async () => {
const response = await request(server).get(
`/icon/${type}/size/${size}`
);
expect(response.status).toEqual(200);
expect(response.header["content-type"]).toEqual(
"image/svg+xml; charset=utf-8"
);
const svg = Buffer.from(response.body).toString();
expect(svg).toContain(`viewBox="0 0 ${size} ${size}"`);
expect(svg).toContain(
` xmlns="http://www.w3.org/2000/svg" `
);
});
});
});
});
});
});
});
}); });
}); });
}); });

View File

@@ -21,7 +21,6 @@ import {
defaultAlbumArtURI, defaultAlbumArtURI,
defaultArtistArtURI, defaultArtistArtURI,
searchResult, searchResult,
iconArtURI,
} from "../src/smapi"; } from "../src/smapi";
import { import {
@@ -55,109 +54,83 @@ describe("service config", () => {
const bonobWithContextPath = url("http://localhost:5678/some-context-path"); const bonobWithContextPath = url("http://localhost:5678/some-context-path");
[bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => { [bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => {
describe(bonobUrl.href(), () => { const server = makeServer(
const server = makeServer( SONOS_DISABLED,
SONOS_DISABLED, aService({ name: "music land" }),
aService({ name: "music land" }), bonobUrl,
bonobUrl, new InMemoryMusicService()
new InMemoryMusicService() );
);
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE }); const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
const presentationUrl = bonobUrl.append({ const presentationUrl = bonobUrl.append({
pathname: PRESENTATION_MAP_ROUTE, pathname: PRESENTATION_MAP_ROUTE,
});
describe(`${stringsUrl}`, () => {
async function fetchStringsXml() {
const res = await request(server).get(stringsUrl.path()).send();
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
return parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
}
it("should return xml for the strings", async () => {
const xml = await fetchStringsXml();
const sonosString = (id: string, lang: string) =>
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
xml
);
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
"Linking sonos with music land"
);
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
"Sonos koppelen aan music land"
);
// no fr-FR translation, so use en-US
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
"Linking sonos with music land"
);
}); });
describe(STRINGS_ROUTE, () => { it("should return a section for all sonos supported languages", async () => {
async function fetchStringsXml() { const xml = await fetchStringsXml();
const res = await request(server).get(stringsUrl.path()).send(); SONOS_LANG.forEach(lang => {
expect(xpath.select(
expect(res.status).toEqual(200); `string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
xml
// removing the sonos xml ns as makes xpath queries with xpath-ts painful )).toBeDefined();
return parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
}
it("should return xml for the strings", async () => {
const xml = await fetchStringsXml();
const sonosString = (id: string, lang: string) =>
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
xml
);
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
"Linking sonos with music land"
);
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
"Sonos koppelen aan music land"
);
// no fr-FR translation, so use en-US
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
"Linking sonos with music land"
);
});
it("should return a section for all sonos supported languages", async () => {
const xml = await fetchStringsXml();
SONOS_LANG.forEach((lang) => {
expect(
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
xml
)
).toBeDefined();
});
}); });
}); });
});
describe(PRESENTATION_MAP_ROUTE, () => { describe(`${presentationUrl}`, () => {
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => { it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send(); const res = await request(server).get(presentationUrl.path()).send();
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful // removing the sonos xml ns as makes xpath queries with xpath-ts painful
const xml = parseXML( const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "") res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
); );
const imageSizeMap = (size: string) => SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
xpath.select( expect(imageSizeMap(size)).toEqual(`/art/size/${size}`);
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
});
}); });
it("should have an BrowseIconSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send();
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="BrowseIconSizeMap"]/Match/browseIconSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
});
});
}); });
}); });
}); });
@@ -273,7 +246,7 @@ describe("track", () => {
albumId: someTrack.album.id, albumId: someTrack.album.id,
albumArtist: someTrack.artist.name, albumArtist: someTrack.artist.name,
albumArtistId: someTrack.artist.id, albumArtistId: someTrack.artist.id,
albumArtURI: `http://localhost:4567/foo/art/album/${someTrack.album.id}/size/180?access-token=1234`, albumArtURI: `http://localhost:4567/foo/album/${someTrack.album.id}/art/size/180?access-token=1234`,
artist: someTrack.artist.name, artist: someTrack.artist.name,
artistId: someTrack.artist.id, artistId: someTrack.artist.id,
duration: someTrack.duration, duration: someTrack.duration,
@@ -308,7 +281,7 @@ describe("defaultAlbumArtURI", () => {
const album = anAlbum(); const album = anAlbum();
expect(defaultAlbumArtURI(bonobUrl, album).href()).toEqual( expect(defaultAlbumArtURI(bonobUrl, album).href()).toEqual(
`http://localhost:1234/context-path/art/album/${album.id}/size/180?search=yes` `http://localhost:1234/context-path/album/${album.id}/art/size/180?search=yes`
); );
}); });
}); });
@@ -319,7 +292,7 @@ describe("defaultArtistArtURI", () => {
const artist = anArtist(); const artist = anArtist();
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual( expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
`http://localhost:1234/something/art/artist/${artist.id}/size/180?s=123` `http://localhost:1234/something/artist/${artist.id}/art/size/180?s=123`
); );
}); });
}); });
@@ -477,8 +450,7 @@ describe("api", () => {
.catch((e: any) => { .catch((e: any) => {
expect(e.root.Envelope.Body.Fault).toEqual({ expect(e.root.Envelope.Body.Fault).toEqual({
faultcode: "Client.NOT_LINKED_RETRY", faultcode: "Client.NOT_LINKED_RETRY",
faultstring: faultstring: "Link Code not found yet, sonos app will keep polling until you log in to bonob",
"Link Code not found yet, sonos app will keep polling until you log in to bonob",
detail: { detail: {
ExceptionInfo: "NOT_LINKED_RETRY", ExceptionInfo: "NOT_LINKED_RETRY",
SonosError: "5", SonosError: "5",
@@ -735,72 +707,46 @@ describe("api", () => {
getMetadataResult({ getMetadataResult({
mediaCollection: [ mediaCollection: [
{ {
itemType: "container",
id: "artists", id: "artists",
title: "Artists", title: "Artists",
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
id: "albums",
title: "Albums",
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
}, },
{ itemType: "albumList", id: "albums", title: "Albums" },
{ {
itemType: "playlist",
id: "playlists", id: "playlists",
title: "Playlists", title: "Playlists",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: { attributes: {
readOnly: "false", readOnly: "false",
renameable: "false", renameable: "false",
userContent: "true", userContent: "true",
}, },
}, },
{ itemType: "container", id: "genres", title: "Genres" },
{ {
id: "genres", itemType: "albumList",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
id: "randomAlbums", id: "randomAlbums",
title: "Random", title: "Random",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "starredAlbums", id: "starredAlbums",
title: "Starred", title: "Starred",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyAdded", id: "recentlyAdded",
title: "Recently added", title: "Recently added",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyPlayed", id: "recentlyPlayed",
title: "Recently played", title: "Recently played",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "mostPlayed", id: "mostPlayed",
title: "Most played", title: "Most played",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
}, },
], ],
index: 0, index: 0,
@@ -812,7 +758,7 @@ describe("api", () => {
describe("when an accept-language header is present with value nl-NL", () => { describe("when an accept-language header is present with value nl-NL", () => {
it("should return nl-NL", async () => { it("should return nl-NL", async () => {
ws.addHttpHeader("accept-language", "nl-NL, en-US;q=0.9"); ws.addHttpHeader("accept-language", "nl-NL")
const root = await ws.getMetadataAsync({ const root = await ws.getMetadataAsync({
id: "root", id: "root",
index: 0, index: 0,
@@ -822,72 +768,46 @@ describe("api", () => {
getMetadataResult({ getMetadataResult({
mediaCollection: [ mediaCollection: [
{ {
itemType: "container",
id: "artists", id: "artists",
title: "Artiesten", title: "Artiesten",
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
id: "albums",
title: "Albums",
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
}, },
{ itemType: "albumList", id: "albums", title: "Albums" },
{ {
itemType: "playlist",
id: "playlists", id: "playlists",
title: "Afspeellijsten", title: "Afspeellijsten",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: { attributes: {
readOnly: "false", readOnly: "false",
renameable: "false", renameable: "false",
userContent: "true", userContent: "true",
}, },
}, },
{ itemType: "container", id: "genres", title: "Genres" },
{ {
id: "genres", itemType: "albumList",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
id: "randomAlbums", id: "randomAlbums",
title: "Willekeurig", title: "Willekeurig",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "starredAlbums", id: "starredAlbums",
title: "Favorieten", title: "Favorieten",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyAdded", id: "recentlyAdded",
title: "Onlangs toegevoegd", title: "Onlangs toegevoegd",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "recentlyPlayed", id: "recentlyPlayed",
title: "Onlangs afgespeeld", title: "Onlangs afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
}, },
{ {
itemType: "albumList",
id: "mostPlayed", id: "mostPlayed",
title: "Meest afgespeeld", title: "Meest afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
}, },
], ],
index: 0, index: 0,
@@ -1656,7 +1576,7 @@ describe("api", () => {
); );
expect(musicLibrary.albums).toHaveBeenCalledWith({ expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByName", type: "alphabeticalByArtist",
_index: paging.index, _index: paging.index,
_count: paging.count, _count: paging.count,
}); });
@@ -1702,7 +1622,7 @@ describe("api", () => {
); );
expect(musicLibrary.albums).toHaveBeenCalledWith({ expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByName", type: "alphabeticalByArtist",
_index: paging.index, _index: paging.index,
_count: paging.count, _count: paging.count,
}); });

View File

@@ -10,7 +10,6 @@
"strict": true, "strict": true,
"noImplicitAny": false, "noImplicitAny": false,
"typeRoots" : [ "typeRoots" : [
"../typings",
"../node_modules/@types" "../node_modules/@types"
] ]
}, },

View File

@@ -4,11 +4,9 @@
/* Basic Options */ /* Basic Options */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [ "lib": ["es2019"], /* Specify library files to be included in the compilation. */
"es2019"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -16,8 +14,8 @@
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build" /* Redirect output structure to the directory. */, "outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "." /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */ // "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */ // "removeComments": true, /* Do not emit comments to output. */
@@ -27,35 +25,31 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */, "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true /* Report errors on unused parameters. */, "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */ /* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [ // "typeRoots": [], /* List of folders to include type definitions from. */
"./typings", // "types": [], /* Type declaration files to be included in compilation. */
"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'. */, "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. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -70,7 +64,7 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */ /* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */, "skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
} }
} }

View File

@@ -1,4 +0,0 @@
declare module "scale-that-svg" {
const noTypesYet: any;
export default noTypesYet;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M7 15A6 6 0 1 0 7 27 6 6 0 1 0 7 15zM25 15A6 6 0 1 0 25 27 6 6 0 1 0 25 15zM16 18A2 2 0 1 0 16 22 2 2 0 1 0 16 18z"/><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M30.5,18.6l-2.3-6.4C27.5,10.5,25.9,9.2,24,9v0c0-1.7-1.3-3-3-3s-3,1.3-3,3h-4c0-1.7-1.3-3-3-3S8,7.3,8,9v0c-1.9,0.2-3.5,1.4-4.2,3.3l-2.3,6.4"/></svg>

Before

Width:  |  Height:  |  Size: 473 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><path d="M64.9,98c-0.2,0.2-0.4,0.3-0.7,0.5l-0.1-0.1c-3.7-3-7-6.4-9.9-10l-12.6,4.6c-2.3-6.3-0.1-13.1,4.9-17l0,0c-3.8-8.2-5.9-17.3-5.9-26.7v-16c0.3-0.1,0.5-0.2,0.8-0.2c1.6-0.4,2.5-2.1,2.1-3.7c-0.4-1.6-2.1-2.5-3.7-2.1C26.2,30.9,14.2,38.6,5.1,49.4C4.2,50.5,4,52,4.5,53.3l6.9,19c5.8,16,18.4,28.4,34.6,33.9c2.6,0.9,5.3,1.3,8,1.3c5.1,0,10.2-1.6,14.5-4.7c1.3-1,1.6-2.8,0.7-4.2C68.1,97.4,66.2,97,64.9,98z M34.5,61.5L26,64.6c-1.6,0.6-3.3-0.2-3.8-1.8c-0.6-1.6,0.2-3.3,1.8-3.8l8.5-3.1c1.6-0.6,3.3,0.2,3.8,1.8S36,60.9,34.5,61.5z"/><path d="M118.8,25.9l-0.5-0.3c-21.2-12.2-47.5-12.2-68.8,0c0,0,0,0,0,0c-1.2,0.7-2,2-2,3.4v20.3c0,17.1,7.6,33,20.9,43.7c4.5,3.6,10,5.4,15.5,5.4s11-1.8,15.5-5.4c13.3-10.7,20.9-26.6,20.9-43.7V28.5C120.3,27.4,119.8,26.4,118.8,25.9z M84,79v5.9c0,3.2,1.8,5.7,4.2,7c-5.5,1.3-11.4,0.1-15.9-3.6c-11.8-9.6-18.6-23.8-18.6-39v-19c9.4-5.2,19.9-7.8,30.3-7.8V64h15C99,72.3,92.3,79,84,79z M103.1,47h-9c-1.7,0-3-1.3-3-3s1.3-3,3-3h9c1.7,0,3,1.3,3,3S104.7,47,103.1,47z"/><path d="M84 79l0-15H69C69 72.3 75.7 79 84 79zM76.1 44c0-1.7-1.3-3-3-3h-9c-1.7 0-3 1.3-3 3s1.3 3 3 3h9C74.7 47 76.1 45.7 76.1 44z"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 418 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 525 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" role="img">
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"></path>
<title>Most Played</title>
</svg>

Before

Width:  |  Height:  |  Size: 247 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M22 6h-5v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6zm-7 0H3v2h12V6zm0 4H3v2h12v-2zm-4 4H3v2h8v-2zm4 3c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 360 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" role="img">
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"></path>
<title>Random</title>
</svg>

Before

Width:  |  Height:  |  Size: 352 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zm-7-2h2v-3h3V9h-3V6h-2v3h-3v2h3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zM12 5.5v9l6-4.5z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 324 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<path d="M12 3l.01 10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4.01 4S14 19.21 14 17V7h4V3h-6zm-1.99 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="activeIcon">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

710
yarn.lock
View File

@@ -362,15 +362,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.10.3":
version: 7.15.3
resolution: "@babel/runtime@npm:7.15.3"
dependencies:
regenerator-runtime: ^0.13.4
checksum: 2f0b8d2d4e36035ab1d84af0ec26aafa098536870f27c8e07de0a0e398f7a394fdea68a88165535ffb52ded6a68912bdc3450bdf91f229eb132e1c89470789f5
languageName: node
linkType: hard
"@babel/template@npm:^7.14.5, @babel/template@npm:^7.3.3": "@babel/template@npm:^7.14.5, @babel/template@npm:^7.3.3":
version: 7.14.5 version: 7.14.5
resolution: "@babel/template@npm:7.14.5" resolution: "@babel/template@npm:7.14.5"
@@ -713,15 +704,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@swiftcarrot/color-fns@npm:^3.2.0":
version: 3.2.0
resolution: "@swiftcarrot/color-fns@npm:3.2.0"
dependencies:
"@babel/runtime": ^7.10.3
checksum: 2d966d3db068d8f0489fa77ab28a985e44e07402c5811d3f9d36b45affa9e6ab7982503c4180c5aba70b5d1e973ccc6a3e3a801b2e8b39cd9eec3070a6cd10db
languageName: node
linkType: hard
"@szmarczak/http-timer@npm:^1.1.2": "@szmarczak/http-timer@npm:^1.1.2":
version: 1.1.2 version: 1.1.2
resolution: "@szmarczak/http-timer@npm:1.1.2" resolution: "@szmarczak/http-timer@npm:1.1.2"
@@ -932,13 +914,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/pako@npm:^1.0.1":
version: 1.0.2
resolution: "@types/pako@npm:1.0.2"
checksum: b94d5a82cfe339427549c3e22d77d9c651f15e49e00c7ec3dc274611ad6c8ed9f68231fb133c4c1733cd24506e51c101cd943991af68b429fdadb6d41357e830
languageName: node
linkType: hard
"@types/prettier@npm:^2.0.0": "@types/prettier@npm:^2.0.0":
version: 2.3.0 version: 2.3.0
resolution: "@types/prettier@npm:2.3.0" resolution: "@types/prettier@npm:2.3.0"
@@ -1522,13 +1497,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"blob-util@npm:^2.0.2":
version: 2.0.2
resolution: "blob-util@npm:2.0.2"
checksum: d543e6b92e4ca715ca33c78e89a07a2290d43e5b2bc897d7ec588c5c7bbf59df93e45225ac0c9258aa6ce4320358990f99c9288f1c48280f8ec5d7a2e088d19b
languageName: node
linkType: hard
"body-parser@npm:1.19.0": "body-parser@npm:1.19.0":
version: 1.19.0 version: 1.19.0
resolution: "body-parser@npm:1.19.0" resolution: "body-parser@npm:1.19.0"
@@ -1569,12 +1537,10 @@ __metadata:
express: ^4.17.1 express: ^4.17.1
fp-ts: ^2.9.5 fp-ts: ^2.9.5
get-port: ^5.1.1 get-port: ^5.1.1
image-js: ^0.32.0
jest: ^26.6.3 jest: ^26.6.3
morgan: ^1.10.0 morgan: ^1.10.0
node-html-parser: ^2.1.0 node-html-parser: ^2.1.0
nodemon: ^2.0.7 nodemon: ^2.0.7
scale-that-svg: ^1.0.5
sharp: ^0.27.2 sharp: ^0.27.2
soap: ^0.37.0 soap: ^0.37.0
supertest: ^6.1.3 supertest: ^6.1.3
@@ -1804,13 +1770,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"canny-edge-detector@npm:^1.0.0":
version: 1.0.0
resolution: "canny-edge-detector@npm:1.0.0"
checksum: a38e9117219ac5819072d5ae119f61a87f3aa32d4a3e7bc0821e82aa92ca35bdf99f227fb9b9049cf9b9ae31719c868087b904d3b2ecaab86eade47eafc35bac
languageName: node
linkType: hard
"capture-exit@npm:^2.0.0": "capture-exit@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "capture-exit@npm:2.0.0" resolution: "capture-exit@npm:2.0.0"
@@ -1945,17 +1904,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"clean-deep@npm:3.0.2":
version: 3.0.2
resolution: "clean-deep@npm:3.0.2"
dependencies:
lodash.isempty: ^4.4.0
lodash.isplainobject: ^4.0.6
lodash.transform: ^4.6.0
checksum: e086672a5dd049bdee7d0fba7ba3bfd61cba5c4ec12de672c2d2954c657d0f113d42a46450a6bb0da70e682271bebbd961fd23359efe6a5f5321192822da4655
languageName: node
linkType: hard
"clean-stack@npm:^2.0.0": "clean-stack@npm:^2.0.0":
version: 2.2.0 version: 2.2.0
resolution: "clean-stack@npm:2.2.0" resolution: "clean-stack@npm:2.2.0"
@@ -2412,16 +2360,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"deep-rename-keys@npm:^0.2.1":
version: 0.2.1
resolution: "deep-rename-keys@npm:0.2.1"
dependencies:
kind-of: ^3.0.2
rename-keys: ^1.1.2
checksum: 34c838a7ee375e9579be2ba1de59a5ec5aef46bee96bb08cffdd8b6b3887d941bbfce0caccf3ecd7c5a74e6f3a70c7df0b469e732a97e5849a5149ddc1e2a062
languageName: node
linkType: hard
"deepmerge@npm:^4.2.2": "deepmerge@npm:^4.2.2":
version: 4.2.2 version: 4.2.2
resolution: "deepmerge@npm:4.2.2" resolution: "deepmerge@npm:4.2.2"
@@ -2578,13 +2516,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"element-to-path@npm:1.2.0":
version: 1.2.0
resolution: "element-to-path@npm:1.2.0"
checksum: 65c99a2ec3be262323ae94b55c32da643910e25560ca5fc72d5e1b5e0cef241d62827fec29080d7739c66c2395a54e5fb66d8b70eb55aac01fbcbf2606a0b8db
languageName: node
linkType: hard
"emittery@npm:^0.7.1": "emittery@npm:^0.7.1":
version: 0.7.2 version: 0.7.2
resolution: "emittery@npm:0.7.2" resolution: "emittery@npm:0.7.2"
@@ -2753,13 +2684,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eventemitter3@npm:^2.0.0":
version: 2.0.3
resolution: "eventemitter3@npm:2.0.3"
checksum: dfbf4a07144afea0712d8e6a7f30ae91beb7c12c36c3d480818488aafa437d9a331327461f82c12dfd60a4fbad502efc97f684089cda02809988b84a23630752
languageName: node
linkType: hard
"exec-sh@npm:^0.3.2": "exec-sh@npm:^0.3.2":
version: 0.3.6 version: 0.3.6
resolution: "exec-sh@npm:0.3.6" resolution: "exec-sh@npm:0.3.6"
@@ -2936,15 +2860,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-bmp@npm:^1.0.0":
version: 1.0.0
resolution: "fast-bmp@npm:1.0.0"
dependencies:
iobuffer: ^3.1.0
checksum: aa9a6242c51d8c4ce7e1059dc8dae3dc1e099b2f651a928fbdca9bfa6215288dbc5909f0b75d3b2688ab65bad87f12565aac21c25c7edb712c9554a76cd3c940
languageName: node
linkType: hard
"fast-deep-equal@npm:^3.1.1": "fast-deep-equal@npm:^3.1.1":
version: 3.1.3 version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3" resolution: "fast-deep-equal@npm:3.1.3"
@@ -2952,16 +2867,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-jpeg@npm:^1.0.1":
version: 1.0.1
resolution: "fast-jpeg@npm:1.0.1"
dependencies:
iobuffer: ^2.1.0
tiff: ^2.0.0
checksum: 60b40f4f8f61989bcabac820e3b737edfed0a11a766eb034dee69f51abb795ffcdaa5487af40299fc2f2470aed130049de464e1624abf197f67e5f0c84b94f2a
languageName: node
linkType: hard
"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0": "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0":
version: 2.1.0 version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0" resolution: "fast-json-stable-stringify@npm:2.1.0"
@@ -2976,24 +2881,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-list@npm:^1.0.3":
version: 1.0.3
resolution: "fast-list@npm:1.0.3"
checksum: 2bc01386f6c994c35f188cbb1bac5dcff1937c8910b8fdae079d2e7a28a9d6ebfb8b1a9247cdf5c6aa92778cd85e14dca442bcf56d860947cd16f138c9252605
languageName: node
linkType: hard
"fast-png@npm:^5.0.4":
version: 5.0.4
resolution: "fast-png@npm:5.0.4"
dependencies:
"@types/pako": ^1.0.1
iobuffer: ^5.0.2
pako: ^2.0.2
checksum: e7f2cce48821536619d3c38f537635da0090ef01c71229f3c30b5f32c3d9de14d4509b21682a22e4deaddfac8cc9b5f6a23f2e7883521913483cdba8b657d6c6
languageName: node
linkType: hard
"fast-safe-stringify@npm:^2.0.4, fast-safe-stringify@npm:^2.0.7": "fast-safe-stringify@npm:^2.0.4, fast-safe-stringify@npm:^2.0.7":
version: 2.0.7 version: 2.0.7
resolution: "fast-safe-stringify@npm:2.0.7" resolution: "fast-safe-stringify@npm:2.0.7"
@@ -3026,20 +2913,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fft.js@npm:^4.0.3":
version: 4.0.4
resolution: "fft.js@npm:4.0.4"
checksum: 55e4f5ee0afc9ba9b3759c6fe303e0a2f2c1ebb9bc525914de9699b76f55e01da97d61ec30dedde0dfb0a8be3c0711d47af7aca53a1425f9875468254b704c7f
languageName: node
linkType: hard
"file-type@npm:^10.10.0":
version: 10.11.0
resolution: "file-type@npm:10.11.0"
checksum: cadd8cd187692dcde637a3ff53bb51c5d935633fc8085e7d25bfb3b4bf995e14a43f2baf71bdcb9d7235b3e725bd158b75d25911fa2f73e5812955382228c511
languageName: node
linkType: hard
"fill-range@npm:^4.0.0": "fill-range@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "fill-range@npm:4.0.0" resolution: "fill-range@npm:4.0.0"
@@ -3451,13 +3324,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"has-own@npm:^1.0.1":
version: 1.0.1
resolution: "has-own@npm:1.0.1"
checksum: 3893dd749c56fbda5c8a6277cf627d5ec8de2094fd8beb2cb17d492c1773e5826d092512714b94ffa11fb8c2f5c31106adf6dd60d53680c2846246addff94338
languageName: node
linkType: hard
"has-symbols@npm:^1.0.1": "has-symbols@npm:^1.0.1":
version: 1.0.2 version: 1.0.2
resolution: "has-symbols@npm:1.0.2" resolution: "has-symbols@npm:1.0.2"
@@ -3696,48 +3562,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"image-js@npm:^0.32.0":
version: 0.32.0
resolution: "image-js@npm:0.32.0"
dependencies:
"@swiftcarrot/color-fns": ^3.2.0
blob-util: ^2.0.2
canny-edge-detector: ^1.0.0
fast-bmp: ^1.0.0
fast-jpeg: ^1.0.1
fast-list: ^1.0.3
fast-png: ^5.0.4
has-own: ^1.0.1
image-type: ^4.1.0
is-array-type: ^1.0.0
is-integer: ^1.0.7
jpeg-js: ^0.4.3
js-priority-queue: ^0.1.5
js-quantities: ^1.7.6
median-quickselect: ^1.0.1
ml-convolution: 0.2.0
ml-disjoint-set: ^1.0.0
ml-matrix: ^6.8.0
ml-matrix-convolution: 0.4.3
ml-regression: ^5.0.0
monotone-chain-convex-hull: ^1.0.0
new-array: ^1.0.0
robust-point-in-polygon: ^1.0.3
tiff: ^5.0.0
web-worker-manager: ^0.2.0
checksum: 0c5f672a9c6c2dbcdc698317ce964cf21294901d5fe18d5b996d64c3fb2f16699a8c6957bd60e825c582b0d86c35ea7758f9870ca80f2b552275e44f48bdd622
languageName: node
linkType: hard
"image-type@npm:^4.1.0":
version: 4.1.0
resolution: "image-type@npm:4.1.0"
dependencies:
file-type: ^10.10.0
checksum: debb29d8de4f5f00617cde8935e5c63bbe2d5ce76d0b387e98066572474b00002af086fd0093553ef9aad9f6f6b76e22af4eaad80d7c420794c39576304be2e5
languageName: node
linkType: hard
"import-lazy@npm:^2.1.0": "import-lazy@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "import-lazy@npm:2.1.0" resolution: "import-lazy@npm:2.1.0"
@@ -3816,29 +3640,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iobuffer@npm:^2.1.0":
version: 2.1.0
resolution: "iobuffer@npm:2.1.0"
checksum: a283788bbe642a440f729b7123abd873cf1f2a7204eb30d19752c0eb0775d9bfcc785dbabb52b586788092cce19e6e0da83085ad7297a7bdbcd19c001a0851c8
languageName: node
linkType: hard
"iobuffer@npm:^3.1.0":
version: 3.2.0
resolution: "iobuffer@npm:3.2.0"
dependencies:
utf8: ^2.1.2
checksum: 50c1547ac138da6ea7c09b394040e6dbbbe96af93553d43e7076f78b741745a7d781ac38a27be5ff4b1ad24545edb64020940254860e873bac1660acce3c030f
languageName: node
linkType: hard
"iobuffer@npm:^5.0.2, iobuffer@npm:^5.0.3":
version: 5.0.3
resolution: "iobuffer@npm:5.0.3"
checksum: e30548416afcfa9b0bca6122a9674b087e4a5f9082f21a6d7722833782536d8d7966eea786dd2082057363a513c34a339bec7888bb125dd6c4348670e4b30a1a
languageName: node
linkType: hard
"ip@npm:^1.1.5": "ip@npm:^1.1.5":
version: 1.1.5 version: 1.1.5
resolution: "ip@npm:1.1.5" resolution: "ip@npm:1.1.5"
@@ -3871,20 +3672,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-any-array@npm:^1.0.0":
version: 1.0.0
resolution: "is-any-array@npm:1.0.0"
checksum: c24654b1dc34b2e543c1a3a5fc8169c3fd467ba9ffc2e44b0e04561dd5549e71d063006fe0ad27359f9964cd6a0d4870375b3753b14b878c9ff5d7853849727e
languageName: node
linkType: hard
"is-array-type@npm:^1.0.0":
version: 1.0.0
resolution: "is-array-type@npm:1.0.0"
checksum: 14d7efede3221f04c7d6b1166f5bdff34463251b95521f821a9f6cde88a9fda715a1393a80aaebc15494d2e971a0b348b0c92de267bde63a3b18bfeae80da732
languageName: node
linkType: hard
"is-arrayish@npm:^0.2.1": "is-arrayish@npm:^0.2.1":
version: 0.2.1 version: 0.2.1
resolution: "is-arrayish@npm:0.2.1" resolution: "is-arrayish@npm:0.2.1"
@@ -4007,13 +3794,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-finite@npm:^1.0.0":
version: 1.1.0
resolution: "is-finite@npm:1.1.0"
checksum: 532b97ed3d03e04c6bd203984d9e4ba3c0c390efee492bad5d1d1cd1802a68ab27adbd3ef6382f6312bed6c8bb1bd3e325ea79a8dc8fe080ed7a06f5f97b93e7
languageName: node
linkType: hard
"is-fullwidth-code-point@npm:^1.0.0": "is-fullwidth-code-point@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "is-fullwidth-code-point@npm:1.0.0" resolution: "is-fullwidth-code-point@npm:1.0.0"
@@ -4063,15 +3843,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-integer@npm:^1.0.7":
version: 1.0.7
resolution: "is-integer@npm:1.0.7"
dependencies:
is-finite: ^1.0.0
checksum: e57ab783fa401df8f86a80f8e47d3e7dfdd19a0acb7183d7bb1e55830364172d7035b6980e98d856a2508b261923803d800c2a430c1e6801d9792c3394827f30
languageName: node
linkType: hard
"is-lambda@npm:^1.0.1": "is-lambda@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "is-lambda@npm:1.0.1" resolution: "is-lambda@npm:1.0.1"
@@ -4116,7 +3887,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-plain-object@npm:^2.0.1, is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": "is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4":
version: 2.0.4 version: 2.0.4
resolution: "is-plain-object@npm:2.0.4" resolution: "is-plain-object@npm:2.0.4"
dependencies: dependencies:
@@ -4711,27 +4482,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jpeg-js@npm:^0.4.3":
version: 0.4.3
resolution: "jpeg-js@npm:0.4.3"
checksum: 9e5bacc9135efa7da340b62e81fa56fab0c8516ef617228758132af5b7d31b516cc6e1500cdffb82d3161629be341be980099f2b37eb76b81e26db6e3e848c77
languageName: node
linkType: hard
"js-priority-queue@npm:^0.1.5":
version: 0.1.5
resolution: "js-priority-queue@npm:0.1.5"
checksum: 741a54456101e5625b8b19080b4e5cc329ebc443b21221f809afa807a2a2f7c0cbf717649166de079013753adc89e82b60cb3eeb02bb9250808ff8fc36bd7a00
languageName: node
linkType: hard
"js-quantities@npm:^1.7.6":
version: 1.7.6
resolution: "js-quantities@npm:1.7.6"
checksum: ab3ea04650d2581b09540328e4d644576e998de3ec8c67c7fd113ea2e8520a304568120a813d043a8b529243fecb303effacdd3ca0ad9e077394306749bae715
languageName: node
linkType: hard
"js-tokens@npm:^4.0.0": "js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
@@ -4962,27 +4712,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash.isempty@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.isempty@npm:4.4.0"
checksum: a8118f23f7ed72a1dbd176bf27f297d1e71aa1926288449cb8f7cef99ba1bc7527eab52fe7899ab080fa1dc150aba6e4a6367bf49fa4e0b78da1ecc095f8d8c5
languageName: node
linkType: hard
"lodash.isplainobject@npm:^4.0.6":
version: 4.0.6
resolution: "lodash.isplainobject@npm:4.0.6"
checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337
languageName: node
linkType: hard
"lodash.transform@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.transform@npm:4.6.0"
checksum: f9d0f583409212e4e94c08c0de1c9e71679e26658d2645be16ee6db55ee2572db5a8395c76f471c00c7d18f3a86c781f7ac51238a7cfa29e9cca253aa0b97149
languageName: node
linkType: hard
"lodash@npm:4.x, lodash@npm:^4.17.19, lodash@npm:^4.17.5, lodash@npm:^4.7.0": "lodash@npm:4.x, lodash@npm:^4.17.19, lodash@npm:^4.17.5, lodash@npm:^4.7.0":
version: 4.17.21 version: 4.17.21
resolution: "lodash@npm:4.17.21" resolution: "lodash@npm:4.17.21"
@@ -5097,13 +4826,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"median-quickselect@npm:^1.0.1":
version: 1.0.1
resolution: "median-quickselect@npm:1.0.1"
checksum: a01a7270c9c48be15aed3036b78bfcf0c0417987fafa3add8ba7293250d484dc2b6ba4abc527492dbfc6f94880bd353b5f3eafc62429baa866101f51a8a5eb61
languageName: node
linkType: hard
"merge-descriptors@npm:1.0.1": "merge-descriptors@npm:1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "merge-descriptors@npm:1.0.1" resolution: "merge-descriptors@npm:1.0.1"
@@ -5330,240 +5052,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ml-array-max@npm:^1.2.3":
version: 1.2.3
resolution: "ml-array-max@npm:1.2.3"
dependencies:
is-any-array: ^1.0.0
checksum: c97e4395653edf15ea2a050d06fd7590306da0b18f0adee985d4db32860f9dd4caa30c22f5ef859164094eab6d13b6e4e71796e07c33625fd65e07d3fda2b5f7
languageName: node
linkType: hard
"ml-array-median@npm:^1.1.1":
version: 1.1.5
resolution: "ml-array-median@npm:1.1.5"
dependencies:
is-any-array: ^1.0.0
median-quickselect: ^1.0.1
checksum: 235845cac797ccd8ef47cc83415496fa0d304e7f5b5ef2d307faa6270b71e0ccef7bcc422709e8c4a0b7e98898dda5c00f19272b075e3af8682dff83ce8fd90c
languageName: node
linkType: hard
"ml-array-min@npm:^1.2.2":
version: 1.2.2
resolution: "ml-array-min@npm:1.2.2"
dependencies:
is-any-array: ^1.0.0
checksum: 1b5da44a51ad2b4720828ed69a856dda63c5e20adeb2fbbcd622e76c14697f97e8e61d504cbb66fffc6c88fe6841344abd6f0aa92209622b8d9c672073dba3f3
languageName: node
linkType: hard
"ml-array-rescale@npm:^1.3.5":
version: 1.3.5
resolution: "ml-array-rescale@npm:1.3.5"
dependencies:
is-any-array: ^1.0.0
ml-array-max: ^1.2.3
ml-array-min: ^1.2.2
checksum: 5abfe1070cde8d38bf8773b55de83c17627271c92c5e1360723b919ecff0b145ce29dd7e7a4d7f1ee603f0ebb780e912e1348ceaa9b7c17855a075c0eb99aa17
languageName: node
linkType: hard
"ml-convolution@npm:0.2.0":
version: 0.2.0
resolution: "ml-convolution@npm:0.2.0"
dependencies:
fft.js: ^4.0.3
next-power-of-two: ^1.0.0
checksum: 95f625e302f5aeff5f0b14236c23b924a2571c945a4583ef256c05ddf81388c75435186c223efc61595c1d5b35b7e9abed128cd69b87a98e9ea2cbf4fab2f2ed
languageName: node
linkType: hard
"ml-disjoint-set@npm:^1.0.0":
version: 1.0.0
resolution: "ml-disjoint-set@npm:1.0.0"
checksum: 0bc6f29243863da943da0c8552672477882acf53a86d8dac9ad177d892d8825604cddb79655ff98874f18f82a23cb1266301e050d5efb5dc4c951e4da6fd37f0
languageName: node
linkType: hard
"ml-distance-euclidean@npm:^2.0.0":
version: 2.0.0
resolution: "ml-distance-euclidean@npm:2.0.0"
checksum: e31f98a947ce6971c35d74e6d2521800f0d219efb34c78b20b5f52debd206008d52e677685c09839e6bab5d2ed233aa009314236e4e548d5fafb60f2f71e2b3e
languageName: node
linkType: hard
"ml-fft@npm:1.3.5":
version: 1.3.5
resolution: "ml-fft@npm:1.3.5"
checksum: 8dfa57e640a7b0259038e28fa497e66eed9e63140d9ab4c56182721b13777c97c6ce1874f00fbe7cb2eb18d78dece90bbee26d9b96bd35c3901b972d4d608638
languageName: node
linkType: hard
"ml-kernel-gaussian@npm:^2.0.2":
version: 2.0.2
resolution: "ml-kernel-gaussian@npm:2.0.2"
dependencies:
ml-distance-euclidean: ^2.0.0
checksum: 5219a769ae046fea4612e186a1b1a400c19e8c8f12508665c6f553790da8bfd2bef20851feab4c005324f501ded3b7195770ff6b2e670f9e8c2a89a0a7d1cc43
languageName: node
linkType: hard
"ml-kernel-polynomial@npm:^2.0.1":
version: 2.0.1
resolution: "ml-kernel-polynomial@npm:2.0.1"
checksum: a5b75efa8ca97729b8882b930f50a3c604ea827bff61384fca7f487098c32ba01c1b34bd75c4b163d0e44a4a531bbdc018ac584ebca28b9158a9bd86e752fe71
languageName: node
linkType: hard
"ml-kernel-sigmoid@npm:^1.0.1":
version: 1.0.1
resolution: "ml-kernel-sigmoid@npm:1.0.1"
checksum: ec30b4ff11be1d33b677cddc399bb7e3797a0105a6ee9cdfd59995a92a651c6e220f4a8e104f54e97407d0bb048f6f158ef6324aaac6f07ca553ce9e121ff175
languageName: node
linkType: hard
"ml-kernel@npm:^3.0.0":
version: 3.0.0
resolution: "ml-kernel@npm:3.0.0"
dependencies:
ml-distance-euclidean: ^2.0.0
ml-kernel-gaussian: ^2.0.2
ml-kernel-polynomial: ^2.0.1
ml-kernel-sigmoid: ^1.0.1
ml-matrix: ^6.1.2
checksum: 095521c766d1f65e13ef6f9a6d69fa11ef980fa2934142aa788b632f228b7afaf7cd1205012c073f18899aae6fc6393be9f0ed583c79a615035927ffe2e15ebb
languageName: node
linkType: hard
"ml-matrix-convolution@npm:0.4.3":
version: 0.4.3
resolution: "ml-matrix-convolution@npm:0.4.3"
dependencies:
ml-fft: 1.3.5
ml-stat: ^1.2.0
checksum: c145ed1debea80df91ff49b78a039b74c64843a1990fea70532b79145a55792ecedd1ecf2bc5e831a447ec9c8c58de2921e9bf701d77fade8f21c700f51b7cf3
languageName: node
linkType: hard
"ml-matrix@npm:^6.1.2, ml-matrix@npm:^6.4.1, ml-matrix@npm:^6.8.0":
version: 6.8.0
resolution: "ml-matrix@npm:6.8.0"
dependencies:
ml-array-rescale: ^1.3.5
checksum: 5d7456e981697148b8ab5c593a1c7d764a337de4e582b61dd830fde299e46a694fbe43d2d2c8321c9d27e71eed00a8285b0c6b9f89b437dc6cad93607813ed1d
languageName: node
linkType: hard
"ml-regression-base@npm:^2.0.1, ml-regression-base@npm:^2.1.3":
version: 2.1.3
resolution: "ml-regression-base@npm:2.1.3"
dependencies:
is-any-array: ^1.0.0
checksum: a0517456163318dee071c1f3ccb092ff1453ae0da24484d497b1e835f5eac9d3ddf7aa2577d4a766ccaf821ef72c146c0c1170770bbb7586d59557207d95592b
languageName: node
linkType: hard
"ml-regression-exponential@npm:^2.0.0":
version: 2.1.0
resolution: "ml-regression-exponential@npm:2.1.0"
dependencies:
ml-regression-base: ^2.1.3
ml-regression-simple-linear: ^2.0.3
checksum: 613c6c34135503ab0db12428eaebab67e8b7f22e7e8ed2c22c717b5fe406d97e6c28fd0e45f448d2ee64bd3eccdb7f42c8be700e8b4d7756d81b8c5a1b67953b
languageName: node
linkType: hard
"ml-regression-multivariate-linear@npm:^2.0.2":
version: 2.0.3
resolution: "ml-regression-multivariate-linear@npm:2.0.3"
dependencies:
ml-matrix: ^6.4.1
checksum: 4e8ebc124f0bb51229c8adf2b23e01d412745b1212901182ed9a76f4d88031f24c443d743e50e0711d6ca632052cb7cf326c8d4c92218a6af55628d56d052f42
languageName: node
linkType: hard
"ml-regression-polynomial@npm:^2.0.0":
version: 2.2.0
resolution: "ml-regression-polynomial@npm:2.2.0"
dependencies:
ml-matrix: ^6.8.0
ml-regression-base: ^2.1.3
checksum: 4eca53fabf0fb875416b3155373360809230bc1cab91dfae22582522785054c34fcfa89889235649f024b50d26d916d936a1530c10cada1aa5551d280e7e16a6
languageName: node
linkType: hard
"ml-regression-power@npm:^2.0.0":
version: 2.0.0
resolution: "ml-regression-power@npm:2.0.0"
dependencies:
ml-regression-base: ^2.0.1
ml-regression-simple-linear: ^2.0.2
checksum: 847fec484126706bbbdeab8c2145ac533401e00513ab625ee44c42ee184d89514732ef9bd911c1911b19766864e08984c3ccb3a7f65318340608e7022a5e1294
languageName: node
linkType: hard
"ml-regression-robust-polynomial@npm:^2.0.0":
version: 2.0.0
resolution: "ml-regression-robust-polynomial@npm:2.0.0"
dependencies:
ml-matrix: ^6.1.2
ml-regression-base: ^2.0.1
checksum: 3bc061cddb745dfafe48f70315e9fc2c54fcfa42884125117b8c6489e388be99a4636181811bfa30e0bb8c7ccebf0f4a7740afd92cd16f5de368687007a19df0
languageName: node
linkType: hard
"ml-regression-simple-linear@npm:^2.0.2, ml-regression-simple-linear@npm:^2.0.3":
version: 2.0.3
resolution: "ml-regression-simple-linear@npm:2.0.3"
dependencies:
ml-regression-base: ^2.0.1
checksum: 61812f3c6dae61d948e7741938c85fcde87fad7b3a7f0403bcd3e466892f0bb7087373453c55e52089f9dd7dd22ab726ef91ca621bb13fc88100075739135d39
languageName: node
linkType: hard
"ml-regression-theil-sen@npm:^2.0.0":
version: 2.0.0
resolution: "ml-regression-theil-sen@npm:2.0.0"
dependencies:
ml-array-median: ^1.1.1
ml-regression-base: ^2.0.1
checksum: 0f05a537a34e07a9ea016ea092f92d7ef33a650e5dbfd0b1b7c433d94024780fa73ef06b1605fe94a2cad17d28649726062c86af15c4c9f16c40393e1d3afe6e
languageName: node
linkType: hard
"ml-regression@npm:^5.0.0":
version: 5.0.0
resolution: "ml-regression@npm:5.0.0"
dependencies:
ml-kernel: ^3.0.0
ml-matrix: ^6.1.2
ml-regression-base: ^2.0.1
ml-regression-exponential: ^2.0.0
ml-regression-multivariate-linear: ^2.0.2
ml-regression-polynomial: ^2.0.0
ml-regression-power: ^2.0.0
ml-regression-robust-polynomial: ^2.0.0
ml-regression-simple-linear: ^2.0.2
ml-regression-theil-sen: ^2.0.0
checksum: 1f9f31e36e469672828efb81c971f686c2b18afb1b633c94dedc52d944adfd79324e94b9ecf1a3ce7f22171d17111aaed55feb26651984f961eb07e322b7821e
languageName: node
linkType: hard
"ml-stat@npm:^1.2.0":
version: 1.3.3
resolution: "ml-stat@npm:1.3.3"
checksum: ff397cc84f2f3d248e68cb8c7051e391ba2fcdf3b3a3bc7ba52c215f58be6cdeeb2bf024cf83f2f2ede8db58c147f29c8fea7bf3cfc5ce513da2699b42b6fabc
languageName: node
linkType: hard
"monotone-chain-convex-hull@npm:^1.0.0":
version: 1.0.0
resolution: "monotone-chain-convex-hull@npm:1.0.0"
checksum: 333aa6dc628f78334ec45e5adec9c320c6662980aa8d09db5d27fbdcdb7e6f9dad6f32cc298dc45a8db163ed493ef5da9727204590bb7b371482f6aa8b14fa09
languageName: node
linkType: hard
"morgan@npm:^1.10.0": "morgan@npm:^1.10.0":
version: 1.10.0 version: 1.10.0
resolution: "morgan@npm:1.10.0" resolution: "morgan@npm:1.10.0"
@@ -5645,20 +5133,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"new-array@npm:^1.0.0":
version: 1.0.0
resolution: "new-array@npm:1.0.0"
checksum: 844ac096924b618a372558e421ee0faa81a582538f28ef97a571e2b2579a1abedb360be6c726f371e9dfdf73283a882d58ac7d7fa6b870b9fcbf80038fddbba0
languageName: node
linkType: hard
"next-power-of-two@npm:^1.0.0":
version: 1.0.0
resolution: "next-power-of-two@npm:1.0.0"
checksum: a77ee4a1ed42ff5fe6dd73e0485ac259fe73fb9b6395eca861dfcf2a0e3051a16801c1f8c90a33bb4a38a89aaf89f5f72efbe30884632f5439458f38419e778e
languageName: node
linkType: hard
"nice-try@npm:^1.0.4": "nice-try@npm:^1.0.4":
version: 1.0.5 version: 1.0.5
resolution: "nice-try@npm:1.0.5" resolution: "nice-try@npm:1.0.5"
@@ -5926,16 +5400,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"omit-deep@npm:0.3.0":
version: 0.3.0
resolution: "omit-deep@npm:0.3.0"
dependencies:
is-plain-object: ^2.0.1
unset-value: ^0.1.1
checksum: ca603591af98f717ee4e4ae199778d386304f80072164fc1fb9c27abb011845faa27ffb32e7fa4a240698a4d54822526059af74f12f4f73315ecd7f03825d590
languageName: node
linkType: hard
"on-finished@npm:~2.3.0": "on-finished@npm:~2.3.0":
version: 2.3.0 version: 2.3.0
resolution: "on-finished@npm:2.3.0" resolution: "on-finished@npm:2.3.0"
@@ -6060,13 +5524,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pako@npm:^2.0.2, pako@npm:^2.0.3":
version: 2.0.4
resolution: "pako@npm:2.0.4"
checksum: 82b9b0b99dd830c9103856a6dbd10f0cb2c8c32b9768184727ea381a99666de9a47a069d2e6efe6acf09336f363956b50835c196ef9311b34b7274d420eb0d88
languageName: node
linkType: hard
"parse-json@npm:^5.0.0": "parse-json@npm:^5.0.0":
version: 5.2.0 version: 5.2.0
resolution: "parse-json@npm:5.2.0" resolution: "parse-json@npm:5.2.0"
@@ -6442,13 +5899,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"regenerator-runtime@npm:^0.13.4":
version: 0.13.9
resolution: "regenerator-runtime@npm:0.13.9"
checksum: 65ed455fe5afd799e2897baf691ca21c2772e1a969d19bb0c4695757c2d96249eb74ee3553ea34a91062b2a676beedf630b4c1551cc6299afb937be1426ec55e
languageName: node
linkType: hard
"regex-not@npm:^1.0.0, regex-not@npm:^1.0.2": "regex-not@npm:^1.0.0, regex-not@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "regex-not@npm:1.0.2" resolution: "regex-not@npm:1.0.2"
@@ -6484,13 +5934,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rename-keys@npm:^1.1.2":
version: 1.2.0
resolution: "rename-keys@npm:1.2.0"
checksum: 9d8e5ca3d1ae3fe6c0d7319a3fd80ded6ca34651e85bff27604982dcc750aed28d1a621374224a9c9072083769f5eab1fd86d1d5a53f54f96c7705c18267227b
languageName: node
linkType: hard
"repeat-element@npm:^1.1.2": "repeat-element@npm:^1.1.2":
version: 1.1.4 version: 1.1.4
resolution: "repeat-element@npm:1.1.4" resolution: "repeat-element@npm:1.1.4"
@@ -6624,51 +6067,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"robust-orientation@npm:^1.0.2":
version: 1.2.1
resolution: "robust-orientation@npm:1.2.1"
dependencies:
robust-scale: ^1.0.2
robust-subtract: ^1.0.0
robust-sum: ^1.0.0
two-product: ^1.0.2
checksum: 83b87300009716d96cf17af27b2c787bb7cabe00e82b6740ff4777a601babfcf132b3ec3d10cb1a91886423aa51863026d3befd58058af3b90be98abbda0056e
languageName: node
linkType: hard
"robust-point-in-polygon@npm:^1.0.3":
version: 1.0.3
resolution: "robust-point-in-polygon@npm:1.0.3"
dependencies:
robust-orientation: ^1.0.2
checksum: dc68ef96f6f2c6d2087f8e74583dc2e9add1a86aef402096fd1c9dde5c9ec1209f6710178a053cc48cbf4103488c382ae2a26302b2d27bb3dce4d81f0c4c5951
languageName: node
linkType: hard
"robust-scale@npm:^1.0.2":
version: 1.0.2
resolution: "robust-scale@npm:1.0.2"
dependencies:
two-product: ^1.0.2
two-sum: ^1.0.0
checksum: 4217f15c94bc803c0c78f6011507102cb603a4e9f71721d44e155c17c1fbe989382c8a150d20e23ca51164077395dab698498b9650d2377cc0a69902d73d0a1c
languageName: node
linkType: hard
"robust-subtract@npm:^1.0.0":
version: 1.0.0
resolution: "robust-subtract@npm:1.0.0"
checksum: e9dcc39a1a802d4a34d338844d9382ad7e49f58c5d01ce0d66cd18d6477069475af11a80fba0c0e158211c2b272c1c05950e78cbfc29ea7005f4ecc9e9f9d492
languageName: node
linkType: hard
"robust-sum@npm:^1.0.0":
version: 1.0.0
resolution: "robust-sum@npm:1.0.0"
checksum: b9f32829ba3d6fd9cffeee440e1fb93a7d42f264540bd631abf13d0e8737f3a15a16a15764fa8a2fe86d3db6a1970361cf7ad2ed536c858b59e45f6f493a454b
languageName: node
linkType: hard
"rsvp@npm:^4.8.4": "rsvp@npm:^4.8.4":
version: 4.8.5 version: 4.8.5
resolution: "rsvp@npm:4.8.5" resolution: "rsvp@npm:4.8.5"
@@ -6741,17 +6139,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"scale-that-svg@npm:^1.0.5":
version: 1.0.5
resolution: "scale-that-svg@npm:1.0.5"
dependencies:
element-to-path: 1.2.0
svg-path-tools: 1.0.0
svgson: 3.0.0
checksum: ebad60633871e2a2d7a32aa68c0f390bc4c6d1d20149740ebaf8feae7086faa7e607d06a552ba8f34454d2954d081ec3324942c2aaaae066a2eec29c0c1eedbe
languageName: node
linkType: hard
"semver-diff@npm:^3.1.1": "semver-diff@npm:^3.1.1":
version: 3.1.1 version: 3.1.1
resolution: "semver-diff@npm:3.1.1" resolution: "semver-diff@npm:3.1.1"
@@ -7428,25 +6815,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"svg-path-tools@npm:1.0.0":
version: 1.0.0
resolution: "svg-path-tools@npm:1.0.0"
checksum: 8e44971bc160dbd459256a4ebcd05bbe80e2fed4824d6c58fd1a303da65982d0df62d3eec4198720f46077095ea1436537829b216d995e56ed8d50cb621029ea
languageName: node
linkType: hard
"svgson@npm:3.0.0":
version: 3.0.0
resolution: "svgson@npm:3.0.0"
dependencies:
clean-deep: 3.0.2
deep-rename-keys: ^0.2.1
omit-deep: 0.3.0
xml-reader: 2.4.3
checksum: 81d828a2f8af8b320d857b536bbae8d88e53e4543478e707230310f46dca08a4b2da1c00ac49e65fcbf79d39a673ccfbd9dfb8e412892a3584711cebc3ca2f30
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4": "symbol-tree@npm:^3.2.4":
version: 3.2.4 version: 3.2.4
resolution: "symbol-tree@npm:3.2.4" resolution: "symbol-tree@npm:3.2.4"
@@ -7535,25 +6903,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tiff@npm:^2.0.0":
version: 2.1.0
resolution: "tiff@npm:2.1.0"
dependencies:
iobuffer: ^2.1.0
checksum: 2e50c57964b67ede02598460bfd0670f0993773632f996481ba5b65274df65d4f8776bbf6c4c8efda6beaecda9db9f64b65cdb3663783e4a99666bc15a65695c
languageName: node
linkType: hard
"tiff@npm:^5.0.0":
version: 5.0.0
resolution: "tiff@npm:5.0.0"
dependencies:
iobuffer: ^5.0.3
pako: ^2.0.3
checksum: 9f10288ec3153b0200b725bbb5a7b4b88d3c78d699fecaf289c3bdcadd892ce495a5dc9152c38a74f8bf0c457c0bab0c9b0bbccb9e5f25bcc3d6b44418619396
languageName: node
linkType: hard
"tmpl@npm:1.0.x": "tmpl@npm:1.0.x":
version: 1.0.4 version: 1.0.4
resolution: "tmpl@npm:1.0.4" resolution: "tmpl@npm:1.0.4"
@@ -7746,20 +7095,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"two-product@npm:^1.0.2":
version: 1.0.2
resolution: "two-product@npm:1.0.2"
checksum: b289814957df58b91c910c944e7e247aa01a0a70e8fafdf58f01baf7fa1f96c06dc1cbb6cdafb39525e9a5ac0a9566875f1a76a02ef1f736f26e56fca2f0c847
languageName: node
linkType: hard
"two-sum@npm:^1.0.0":
version: 1.0.0
resolution: "two-sum@npm:1.0.0"
checksum: 2c6a995b555233b989f473a5d039bd237d75f4824b9b54dc9d9ab28157f3e412b37156acbb48b322c817a26f3cc85e3da281c9aed4b06e892d2d27ae88db7d32
languageName: node
linkType: hard
"type-check@npm:~0.3.2": "type-check@npm:~0.3.2":
version: 0.3.2 version: 0.3.2
resolution: "type-check@npm:0.3.2" resolution: "type-check@npm:0.3.2"
@@ -7912,16 +7247,6 @@ typescript@^4.1.3:
languageName: node languageName: node
linkType: hard linkType: hard
"unset-value@npm:^0.1.1":
version: 0.1.2
resolution: "unset-value@npm:0.1.2"
dependencies:
has-value: ^0.3.1
isobject: ^3.0.0
checksum: 56c7de1ee6b726002cc67b82954ec31b795836c2312d4d3d114a500eab5f632e1d3d6f5a164aff1ed90d7ffa94a009c452f6357f1f7d23bc444d489f622aeb9d
languageName: node
linkType: hard
"unset-value@npm:^1.0.0": "unset-value@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "unset-value@npm:1.0.0" resolution: "unset-value@npm:1.0.0"
@@ -7985,13 +7310,6 @@ typescript@^4.1.3:
languageName: node languageName: node
linkType: hard linkType: hard
"utf8@npm:^2.1.2":
version: 2.1.2
resolution: "utf8@npm:2.1.2"
checksum: de5d18adb219cae7871e1c105249e2fc7e6cae0e01c2b4c2eb6b099851b3bf62d1db6be6d83b5e4dea09036f8d16dd7222ad46eb326b38940a988e86743c1a61
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
version: 1.0.2 version: 1.0.2
resolution: "util-deprecate@npm:1.0.2" resolution: "util-deprecate@npm:1.0.2"
@@ -8090,13 +7408,6 @@ typescript@^4.1.3:
languageName: node languageName: node
linkType: hard linkType: hard
"web-worker-manager@npm:^0.2.0":
version: 0.2.0
resolution: "web-worker-manager@npm:0.2.0"
checksum: 7a0595e92f80320d51cc4815e885f507faef1744c2b3e7675813e08aeeb807e1ca873457a79425333faa6b0bafc07bd4a97ebacd45135e7cd18934993d2e1386
languageName: node
linkType: hard
"webidl-conversions@npm:^5.0.0": "webidl-conversions@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "webidl-conversions@npm:5.0.0" resolution: "webidl-conversions@npm:5.0.0"
@@ -8290,15 +7601,6 @@ typescript@^4.1.3:
languageName: node languageName: node
linkType: hard linkType: hard
"xml-lexer@npm:^0.2.2":
version: 0.2.2
resolution: "xml-lexer@npm:0.2.2"
dependencies:
eventemitter3: ^2.0.0
checksum: ec9d3f8cbc61ed93b7fc1052d05b23cfe5bfe0064a1146f89bc3a9cfbb0c80c6c40d795cc253b745cdbba0607271d14fa57d496a7754fe350a06a8fceae23359
languageName: node
linkType: hard
"xml-name-validator@npm:^3.0.0": "xml-name-validator@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "xml-name-validator@npm:3.0.0" resolution: "xml-name-validator@npm:3.0.0"
@@ -8306,16 +7608,6 @@ typescript@^4.1.3:
languageName: node languageName: node
linkType: hard linkType: hard
"xml-reader@npm:2.4.3":
version: 2.4.3
resolution: "xml-reader@npm:2.4.3"
dependencies:
eventemitter3: ^2.0.0
xml-lexer: ^0.2.2
checksum: d4b4ca6eb2d61c17d2df2be73dd82a393ae88a4cd10c5152f9908bf3e3bafa5562ce4b63df31ef198bc9a7c8447d4c277c98622438da5b32abf55c2f15984bfa
languageName: node
linkType: hard
"xmlchars@npm:^2.2.0": "xmlchars@npm:^2.2.0":
version: 2.2.0 version: 2.2.0
resolution: "xmlchars@npm:2.2.0" resolution: "xmlchars@npm:2.2.0"