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
COPY src ./src
COPY typings ./typings
COPY web ./web
COPY tests ./tests
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 add/remove playlists
- 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
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
docker run \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
-p 4534:4534 \
--network host \
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
```bash
docker run \
-e BONOB_PORT=3000 \
-e BONOB_SONOS_SEED_HOST=192.168.1.123 \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
-e BONOB_SONOS_SEED_HOST=192.168.1.123 \
-p 3000:3000 \
simojenki/bonob
```
@@ -117,9 +113,9 @@ services:
# ip address of your machine running bonob
BONOB_URL: http://192.168.1.111:4534
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
BONOB_SONOS_SEED_HOST: 192.168.1.121
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
- Startup bonob with your new implementation.
## Credits
- Icons courtesy of: ![Navidrome](https://www.navidrome.org/), ![Vectornator](https://www.vectornator.io/), and @jicho
## TODO
- Artist Radio

View File

@@ -20,7 +20,6 @@
"fp-ts": "^2.9.5",
"morgan": "^1.10.0",
"node-html-parser": "^2.1.0",
"scale-that-svg": "^1.0.5",
"sharp": "^0.27.2",
"soap": "^0.37.0",
"ts-md5": "^1.2.7",
@@ -37,7 +36,6 @@
"@types/supertest": "^2.0.10",
"chai": "^4.2.0",
"get-port": "^5.1.1",
"image-js": "^0.32.0",
"jest": "^26.6.3",
"nodemon": "^2.0.7",
"supertest": "^6.1.3",
@@ -50,8 +48,7 @@
"scripts": {
"clean": "rm -Rf build",
"build": "tsc",
"dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=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",
"dev": "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",
"test": "jest --testPathIgnorePatterns=build"
}

View File

@@ -88,7 +88,7 @@ const translations: Record<LANG, Record<KEY, string>> = {
expectedConfig: "Verwachte configuratie",
existingServiceConfig: "Bestaande serviceconfiguratie",
noExistingServiceRegistration: "Geen bestaande serviceregistratie",
register: "Registreren",
register: "Register",
removeRegistration: "Verwijder registratie",
devices: "Apparaten",
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 asLANGs = (acceptLanguageHeader: string | undefined) =>
@@ -141,8 +134,8 @@ export const keys = (lang: LANG = "en-US") => Object.keys(translations[lang]);
export default (serviceName: string): I8N =>
(...langs: string[]): Lang => {
const langToUse =
langs.map((l) => translationsLookup.get(l as LANG)).find((it) => it) ||
const langToUse =
langs.map((l) => translations[l as LANG]).find((it) => it) ||
translations["en-US"];
return (key: KEY) => {
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 AlbumQueryType = 'alphabeticalByArtist' | 'alphabeticalByName' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred';
export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred';
export type AlbumQuery = Paging & {
type: AlbumQueryType;

View File

@@ -367,7 +367,7 @@ export class Navidrome implements MusicService {
)
.then((json) => json["subsonic-response"])
.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;
});

View File

@@ -2,10 +2,6 @@ import { option as O } from "fp-ts";
import express, { Express, Request } from "express";
import * as Eta from "eta";
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";
@@ -17,8 +13,7 @@ import {
SONOS_RECOMMENDED_IMAGE_SIZES,
LOGIN_ROUTE,
CREATE_REGISTRATION_ROUTE,
REMOVE_REGISTRATION_ROUTE,
ICON,
REMOVE_REGISTRATION_ROUTE
} from "./smapi";
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
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";
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 {
range: (length: number) => string;
}
@@ -112,12 +87,7 @@ function server(
app.set("view engine", "eta");
app.set("views", "./web/views");
const langFor = (req: Request) => {
logger.debug(
`${req.path} (req[accept-language]=${req.headers["accept-language"]})`
);
return i8n(...asLANGs(req.headers["accept-language"]));
};
const langFor = (req: Request) => i8n(...asLANGs(req.headers["accept-language"]))
app.get("/", (req, res) => {
const lang = langFor(req);
@@ -132,12 +102,8 @@ function server(
services,
bonobService: service,
registeredBonobService,
createRegistrationRoute: bonobUrl
.append({ pathname: CREATE_REGISTRATION_ROUTE })
.pathname(),
removeRegistrationRoute: bonobUrl
.append({ pathname: REMOVE_REGISTRATION_ROUTE })
.pathname(),
createRegistrationRoute: bonobUrl.append({ pathname: CREATE_REGISTRATION_ROUTE }).pathname(),
removeRegistrationRoute: bonobUrl.append({ pathname: REMOVE_REGISTRATION_ROUTE }).pathname(),
});
}
);
@@ -147,8 +113,8 @@ function server(
return res.send({
service: {
name: service.name,
sid: service.sid,
},
sid: service.sid
}
});
});
@@ -218,19 +184,15 @@ function server(
res.status(403).render("failure", {
lang,
message: lang("loginFailed"),
cause: authResult.message,
cause: authResult.message
});
}
}
});
app.get(STRINGS_ROUTE, (_, res) => {
const stringNode = (id: string, 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 stringNode = (id: string, 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>`
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
<stringtables xmlns="http://sonos.com/sonosapi">
@@ -246,23 +208,12 @@ function server(
<Match>
<imageSizeMap>
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
(size) =>
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
).join("")}
(size) =>
`<sizeEntry size="${size}" substitution="/art/size/${size}"/>`
).join("")}
</imageSizeMap>
</Match>
</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">
<Match>
<SearchCategories>
@@ -301,8 +252,7 @@ function server(
)
.then(({ musicLibrary, stream }) => {
logger.info(
`stream response from music service for ${id}, status=${
stream.status
`stream response from music service for ${id}, status=${stream.status
}, headers=(${JSON.stringify(stream.headers)})`
);
@@ -375,72 +325,42 @@ function server(
}
});
app.get("/icon/:type/size/:size", (req, res) => {
const type = req.params["type"]!;
const size = req.params["size"]!;
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("/stream/artistRadio/:id", async (req, res) => {
const id = req.params["id"]!;
console.log(`----------> Streaming artist radio!! ${id}`)
res.status(404).send()
});
app.get("/art/:type/:id/size/:size", (req, res) => {
app.get("/:type/:id/art/size/:size", (req, res) => {
const authToken = accessTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
);
const type = req.params["type"]!;
const id = req.params["id"]!;
const size = req.params["size"]!;
const size = Number.parseInt(req.params["size"]!);
if (!authToken) {
return res.status(401).send();
} else if (type != "artist" && type != "album") {
return res.status(400).send();
} else if (!(size.match(/^\d+$/) && Number.parseInt(size) > 0)) {
return res.status(400).send();
} else {
return musicService
.login(authToken)
.then((it) => it.coverArt(id, type, Number.parseInt(size)))
.then((it) => it.coverArt(id, type, size))
.then((coverArt) => {
if (coverArt) {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
return res.send(coverArt.data);
res.send(coverArt.data);
} else {
return res.status(404).send();
res.status(404).send();
}
})
.catch((e: 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 { Clock } from "./clock";
import { URLBuilder } from "./url_builder";
import { asLANGs, I8N } from "./i8n";
import { I8N, LANG } from "./i8n";
export const LOGIN_ROUTE = "/login";
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
@@ -46,8 +46,6 @@ export const SONOS_RECOMMENDED_IMAGE_SIZES = [
"1500",
];
export type ICON = "artists" | "albums" | "playlists" | "genres" | "random" | "starred" | "recentlyAdded" | "recentlyPlayed" | "mostPlayed" | "discover"
const WSDL_FILE = path.resolve(
__dirname,
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
@@ -228,15 +226,12 @@ const playlist = (playlist: PlaylistSummary) => ({
});
export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) =>
bonobUrl.append({ pathname: `/art/album/${album.id}/size/180` });
export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON) =>
bonobUrl.append({ pathname: `/icon/${icon}/size/legacy` });
bonobUrl.append({ pathname: `/album/${album.id}/art/size/180` });
export const defaultArtistArtURI = (
bonobUrl: URLBuilder,
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) => ({
itemType: "album",
@@ -338,7 +333,7 @@ function bindSmapiSoapServiceToExpress(
i8n: I8N
) {
const sonosSoap = new SonosSoap(bonobUrl, linkCodes);
const urlWithToken = (accessToken: string) =>
bonobUrl.append({
searchParams: {
@@ -390,13 +385,30 @@ function bindSmapiSoapServiceToExpress(
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
.then(async ({ musicLibrary, accessToken, typeId }) =>
musicLibrary.track(typeId!).then((it) => ({
getMediaMetadataResult: track(
urlWithToken(accessToken),
it
),
}))
.then(async ({ musicLibrary, accessToken, type, typeId }) => {
console.log(`!!! getMediaMetadata->${id}`)
switch (type) {
case "track": return musicLibrary.track(typeId!).then((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 (
{ id, term }: { id: string; term: string },
@@ -444,7 +456,7 @@ function bindSmapiSoapServiceToExpress(
index,
count,
}: // recursive,
{ id: string; index: number; count: number; recursive: boolean },
{ id: string; index: number; count: number; recursive: boolean },
_,
soapyHeaders: SoapyHeaders,
) =>
@@ -469,12 +481,18 @@ function bindSmapiSoapServiceToExpress(
relatedBrowse:
artist.similarArtists.filter(it => it.inLibrary).length > 0
? [
{
id: `relatedArtists:${artist.id}`,
type: "RELATED_ARTISTS",
},
]
{
id: `relatedArtists:${artist.id}`,
type: "RELATED_ARTISTS",
},
]
: [],
relatedPlay: {
id: `artistRadio:${artist.id}`,
itemType: "stream",
title: "Foobar radio",
canPlay: true
}
},
};
});
@@ -532,7 +550,7 @@ function bindSmapiSoapServiceToExpress(
index,
count,
}: // recursive,
{ id: string; index: number; count: number; recursive: boolean },
{ id: string; index: number; count: number; recursive: boolean },
_,
soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, 'headers'>
@@ -541,11 +559,10 @@ function bindSmapiSoapServiceToExpress(
.then(splitId(id))
.then(({ musicLibrary, accessToken, type, typeId }) => {
const paging = { _index: index, _count: count };
const acceptLanguage = headers["accept-language"];
const lang = i8n((headers["accept-language"] || "en-US") as LANG);
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> =>
musicLibrary.albums(q).then((result) => {
@@ -563,22 +580,19 @@ function bindSmapiSoapServiceToExpress(
return getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "artists",
title: lang("artists"),
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "albums",
title: lang("albums"),
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
},
{
itemType: "playlist",
id: "playlists",
title: lang("playlists"),
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: false,
userContent: true,
@@ -586,40 +600,34 @@ function bindSmapiSoapServiceToExpress(
},
},
{
itemType: "container",
id: "genres",
title: lang("genres"),
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "randomAlbums",
title: lang("random"),
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: lang("starred"),
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: lang("recentlyAdded"),
albumArtURI: iconArtURI(bonobUrl, "recentlyAdded").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: lang("recentlyPlayed"),
albumArtURI: iconArtURI(bonobUrl, "recentlyPlayed").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: lang("mostPlayed"),
albumArtURI: iconArtURI(bonobUrl, "mostPlayed").href(),
itemType: "albumList",
},
],
index: 0,
@@ -647,7 +655,7 @@ function bindSmapiSoapServiceToExpress(
});
case "albums": {
return albums({
type: "alphabeticalByName",
type: "alphabeticalByArtist",
...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 PRESENTATION_AND_STRINGS_VERSION = "21";
export const PRESENTATION_AND_STRINGS_VERSION = "20";
// NOTE: manifest requires https for the URL,
// otherwise you will get an error trying to register
@@ -20,6 +20,7 @@ export type Capability =
| "alFavorites"
| "ucPlaylists"
| "extendedMD"
| "radioExtendedMD"
| "contextHeaders"
| "authorizationHeader"
| "logging"
@@ -32,6 +33,7 @@ export const BONOB_CAPABILITIES: Capability[] = [
"ucPlaylists",
"extendedMD",
"logging",
"radioExtendedMD"
];
export type Device = {
@@ -162,7 +164,7 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
}
})
.catch((e) => {
logger.error(`Failed looking for sonos devices`, { cause: e });
logger.error(`Failed looking for sonos devices ${e}`);
return [];
});
};

View File

@@ -54,129 +54,79 @@ describe("i8n", () => {
describe("fetching translations", () => {
describe("with a single lang", () => {
describe("and 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"
);
});
describe("and there is no templating", () => {
it("should return the value", () => {
expect(i8n("foo")("en-US")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten");
});
});
describe("and the lang is represented", () => {
describe("and there is no templating", () => {
it("should return the value", () => {
expect(i8n("foo")("en-US")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten");
});
});
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("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", () => {
function itShouldReturn(serviceName: string, langs: string[], key: KEY, expected: string) {
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 the first lang is a match", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["en-US", "nl-NL"], "artists", "Artists");
itShouldReturn("foo", ["nl-NL", "en-US"], "artists", "Artiesten");
it("should return the value for the first lang", () => {
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", () => {
itShouldReturn("service123", ["en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
it("should return the value for the firt lang", () => {
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", () => {
itShouldReturn("foo", ["en-us", "nl-NL"], "artists", "Artists");
itShouldReturn("foo", ["nl-nl", "en-US"], "artists", "Artiesten");
it("should return the value for the first lang", () => {
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", () => {
itShouldReturn("service123", ["en-us", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl-nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
});
});
describe("and the first lang is a lang match without region", () => {
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")
it("should return the value for the firt lang", () => {
expect(i8n("service123")("something", "en-US", "nl-NL")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("something", "nl-NL", "en-US")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
});
});
describe("and no lang is a match", () => {
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", () => {
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,
SKA,
} from "./builders";
import _ from "underscore";
describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService();
@@ -211,7 +210,6 @@ describe("InMemoryMusicService", () => {
const artist3_album2 = anAlbum({ genre: POP });
const artist1 = anArtist({
name: "artist1",
albums: [
artist1_album1,
artist1_album2,
@@ -220,8 +218,8 @@ describe("InMemoryMusicService", () => {
artist1_album5,
],
});
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] });
const artist3 = anArtist({ name: "artist3", albums: [artist3_album1, artist3_album2] });
const artist2 = anArtist({ albums: [artist2_album1] });
const artist3 = anArtist({ albums: [artist3_album1, artist3_album2] });
const artistWithNoAlbums = anArtist({ albums: [] });
const allAlbums = [artist1, artist2, artist3, artistWithNoAlbums].flatMap(
@@ -267,48 +265,29 @@ describe("InMemoryMusicService", () => {
describe("fetching multiple albums", () => {
describe("with no filtering", () => {
describe("fetching all on one page", () => {
describe("alphabeticalByArtist", () => {
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: totalAlbumCount,
});
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
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", () => {

View File

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

View File

@@ -447,7 +447,7 @@ describe("Navidrome", () => {
});
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) => navidrome.login(it.authToken))
.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 xml = similarSongsXml([]);
console.log(`xml = ${xml}`)
mockGET
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
.mockImplementationOnce(() =>
@@ -3742,7 +3743,7 @@ describe("Navidrome", () => {
});
});
describe("when the id doesnt exist", () => {
describe("when there id doesnt exist", () => {
it("should fail", async () => {
const id = "idThatHasAnError";
@@ -3756,7 +3757,7 @@ describe("Navidrome", () => {
.generateToken({ username, password })
.then((it) => it as AuthSuccess)
.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]!;
});
console.log(`posting to action ${action}`);
return request(this.server)
.post(action)
.type("form")
@@ -243,7 +245,7 @@ describe("scenarios", () => {
...BLONDIE.albums,
...BOB_MARLEY.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 dayjs from "dayjs";
import request from "supertest";
import Image from "image-js";
import { MusicService } from "../src/music_service";
import makeServer, {
BONOB_ACCESS_TOKEN_HEADER,
ICONS,
RangeBytesFromFilter,
rangeFilterFor,
} from "../src/server";
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
import { aDevice, aService } from "./builders";
@@ -21,9 +17,6 @@ import { Response } from "express";
import { Transform } from "stream";
import url from "../src/url_builder";
import i8n, { randomLang } from "../src/i8n";
import {
SONOS_RECOMMENDED_IMAGE_SIZES,
} from "../src/smapi";
describe("rangeFilterFor", () => {
describe("invalid range header string", () => {
@@ -285,16 +278,10 @@ describe("server", () => {
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(200);
expect(res.text).toMatch(
`<input type="submit" value="${lang("register")}">`
);
expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
expect(res.text).toMatch(
`<h3>${lang("noExistingServiceRegistration")}</h3>`
);
expect(res.text).not.toMatch(
`<input type="submit" value="${lang("removeRegistration")}">`
);
expect(res.text).toMatch(`<h3>${lang("noExistingServiceRegistration")}</h3>`);
expect(res.text).not.toMatch(`<input type="submit" value="${lang("removeRegistration")}">`);
});
});
});
@@ -330,16 +317,10 @@ describe("server", () => {
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(200);
expect(res.text).toMatch(
`<input type="submit" value="${lang("register")}">`
);
expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
expect(res.text).toMatch(
`<h3>${lang("existingServiceConfig")}</h3>`
);
expect(res.text).toMatch(
`<input type="submit" value="${lang("removeRegistration")}">`
);
expect(res.text).toMatch(`<h3>${lang("existingServiceConfig")}</h3>`);
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 () => {
const res = await request(server)
.get(bonobUrl.append({ pathname: "/about" }).path())
.send();
.get(bonobUrl.append({ pathname: "/about" }).path())
.send();
expect(res.status).toEqual(200);
expect(res.body).toEqual({
service: {
name: theService.name,
sid: theService.sid,
},
});
sid: theService.sid
}});
});
});
@@ -395,21 +375,21 @@ describe("server", () => {
describe("when is successful", () => {
it("should return a nice message", async () => {
sonos.register.mockResolvedValue(true);
const res = await request(server)
.post(bonobUrl.append({ pathname: "/registration/add" }).path())
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(200);
expect(res.text).toMatch(`<title>${lang("success")}</title>`);
expect(res.text).toMatch(lang("successfullyRegistered"));
expect(sonos.register.mock.calls.length).toEqual(1);
expect(sonos.register.mock.calls[0][0]).toBe(theService);
});
});
describe("when is unsuccessful", () => {
it("should return a failure message", async () => {
sonos.register.mockResolvedValue(false);
@@ -418,11 +398,11 @@ describe("server", () => {
.post(bonobUrl.append({ pathname: "/registration/add" }).path())
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(500);
expect(res.text).toMatch(`<title>${lang("failure")}</title>`);
expect(res.text).toMatch(lang("registrationFailed"));
expect(sonos.register.mock.calls.length).toEqual(1);
expect(sonos.register.mock.calls[0][0]).toBe(theService);
});
@@ -433,38 +413,34 @@ describe("server", () => {
describe("when is successful", () => {
it("should return a nice message", async () => {
sonos.remove.mockResolvedValue(true);
const res = await request(server)
.post(
bonobUrl.append({ pathname: "/registration/remove" }).path()
)
.post(bonobUrl.append({ pathname: "/registration/remove" }).path())
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(200);
expect(res.text).toMatch(`<title>${lang("success")}</title>`);
expect(res.text).toMatch(lang("successfullyRemovedRegistration"));
expect(sonos.remove.mock.calls.length).toEqual(1);
expect(sonos.remove.mock.calls[0][0]).toBe(theService.sid);
});
});
describe("when is unsuccessful", () => {
it("should return a failure message", async () => {
sonos.remove.mockResolvedValue(false);
const res = await request(server)
.post(
bonobUrl.append({ pathname: "/registration/remove" }).path()
)
.post(bonobUrl.append({ pathname: "/registration/remove" }).path())
.set("accept-language", acceptLanguage)
.send();
expect(res.status).toEqual(500);
expect(res.text).toMatch(`<title>${lang("failure")}</title>`);
expect(res.text).toMatch(lang("failedToRemoveRegistration"));
expect(sonos.remove.mock.calls.length).toEqual(1);
expect(sonos.remove.mock.calls[0][0]).toBe(theService.sid);
});
@@ -478,7 +454,7 @@ describe("server", () => {
remove: jest.fn(),
};
const theService = aService({
name: serviceNameForLang,
name: serviceNameForLang
});
const musicService = {
@@ -498,6 +474,7 @@ describe("server", () => {
const clock = {
now: jest.fn(),
};
const server = makeServer(
sonos as unknown as Sonos,
@@ -519,18 +496,10 @@ describe("server", () => {
expect(res.status).toEqual(200);
expect(res.text).toMatch(`<title>${lang("login")}</title>`);
expect(res.text).toMatch(
`<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(
`<input type="submit" value="${lang("login")}" id="submit">`
);
expect(res.text).toMatch(`<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(`<input type="submit" value="${lang("login")}" id="submit">`);
});
describe("when the credentials are valid", () => {
@@ -609,7 +578,7 @@ describe("server", () => {
expect(res.text).toContain(lang("invalidLinkCode"));
});
});
});
});
describe("/stream", () => {
const musicService = {
@@ -1042,7 +1011,7 @@ describe("server", () => {
});
});
describe("/art", () => {
describe("art", () => {
const musicService = {
login: jest.fn(),
};
@@ -1071,7 +1040,7 @@ describe("server", () => {
describe("when there is no access-token", () => {
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);
});
@@ -1082,7 +1051,7 @@ describe("server", () => {
now = now.add(1, "day");
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);
@@ -1094,7 +1063,7 @@ describe("server", () => {
it("should return a 400", async () => {
const res = await request(server)
.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);
@@ -1103,21 +1072,6 @@ describe("server", () => {
});
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", () => {
it("should return the image and a 200", async () => {
const coverArt = {
@@ -1132,7 +1086,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -1158,7 +1112,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -1174,7 +1128,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -1184,21 +1138,6 @@ describe("server", () => {
});
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", () => {
it("should return the image and a 200", async () => {
const coverArt = {
@@ -1212,7 +1151,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -1237,7 +1176,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -1252,7 +1191,7 @@ describe("server", () => {
const res = await request(server)
.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);
@@ -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,
defaultArtistArtURI,
searchResult,
iconArtURI,
} from "../src/smapi";
import {
@@ -55,109 +54,83 @@ describe("service config", () => {
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
[bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => {
describe(bonobUrl.href(), () => {
const server = makeServer(
SONOS_DISABLED,
aService({ name: "music land" }),
bonobUrl,
new InMemoryMusicService()
);
const server = makeServer(
SONOS_DISABLED,
aService({ name: "music land" }),
bonobUrl,
new InMemoryMusicService()
);
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
const presentationUrl = bonobUrl.append({
pathname: PRESENTATION_MAP_ROUTE,
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
const presentationUrl = bonobUrl.append({
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, () => {
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"
);
});
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();
});
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, () => {
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send();
describe(`${presentationUrl}`, () => {
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
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
const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
// 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="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
});
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/art/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,
albumArtist: someTrack.artist.name,
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,
artistId: someTrack.artist.id,
duration: someTrack.duration,
@@ -308,7 +281,7 @@ describe("defaultAlbumArtURI", () => {
const album = anAlbum();
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();
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) => {
expect(e.root.Envelope.Body.Fault).toEqual({
faultcode: "Client.NOT_LINKED_RETRY",
faultstring:
"Link Code not found yet, sonos app will keep polling until you log in to bonob",
faultstring: "Link Code not found yet, sonos app will keep polling until you log in to bonob",
detail: {
ExceptionInfo: "NOT_LINKED_RETRY",
SonosError: "5",
@@ -735,72 +707,46 @@ describe("api", () => {
getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "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",
title: "Playlists",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: "false",
renameable: "false",
userContent: "true",
},
},
{ itemType: "container", id: "genres", title: "Genres" },
{
id: "genres",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "randomAlbums",
title: "Random",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: "Starred",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: "Recently added",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: "Recently played",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: "Most played",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
},
],
index: 0,
@@ -812,7 +758,7 @@ describe("api", () => {
describe("when an accept-language header is present with value nl-NL", () => {
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({
id: "root",
index: 0,
@@ -822,72 +768,46 @@ describe("api", () => {
getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "artists",
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",
title: "Afspeellijsten",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: "false",
renameable: "false",
userContent: "true",
},
},
{ itemType: "container", id: "genres", title: "Genres" },
{
id: "genres",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "randomAlbums",
title: "Willekeurig",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: "Favorieten",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: "Onlangs toegevoegd",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: "Onlangs afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: "Meest afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
},
],
index: 0,
@@ -1656,7 +1576,7 @@ describe("api", () => {
);
expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByName",
type: "alphabeticalByArtist",
_index: paging.index,
_count: paging.count,
});
@@ -1702,7 +1622,7 @@ describe("api", () => {
);
expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByName",
type: "alphabeticalByArtist",
_index: paging.index,
_count: paging.count,
});

View File

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

View File

@@ -4,11 +4,9 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"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'. */,
"lib": [
"es2019"
] /* Specify library files to be included in the compilation. */,
"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'. */
"lib": ["es2019"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "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. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"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. */,
"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. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "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'). */
/* 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. */,
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "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. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "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'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./typings",
"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. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* 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. */
"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. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -70,7 +64,7 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"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
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":
version: 7.14.5
resolution: "@babel/template@npm:7.14.5"
@@ -713,15 +704,6 @@ __metadata:
languageName: node
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":
version: 1.1.2
resolution: "@szmarczak/http-timer@npm:1.1.2"
@@ -932,13 +914,6 @@ __metadata:
languageName: node
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":
version: 2.3.0
resolution: "@types/prettier@npm:2.3.0"
@@ -1522,13 +1497,6 @@ __metadata:
languageName: node
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":
version: 1.19.0
resolution: "body-parser@npm:1.19.0"
@@ -1569,12 +1537,10 @@ __metadata:
express: ^4.17.1
fp-ts: ^2.9.5
get-port: ^5.1.1
image-js: ^0.32.0
jest: ^26.6.3
morgan: ^1.10.0
node-html-parser: ^2.1.0
nodemon: ^2.0.7
scale-that-svg: ^1.0.5
sharp: ^0.27.2
soap: ^0.37.0
supertest: ^6.1.3
@@ -1804,13 +1770,6 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "capture-exit@npm:2.0.0"
@@ -1945,17 +1904,6 @@ __metadata:
languageName: node
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":
version: 2.2.0
resolution: "clean-stack@npm:2.2.0"
@@ -2412,16 +2360,6 @@ __metadata:
languageName: node
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":
version: 4.2.2
resolution: "deepmerge@npm:4.2.2"
@@ -2578,13 +2516,6 @@ __metadata:
languageName: node
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":
version: 0.7.2
resolution: "emittery@npm:0.7.2"
@@ -2753,13 +2684,6 @@ __metadata:
languageName: node
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":
version: 0.3.6
resolution: "exec-sh@npm:0.3.6"
@@ -2936,15 +2860,6 @@ __metadata:
languageName: node
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":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@@ -2952,16 +2867,6 @@ __metadata:
languageName: node
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":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
@@ -2976,24 +2881,6 @@ __metadata:
languageName: node
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":
version: 2.0.7
resolution: "fast-safe-stringify@npm:2.0.7"
@@ -3026,20 +2913,6 @@ __metadata:
languageName: node
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":
version: 4.0.0
resolution: "fill-range@npm:4.0.0"
@@ -3451,13 +3324,6 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "has-symbols@npm:1.0.2"
@@ -3696,48 +3562,6 @@ __metadata:
languageName: node
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":
version: 2.1.0
resolution: "import-lazy@npm:2.1.0"
@@ -3816,29 +3640,6 @@ __metadata:
languageName: node
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":
version: 1.1.5
resolution: "ip@npm:1.1.5"
@@ -3871,20 +3672,6 @@ __metadata:
languageName: node
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":
version: 0.2.1
resolution: "is-arrayish@npm:0.2.1"
@@ -4007,13 +3794,6 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "is-fullwidth-code-point@npm:1.0.0"
@@ -4063,15 +3843,6 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "is-lambda@npm:1.0.1"
@@ -4116,7 +3887,7 @@ __metadata:
languageName: node
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
resolution: "is-plain-object@npm:2.0.4"
dependencies:
@@ -4711,27 +4482,6 @@ __metadata:
languageName: node
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":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
@@ -4962,27 +4712,6 @@ __metadata:
languageName: node
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":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@@ -5097,13 +4826,6 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "merge-descriptors@npm:1.0.1"
@@ -5330,240 +5052,6 @@ __metadata:
languageName: node
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":
version: 1.10.0
resolution: "morgan@npm:1.10.0"
@@ -5645,20 +5133,6 @@ __metadata:
languageName: node
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":
version: 1.0.5
resolution: "nice-try@npm:1.0.5"
@@ -5926,16 +5400,6 @@ __metadata:
languageName: node
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":
version: 2.3.0
resolution: "on-finished@npm:2.3.0"
@@ -6060,13 +5524,6 @@ __metadata:
languageName: node
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":
version: 5.2.0
resolution: "parse-json@npm:5.2.0"
@@ -6442,13 +5899,6 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "regex-not@npm:1.0.2"
@@ -6484,13 +5934,6 @@ __metadata:
languageName: node
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":
version: 1.1.4
resolution: "repeat-element@npm:1.1.4"
@@ -6624,51 +6067,6 @@ __metadata:
languageName: node
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":
version: 4.8.5
resolution: "rsvp@npm:4.8.5"
@@ -6741,17 +6139,6 @@ __metadata:
languageName: node
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":
version: 3.1.1
resolution: "semver-diff@npm:3.1.1"
@@ -7428,25 +6815,6 @@ __metadata:
languageName: node
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":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
@@ -7535,25 +6903,6 @@ __metadata:
languageName: node
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":
version: 1.0.4
resolution: "tmpl@npm:1.0.4"
@@ -7746,20 +7095,6 @@ __metadata:
languageName: node
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":
version: 0.3.2
resolution: "type-check@npm:0.3.2"
@@ -7912,16 +7247,6 @@ typescript@^4.1.3:
languageName: node
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":
version: 1.0.0
resolution: "unset-value@npm:1.0.0"
@@ -7985,13 +7310,6 @@ typescript@^4.1.3:
languageName: node
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":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@@ -8090,13 +7408,6 @@ typescript@^4.1.3:
languageName: node
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":
version: 5.0.0
resolution: "webidl-conversions@npm:5.0.0"
@@ -8290,15 +7601,6 @@ typescript@^4.1.3:
languageName: node
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":
version: 3.0.0
resolution: "xml-name-validator@npm:3.0.0"
@@ -8306,16 +7608,6 @@ typescript@^4.1.3:
languageName: node
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":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"