mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to cache subsonic artist images locally on disk (#61)
This commit is contained in:
@@ -150,6 +150,7 @@ BNB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
|||||||
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||||
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
||||||
BNB_SUBSONIC_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom subsonic clients when streaming. ie. "audio/flac,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs.
|
BNB_SUBSONIC_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom subsonic clients when streaming. ie. "audio/flac,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs.
|
||||||
|
BNB_SUBSONIC_ARTIST_IMAGE_CACHE | undefined | Path for caching of artist images as are sourced externally. ie. Navidrome provides spotify URLs
|
||||||
BNB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
|
BNB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
|
||||||
BNB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
|
BNB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
|
||||||
BNB_ICON_FOREGROUND_COLOR | undefined | Icon foreground color in sonos app, must be a valid [svg color](https://www.december.com/html/spec/colorsvg.html)
|
BNB_ICON_FOREGROUND_COLOR | undefined | Icon foreground color in sonos app, must be a valid [svg color](https://www.december.com/html/spec/colorsvg.html)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svrooij/sonos": "^2.4.0",
|
"@svrooij/sonos": "^2.4.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/sharp": "^0.28.6",
|
"@types/sharp": "^0.28.6",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"eta": "^1.12.3",
|
"eta": "^1.12.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fp-ts": "^2.11.1",
|
"fp-ts": "^2.11.1",
|
||||||
|
"fs-extra": "^10.0.0",
|
||||||
"libxmljs2": "^0.28.0",
|
"libxmljs2": "^0.28.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-html-parser": "^4.1.4",
|
"node-html-parser": "^4.1.4",
|
||||||
@@ -35,12 +37,14 @@
|
|||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@types/tmp": "^0.2.1",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
"image-js": "^0.33.0",
|
"image-js": "^0.33.0",
|
||||||
"jest": "^27.1.0",
|
"jest": "^27.1.0",
|
||||||
"nodemon": "^2.0.12",
|
"nodemon": "^2.0.12",
|
||||||
"supertest": "^6.1.6",
|
"supertest": "^6.1.6",
|
||||||
|
"tmp": "^0.2.1",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^27.0.5",
|
||||||
"ts-mockito": "^2.6.1",
|
"ts-mockito": "^2.6.1",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
|
|||||||
35
src/app.ts
35
src/app.ts
@@ -2,7 +2,13 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import server from "./server";
|
import server from "./server";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appendMimeTypeToClientFor, DEFAULT, Subsonic } from "./subsonic";
|
import {
|
||||||
|
appendMimeTypeToClientFor,
|
||||||
|
axiosImageFetcher,
|
||||||
|
cachingImageFetcher,
|
||||||
|
DEFAULT,
|
||||||
|
Subsonic,
|
||||||
|
} from "./subsonic";
|
||||||
import encryption from "./encryption";
|
import encryption from "./encryption";
|
||||||
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
|
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
|
||||||
import { InMemoryLinkCodes } from "./link_codes";
|
import { InMemoryLinkCodes } from "./link_codes";
|
||||||
@@ -28,10 +34,15 @@ const streamUserAgent = config.subsonic.customClientsFor
|
|||||||
? appendMimeTypeToClientFor(config.subsonic.customClientsFor.split(","))
|
? appendMimeTypeToClientFor(config.subsonic.customClientsFor.split(","))
|
||||||
: DEFAULT;
|
: DEFAULT;
|
||||||
|
|
||||||
|
const artistImageFetcher = config.subsonic.artistImageCache
|
||||||
|
? cachingImageFetcher(config.subsonic.artistImageCache, axiosImageFetcher)
|
||||||
|
: axiosImageFetcher;
|
||||||
|
|
||||||
const subsonic = new Subsonic(
|
const subsonic = new Subsonic(
|
||||||
config.subsonic.url,
|
config.subsonic.url,
|
||||||
encryption(config.secret),
|
encryption(config.secret),
|
||||||
streamUserAgent
|
streamUserAgent,
|
||||||
|
artistImageFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const featureFlagAwareMusicService: MusicService = {
|
const featureFlagAwareMusicService: MusicService = {
|
||||||
@@ -60,7 +71,9 @@ const featureFlagAwareMusicService: MusicService = {
|
|||||||
|
|
||||||
export const GIT_INFO = path.join(__dirname, "..", ".gitinfo");
|
export const GIT_INFO = path.join(__dirname, "..", ".gitinfo");
|
||||||
|
|
||||||
const version = fs.existsSync(GIT_INFO) ? fs.readFileSync(GIT_INFO).toString().trim() : "v??"
|
const version = fs.existsSync(GIT_INFO)
|
||||||
|
? fs.readFileSync(GIT_INFO).toString().trim()
|
||||||
|
: "v??";
|
||||||
|
|
||||||
const app = server(
|
const app = server(
|
||||||
sonosSystem,
|
sonosSystem,
|
||||||
@@ -69,12 +82,12 @@ const app = server(
|
|||||||
featureFlagAwareMusicService,
|
featureFlagAwareMusicService,
|
||||||
{
|
{
|
||||||
linkCodes: () => new InMemoryLinkCodes(),
|
linkCodes: () => new InMemoryLinkCodes(),
|
||||||
accessTokens: () => new InMemoryAccessTokens(sha256(config.secret)),
|
accessTokens: () => new InMemoryAccessTokens(sha256(config.secret)),
|
||||||
clock: SystemClock,
|
clock: SystemClock,
|
||||||
iconColors: config.icons,
|
iconColors: config.icons,
|
||||||
applyContextPath: true,
|
applyContextPath: true,
|
||||||
logRequests: true,
|
logRequests: true,
|
||||||
version
|
version,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -90,12 +103,12 @@ if (config.sonos.autoRegister) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if(config.sonos.discovery.enabled) {
|
} else if (config.sonos.discovery.enabled) {
|
||||||
sonosSystem.devices().then(devices => {
|
sonosSystem.devices().then((devices) => {
|
||||||
devices.forEach(d => {
|
devices.forEach((d) => {
|
||||||
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`)
|
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export default function () {
|
|||||||
subsonic: {
|
subsonic: {
|
||||||
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
|
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
|
||||||
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
||||||
|
artistImageCache: bnbEnvVar("SUBSONIC_ARTIST_IMAGE_CACHE"),
|
||||||
},
|
},
|
||||||
scrobbleTracks: bnbEnvVar("SCROBBLE_TRACKS", { default: "true" }) == "true",
|
scrobbleTracks: bnbEnvVar("SCROBBLE_TRACKS", { default: "true" }) == "true",
|
||||||
reportNowPlaying:
|
reportNowPlaying:
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ import {
|
|||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
Genre,
|
Genre,
|
||||||
Track,
|
Track,
|
||||||
|
CoverArt,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
import X2JS from "x2js";
|
import X2JS from "x2js";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
import axios, { AxiosRequestConfig } from "axios";
|
import axios, { AxiosRequestConfig } from "axios";
|
||||||
import { Encryption } from "./encryption";
|
import { Encryption } from "./encryption";
|
||||||
@@ -311,19 +314,61 @@ export const asURLSearchParams = (q: any) => {
|
|||||||
return urlSearchParams;
|
return urlSearchParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
|
||||||
|
|
||||||
|
export const cachingImageFetcher =
|
||||||
|
(cacheDir: string, delegate: ImageFetcher) =>
|
||||||
|
(url: string): Promise<CoverArt | undefined> => {
|
||||||
|
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
||||||
|
return fse
|
||||||
|
.readFile(filename)
|
||||||
|
.then((data) => ({ contentType: "image/png", data }))
|
||||||
|
.catch(() =>
|
||||||
|
delegate(url).then((image) => {
|
||||||
|
if (image) {
|
||||||
|
return sharp(image.data)
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
.then((png) => {
|
||||||
|
return fse
|
||||||
|
.writeFile(filename, png)
|
||||||
|
.then(() => ({ contentType: "image/png", data: png }));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const axiosImageFetcher = (url: string): Promise<CoverArt | undefined> =>
|
||||||
|
axios
|
||||||
|
.get(url, {
|
||||||
|
headers: BROWSER_HEADERS,
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
})
|
||||||
|
.then((res) => ({
|
||||||
|
contentType: res.headers["content-type"],
|
||||||
|
data: Buffer.from(res.data, "binary"),
|
||||||
|
}))
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
export class Subsonic implements MusicService {
|
export class Subsonic implements MusicService {
|
||||||
url: string;
|
url: string;
|
||||||
encryption: Encryption;
|
encryption: Encryption;
|
||||||
streamClientApplication: StreamClientApplication;
|
streamClientApplication: StreamClientApplication;
|
||||||
|
externalImageFetcher: ImageFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
encryption: Encryption,
|
encryption: Encryption,
|
||||||
streamClientApplication: StreamClientApplication = DEFAULT
|
streamClientApplication: StreamClientApplication = DEFAULT,
|
||||||
|
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
||||||
) {
|
) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.encryption = encryption;
|
this.encryption = encryption;
|
||||||
this.streamClientApplication = streamClientApplication;
|
this.streamClientApplication = streamClientApplication;
|
||||||
|
this.externalImageFetcher = externalImageFetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
get = async (
|
get = async (
|
||||||
@@ -630,28 +675,21 @@ export class Subsonic implements MusicService {
|
|||||||
(it) => it.coverArt
|
(it) => it.coverArt
|
||||||
);
|
);
|
||||||
if (artist.image.large) {
|
if (artist.image.large) {
|
||||||
return axios
|
return this.externalImageFetcher(artist.image.large!).then(
|
||||||
.get(artist.image.large!, {
|
(image) => {
|
||||||
headers: BROWSER_HEADERS,
|
if (image && size) {
|
||||||
responseType: "arraybuffer",
|
return sharp(image.data)
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
const image = Buffer.from(res.data, "binary");
|
|
||||||
if (size) {
|
|
||||||
return sharp(image)
|
|
||||||
.resize(size)
|
.resize(size)
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
.then((resized) => ({
|
.then((resized) => ({
|
||||||
contentType: res.headers["content-type"],
|
contentType: image.contentType,
|
||||||
data: resized,
|
data: resized,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return {
|
return image;
|
||||||
contentType: res.headers["content-type"],
|
|
||||||
data: image,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} else if (albumsWithCoverArt.length > 0) {
|
} else if (albumsWithCoverArt.length > 0) {
|
||||||
return subsonic
|
return subsonic
|
||||||
.getCoverArt(
|
.getCoverArt(
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import { v4 as uuid } from "uuid";
|
|||||||
import { Credentials } from "../src/smapi";
|
import { Credentials } from "../src/smapi";
|
||||||
|
|
||||||
import { Service, Device } from "../src/sonos";
|
import { Service, Device } from "../src/sonos";
|
||||||
import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary, PlaylistSummary, Playlist } from "../src/music_service";
|
import {
|
||||||
|
Album,
|
||||||
|
Artist,
|
||||||
|
Track,
|
||||||
|
albumToAlbumSummary,
|
||||||
|
artistToArtistSummary,
|
||||||
|
PlaylistSummary,
|
||||||
|
Playlist,
|
||||||
|
} from "../src/music_service";
|
||||||
import randomString from "../src/random_string";
|
import randomString from "../src/random_string";
|
||||||
import { b64Encode } from "../src/b64";
|
import { b64Encode } from "../src/b64";
|
||||||
|
|
||||||
@@ -29,12 +37,14 @@ export const aService = (fields: Partial<Service> = {}): Service => ({
|
|||||||
...fields,
|
...fields,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function aPlaylistSummary(fields: Partial<PlaylistSummary> = {}): PlaylistSummary {
|
export function aPlaylistSummary(
|
||||||
|
fields: Partial<PlaylistSummary> = {}
|
||||||
|
): PlaylistSummary {
|
||||||
return {
|
return {
|
||||||
id: `playlist-${uuid()}`,
|
id: `playlist-${uuid()}`,
|
||||||
name: `playlistname-${randomString()}`,
|
name: `playlistname-${randomString()}`,
|
||||||
...fields
|
...fields,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aPlaylist(fields: Partial<Playlist> = {}): Playlist {
|
export function aPlaylist(fields: Partial<Playlist> = {}): Playlist {
|
||||||
@@ -42,8 +52,8 @@ export function aPlaylist(fields: Partial<Playlist> = {}): Playlist {
|
|||||||
id: `playlist-${uuid()}`,
|
id: `playlist-${uuid()}`,
|
||||||
name: `playlist-${randomString()}`,
|
name: `playlist-${randomString()}`,
|
||||||
entries: [aTrack(), aTrack()],
|
entries: [aTrack(), aTrack()],
|
||||||
...fields
|
...fields,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aDevice(fields: Partial<Device> = {}): Device {
|
export function aDevice(fields: Partial<Device> = {}): Device {
|
||||||
@@ -105,14 +115,14 @@ export function anArtist(fields: Partial<Artist> = {}): Artist {
|
|||||||
],
|
],
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
artist.albums.forEach(album => {
|
artist.albums.forEach((album) => {
|
||||||
album.artistId = artist.id;
|
album.artistId = artist.id;
|
||||||
album.artistName = artist.name;
|
album.artistName = artist.name;
|
||||||
})
|
});
|
||||||
return artist;
|
return artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aGenre = (name: string) => ({ id: b64Encode(name), name })
|
export const aGenre = (name: string) => ({ id: b64Encode(name), name });
|
||||||
|
|
||||||
export const HIP_HOP = aGenre("Hip-Hop");
|
export const HIP_HOP = aGenre("Hip-Hop");
|
||||||
export const METAL = aGenre("Metal");
|
export const METAL = aGenre("Metal");
|
||||||
@@ -125,7 +135,16 @@ export const SKA = aGenre("Ska");
|
|||||||
export const PUNK = aGenre("Punk");
|
export const PUNK = aGenre("Punk");
|
||||||
export const TRIP_HOP = aGenre("Trip Hop");
|
export const TRIP_HOP = aGenre("Trip Hop");
|
||||||
|
|
||||||
export const SAMPLE_GENRES = [HIP_HOP, METAL, NEW_WAVE, POP, POP_ROCK, REGGAE, ROCK, SKA];
|
export const SAMPLE_GENRES = [
|
||||||
|
HIP_HOP,
|
||||||
|
METAL,
|
||||||
|
NEW_WAVE,
|
||||||
|
POP,
|
||||||
|
POP_ROCK,
|
||||||
|
REGGAE,
|
||||||
|
ROCK,
|
||||||
|
SKA,
|
||||||
|
];
|
||||||
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];
|
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];
|
||||||
|
|
||||||
export function aTrack(fields: Partial<Track> = {}): Track {
|
export function aTrack(fields: Partial<Track> = {}): Track {
|
||||||
@@ -140,7 +159,9 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
|||||||
number: randomInt(100),
|
number: randomInt(100),
|
||||||
genre,
|
genre,
|
||||||
artist: artistToArtistSummary(artist),
|
artist: artistToArtistSummary(artist),
|
||||||
album: albumToAlbumSummary(anAlbum({ artistId: artist.id, artistName: artist.name, genre })),
|
album: albumToAlbumSummary(
|
||||||
|
anAlbum({ artistId: artist.id, artistName: artist.name, genre })
|
||||||
|
),
|
||||||
coverArt: `coverArt:${uuid()}`,
|
coverArt: `coverArt:${uuid()}`,
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
@@ -173,7 +194,7 @@ export const BLONDIE: Artist = {
|
|||||||
genre: NEW_WAVE,
|
genre: NEW_WAVE,
|
||||||
artistId: BLONDIE_ID,
|
artistId: BLONDIE_ID,
|
||||||
artistName: BLONDIE_NAME,
|
artistName: BLONDIE_NAME,
|
||||||
coverArt: `coverArt:${uuid()}`
|
coverArt: `coverArt:${uuid()}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@@ -182,7 +203,7 @@ export const BLONDIE: Artist = {
|
|||||||
genre: POP_ROCK,
|
genre: POP_ROCK,
|
||||||
artistId: BLONDIE_ID,
|
artistId: BLONDIE_ID,
|
||||||
artistName: BLONDIE_NAME,
|
artistName: BLONDIE_NAME,
|
||||||
coverArt: `coverArt:${uuid()}`
|
coverArt: `coverArt:${uuid()}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
@@ -199,9 +220,33 @@ export const BOB_MARLEY: Artist = {
|
|||||||
id: BOB_MARLEY_ID,
|
id: BOB_MARLEY_ID,
|
||||||
name: BOB_MARLEY_NAME,
|
name: BOB_MARLEY_NAME,
|
||||||
albums: [
|
albums: [
|
||||||
{ id: uuid(), name: "Burin'", year: "1973", genre: REGGAE, artistId: BOB_MARLEY_ID, artistName: BOB_MARLEY_NAME, coverArt: `coverArt:${uuid()}` },
|
{
|
||||||
{ id: uuid(), name: "Exodus", year: "1977", genre: REGGAE, artistId: BOB_MARLEY_ID, artistName: BOB_MARLEY_NAME, coverArt: `coverArt:${uuid()}` },
|
id: uuid(),
|
||||||
{ id: uuid(), name: "Kaya", year: "1978", genre: SKA, artistId: BOB_MARLEY_ID, artistName: BOB_MARLEY_NAME, coverArt: `coverArt:${uuid()}` },
|
name: "Burin'",
|
||||||
|
year: "1973",
|
||||||
|
genre: REGGAE,
|
||||||
|
artistId: BOB_MARLEY_ID,
|
||||||
|
artistName: BOB_MARLEY_NAME,
|
||||||
|
coverArt: `coverArt:${uuid()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: "Exodus",
|
||||||
|
year: "1977",
|
||||||
|
genre: REGGAE,
|
||||||
|
artistId: BOB_MARLEY_ID,
|
||||||
|
artistName: BOB_MARLEY_NAME,
|
||||||
|
coverArt: `coverArt:${uuid()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name: "Kaya",
|
||||||
|
year: "1978",
|
||||||
|
genre: SKA,
|
||||||
|
artistId: BOB_MARLEY_ID,
|
||||||
|
artistName: BOB_MARLEY_NAME,
|
||||||
|
coverArt: `coverArt:${uuid()}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
small: "http://localhost/BOB_MARLEY/sml",
|
small: "http://localhost/BOB_MARLEY/sml",
|
||||||
@@ -238,7 +283,7 @@ export const METALLICA: Artist = {
|
|||||||
genre: METAL,
|
genre: METAL,
|
||||||
artistId: METALLICA_ID,
|
artistId: METALLICA_ID,
|
||||||
artistName: METALLICA_NAME,
|
artistName: METALLICA_NAME,
|
||||||
coverArt: `coverArt:${uuid()}`
|
coverArt: `coverArt:${uuid()}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@@ -247,7 +292,7 @@ export const METALLICA: Artist = {
|
|||||||
genre: METAL,
|
genre: METAL,
|
||||||
artistId: METALLICA_ID,
|
artistId: METALLICA_ID,
|
||||||
artistName: METALLICA_NAME,
|
artistName: METALLICA_NAME,
|
||||||
coverArt: `coverArt:${uuid()}`
|
coverArt: `coverArt:${uuid()}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
@@ -261,3 +306,4 @@ export const METALLICA: Artist = {
|
|||||||
export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA];
|
export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA];
|
||||||
|
|
||||||
export const ALL_ALBUMS = ALL_ARTISTS.flatMap((it) => it.albums || []);
|
export const ALL_ALBUMS = ALL_ARTISTS.flatMap((it) => it.albums || []);
|
||||||
|
|
||||||
|
|||||||
@@ -66,11 +66,13 @@ describe("envVar", () => {
|
|||||||
|
|
||||||
describe("validationPattern", () => {
|
describe("validationPattern", () => {
|
||||||
it("should fail when the value does not match the pattern", () => {
|
it("should fail when the value does not match the pattern", () => {
|
||||||
expect(
|
expect(() =>
|
||||||
() => envVar("bnb-var", {
|
envVar("bnb-var", {
|
||||||
validationPattern: /^foobar$/,
|
validationPattern: /^foobar$/,
|
||||||
})
|
})
|
||||||
).toThrowError(`Invalid value specified for 'bnb-var', must match ${/^foobar$/}`)
|
).toThrowError(
|
||||||
|
`Invalid value specified for 'bnb-var', must match ${/^foobar$/}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -117,7 +119,7 @@ describe("config", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("bonobUrl", () => {
|
describe("bonobUrl", () => {
|
||||||
["BNB_URL", "BONOB_URL", "BONOB_WEB_ADDRESS"].forEach(key => {
|
["BNB_URL", "BONOB_URL", "BONOB_WEB_ADDRESS"].forEach((key) => {
|
||||||
describe(`when ${key} is specified`, () => {
|
describe(`when ${key} is specified`, () => {
|
||||||
it("should be used", () => {
|
it("should be used", () => {
|
||||||
const url = "http://bonob1.example.com:8877/";
|
const url = "http://bonob1.example.com:8877/";
|
||||||
@@ -163,69 +165,73 @@ describe("config", () => {
|
|||||||
|
|
||||||
describe("icons", () => {
|
describe("icons", () => {
|
||||||
describe("foregroundColor", () => {
|
describe("foregroundColor", () => {
|
||||||
["BNB_ICON_FOREGROUND_COLOR", "BONOB_ICON_FOREGROUND_COLOR"].forEach(k => {
|
["BNB_ICON_FOREGROUND_COLOR", "BONOB_ICON_FOREGROUND_COLOR"].forEach(
|
||||||
describe(`when ${k} is not specified`, () => {
|
(k) => {
|
||||||
it(`should default to undefined`, () => {
|
describe(`when ${k} is not specified`, () => {
|
||||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
it(`should default to undefined`, () => {
|
||||||
|
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is ''`, () => {
|
describe(`when ${k} is ''`, () => {
|
||||||
it(`should default to undefined`, () => {
|
it(`should default to undefined`, () => {
|
||||||
process.env[k] = "";
|
process.env[k] = "";
|
||||||
expect(config().icons.foregroundColor).toEqual(undefined);
|
expect(config().icons.foregroundColor).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is specified`, () => {
|
describe(`when ${k} is specified`, () => {
|
||||||
it(`should use it`, () => {
|
it(`should use it`, () => {
|
||||||
process.env[k] = "pink";
|
process.env[k] = "pink";
|
||||||
expect(config().icons.foregroundColor).toEqual("pink");
|
expect(config().icons.foregroundColor).toEqual("pink");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is an invalid string`, () => {
|
describe(`when ${k} is an invalid string`, () => {
|
||||||
it(`should blow up`, () => {
|
it(`should blow up`, () => {
|
||||||
process.env[k] = "#dfasd";
|
process.env[k] = "#dfasd";
|
||||||
expect(() => config()).toThrow(
|
expect(() => config()).toThrow(
|
||||||
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${WORD}`
|
`Invalid value specified for 'BNB_ICON_FOREGROUND_COLOR', must match ${WORD}`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("backgroundColor", () => {
|
describe("backgroundColor", () => {
|
||||||
["BNB_ICON_BACKGROUND_COLOR", "BONOB_ICON_BACKGROUND_COLOR"].forEach(k => {
|
["BNB_ICON_BACKGROUND_COLOR", "BONOB_ICON_BACKGROUND_COLOR"].forEach(
|
||||||
describe(`when ${k} is not specified`, () => {
|
(k) => {
|
||||||
it(`should default to undefined`, () => {
|
describe(`when ${k} is not specified`, () => {
|
||||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
it(`should default to undefined`, () => {
|
||||||
|
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is ''`, () => {
|
describe(`when ${k} is ''`, () => {
|
||||||
it(`should default to undefined`, () => {
|
it(`should default to undefined`, () => {
|
||||||
process.env[k] = "";
|
process.env[k] = "";
|
||||||
expect(config().icons.backgroundColor).toEqual(undefined);
|
expect(config().icons.backgroundColor).toEqual(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is specified`, () => {
|
describe(`when ${k} is specified`, () => {
|
||||||
it(`should use it`, () => {
|
it(`should use it`, () => {
|
||||||
process.env[k] = "blue";
|
process.env[k] = "blue";
|
||||||
expect(config().icons.backgroundColor).toEqual("blue");
|
expect(config().icons.backgroundColor).toEqual("blue");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is an invalid string`, () => {
|
describe(`when ${k} is an invalid string`, () => {
|
||||||
it(`should blow up`, () => {
|
it(`should blow up`, () => {
|
||||||
process.env[k] = "#red";
|
process.env[k] = "#red";
|
||||||
expect(() => config()).toThrow(
|
expect(() => config()).toThrow(
|
||||||
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${WORD}`
|
`Invalid value specified for 'BNB_ICON_BACKGROUND_COLOR', must match ${WORD}`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,7 +240,7 @@ describe("config", () => {
|
|||||||
expect(config().secret).toEqual("bonob");
|
expect(config().secret).toEqual("bonob");
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SECRET", "BONOB_SECRET"].forEach(key => {
|
["BNB_SECRET", "BONOB_SECRET"].forEach((key) => {
|
||||||
it(`should be overridable using ${key}`, () => {
|
it(`should be overridable using ${key}`, () => {
|
||||||
process.env[key] = "new secret";
|
process.env[key] = "new secret";
|
||||||
expect(config().secret).toEqual("new secret");
|
expect(config().secret).toEqual("new secret");
|
||||||
@@ -248,7 +254,7 @@ describe("config", () => {
|
|||||||
expect(config().sonos.serviceName).toEqual("bonob");
|
expect(config().sonos.serviceName).toEqual("bonob");
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SONOS_SERVICE_NAME", "BONOB_SONOS_SERVICE_NAME"].forEach(k => {
|
["BNB_SONOS_SERVICE_NAME", "BONOB_SONOS_SERVICE_NAME"].forEach((k) => {
|
||||||
it("should be overridable", () => {
|
it("should be overridable", () => {
|
||||||
process.env[k] = "foobar1000";
|
process.env[k] = "foobar1000";
|
||||||
expect(config().sonos.serviceName).toEqual("foobar1000");
|
expect(config().sonos.serviceName).toEqual("foobar1000");
|
||||||
@@ -256,21 +262,23 @@ describe("config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SONOS_DEVICE_DISCOVERY", "BONOB_SONOS_DEVICE_DISCOVERY"].forEach(k => {
|
["BNB_SONOS_DEVICE_DISCOVERY", "BONOB_SONOS_DEVICE_DISCOVERY"].forEach(
|
||||||
describeBooleanConfigValue(
|
(k) => {
|
||||||
"deviceDiscovery",
|
describeBooleanConfigValue(
|
||||||
k,
|
"deviceDiscovery",
|
||||||
true,
|
k,
|
||||||
(config) => config.sonos.discovery.enabled
|
true,
|
||||||
);
|
(config) => config.sonos.discovery.enabled
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
describe("seedHost", () => {
|
describe("seedHost", () => {
|
||||||
it("should default to undefined", () => {
|
it("should default to undefined", () => {
|
||||||
expect(config().sonos.discovery.seedHost).toBeUndefined();
|
expect(config().sonos.discovery.seedHost).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SONOS_SEED_HOST", "BONOB_SONOS_SEED_HOST"].forEach(k => {
|
["BNB_SONOS_SEED_HOST", "BONOB_SONOS_SEED_HOST"].forEach((k) => {
|
||||||
it("should be overridable", () => {
|
it("should be overridable", () => {
|
||||||
process.env[k] = "123.456.789.0";
|
process.env[k] = "123.456.789.0";
|
||||||
expect(config().sonos.discovery.seedHost).toEqual("123.456.789.0");
|
expect(config().sonos.discovery.seedHost).toEqual("123.456.789.0");
|
||||||
@@ -278,7 +286,7 @@ describe("config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SONOS_AUTO_REGISTER", "BONOB_SONOS_AUTO_REGISTER"].forEach(k => {
|
["BNB_SONOS_AUTO_REGISTER", "BONOB_SONOS_AUTO_REGISTER"].forEach((k) => {
|
||||||
describeBooleanConfigValue(
|
describeBooleanConfigValue(
|
||||||
"autoRegister",
|
"autoRegister",
|
||||||
k,
|
k,
|
||||||
@@ -287,13 +295,12 @@ describe("config", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("sid", () => {
|
describe("sid", () => {
|
||||||
it("should default to 246", () => {
|
it("should default to 246", () => {
|
||||||
expect(config().sonos.sid).toEqual(246);
|
expect(config().sonos.sid).toEqual(246);
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SONOS_SERVICE_ID", "BONOB_SONOS_SERVICE_ID"].forEach(k => {
|
["BNB_SONOS_SERVICE_ID", "BONOB_SONOS_SERVICE_ID"].forEach((k) => {
|
||||||
it("should be overridable", () => {
|
it("should be overridable", () => {
|
||||||
process.env[k] = "786";
|
process.env[k] = "786";
|
||||||
expect(config().sonos.sid).toEqual(786);
|
expect(config().sonos.sid).toEqual(786);
|
||||||
@@ -304,28 +311,34 @@ describe("config", () => {
|
|||||||
|
|
||||||
describe("subsonic", () => {
|
describe("subsonic", () => {
|
||||||
describe("url", () => {
|
describe("url", () => {
|
||||||
["BNB_SUBSONIC_URL", "BONOB_SUBSONIC_URL", "BONOB_NAVIDROME_URL"].forEach(k => {
|
["BNB_SUBSONIC_URL", "BONOB_SUBSONIC_URL", "BONOB_NAVIDROME_URL"].forEach(
|
||||||
describe(`when ${k} is not specified`, () => {
|
(k) => {
|
||||||
it(`should default to http://${hostname()}:4533`, () => {
|
describe(`when ${k} is not specified`, () => {
|
||||||
expect(config().subsonic.url).toEqual(`http://${hostname()}:4533`);
|
it(`should default to http://${hostname()}:4533`, () => {
|
||||||
|
expect(config().subsonic.url).toEqual(
|
||||||
|
`http://${hostname()}:4533`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is ''`, () => {
|
describe(`when ${k} is ''`, () => {
|
||||||
it(`should default to http://${hostname()}:4533`, () => {
|
it(`should default to http://${hostname()}:4533`, () => {
|
||||||
process.env[k] = "";
|
process.env[k] = "";
|
||||||
expect(config().subsonic.url).toEqual(`http://${hostname()}:4533`);
|
expect(config().subsonic.url).toEqual(
|
||||||
|
`http://${hostname()}:4533`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe(`when ${k} is specified`, () => {
|
describe(`when ${k} is specified`, () => {
|
||||||
it(`should use it for ${k}`, () => {
|
it(`should use it for ${k}`, () => {
|
||||||
const url = "http://navidrome.example.com:1234";
|
const url = "http://navidrome.example.com:1234";
|
||||||
process.env[k] = url;
|
process.env[k] = url;
|
||||||
expect(config().subsonic.url).toEqual(url);
|
expect(config().subsonic.url).toEqual(url);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("customClientsFor", () => {
|
describe("customClientsFor", () => {
|
||||||
@@ -333,17 +346,31 @@ describe("config", () => {
|
|||||||
expect(config().subsonic.customClientsFor).toBeUndefined();
|
expect(config().subsonic.customClientsFor).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_SUBSONIC_CUSTOM_CLIENTS", "BONOB_SUBSONIC_CUSTOM_CLIENTS", "BONOB_NAVIDROME_CUSTOM_CLIENTS"].forEach(k => {
|
[
|
||||||
|
"BNB_SUBSONIC_CUSTOM_CLIENTS",
|
||||||
|
"BONOB_SUBSONIC_CUSTOM_CLIENTS",
|
||||||
|
"BONOB_NAVIDROME_CUSTOM_CLIENTS",
|
||||||
|
].forEach((k) => {
|
||||||
it(`should be overridable for ${k}`, () => {
|
it(`should be overridable for ${k}`, () => {
|
||||||
process.env[k] = "whoop/whoop";
|
process.env[k] = "whoop/whoop";
|
||||||
expect(config().subsonic.customClientsFor).toEqual("whoop/whoop");
|
expect(config().subsonic.customClientsFor).toEqual("whoop/whoop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("artistImageCache", () => {
|
||||||
|
it("should default to undefined", () => {
|
||||||
|
expect(config().subsonic.artistImageCache).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should be overridable for BNB_SUBSONIC_ARTIST_IMAGE_CACHE`, () => {
|
||||||
|
process.env["BNB_SUBSONIC_ARTIST_IMAGE_CACHE"] = "/some/path";
|
||||||
|
expect(config().subsonic.artistImageCache).toEqual("/some/path");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
["BNB_SCROBBLE_TRACKS", "BONOB_SCROBBLE_TRACKS"].forEach((k) => {
|
||||||
["BNB_SCROBBLE_TRACKS", "BONOB_SCROBBLE_TRACKS"].forEach(k => {
|
|
||||||
describeBooleanConfigValue(
|
describeBooleanConfigValue(
|
||||||
"scrobbleTracks",
|
"scrobbleTracks",
|
||||||
k,
|
k,
|
||||||
@@ -352,7 +379,7 @@ describe("config", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
["BNB_REPORT_NOW_PLAYING", "BONOB_REPORT_NOW_PLAYING"].forEach(k => {
|
["BNB_REPORT_NOW_PLAYING", "BONOB_REPORT_NOW_PLAYING"].forEach((k) => {
|
||||||
describeBooleanConfigValue(
|
describeBooleanConfigValue(
|
||||||
"reportNowPlaying",
|
"reportNowPlaying",
|
||||||
k,
|
k,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Md5 } from "ts-md5/dist/md5";
|
import { Md5 } from "ts-md5/dist/md5";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
import tmp from "tmp";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isDodgyImage,
|
isDodgyImage,
|
||||||
@@ -11,6 +14,7 @@ import {
|
|||||||
appendMimeTypeToClientFor,
|
appendMimeTypeToClientFor,
|
||||||
asURLSearchParams,
|
asURLSearchParams,
|
||||||
splitCoverArtId,
|
splitCoverArtId,
|
||||||
|
cachingImageFetcher,
|
||||||
} from "../src/subsonic";
|
} from "../src/subsonic";
|
||||||
import encryption from "../src/encryption";
|
import encryption from "../src/encryption";
|
||||||
|
|
||||||
@@ -158,6 +162,74 @@ describe("asURLSearchParams", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cachingImageFetcher", () => {
|
||||||
|
const delegate = jest.fn();
|
||||||
|
const url = "http://test.example.com/someimage.jpg";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is no image in the cache", () => {
|
||||||
|
it.only("should fetch the image from the source and then cache and return it", async () => {
|
||||||
|
const dir = tmp.dirSync();
|
||||||
|
const cacheFile = path.join(dir.name, `${Md5.hashStr(url)}.png`);
|
||||||
|
const jpgImage = Buffer.from("jpg-image", 'utf-8');
|
||||||
|
const pngImage = Buffer.from("png-image", 'utf-8');
|
||||||
|
|
||||||
|
delegate.mockResolvedValue({ contentType: "image/jpeg", data: jpgImage });
|
||||||
|
const png = jest.fn();
|
||||||
|
(sharp as unknown as jest.Mock).mockReturnValue({ png });
|
||||||
|
png.mockReturnValue({
|
||||||
|
toBuffer: () => Promise.resolve(pngImage),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await cachingImageFetcher(dir.name, delegate)(url);
|
||||||
|
|
||||||
|
expect(result!.contentType).toEqual("image/png");
|
||||||
|
expect(result!.data).toEqual(pngImage);
|
||||||
|
|
||||||
|
expect(delegate).toHaveBeenCalledWith(url);
|
||||||
|
expect(fse.existsSync(cacheFile)).toEqual(true);
|
||||||
|
expect(fse.readFileSync(cacheFile)).toEqual(pngImage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the image is already in the cache", () => {
|
||||||
|
it.only("should fetch the image from the cache and return it", async () => {
|
||||||
|
const dir = tmp.dirSync();
|
||||||
|
const cacheFile = path.join(dir.name, `${Md5.hashStr(url)}.png`);
|
||||||
|
const data = Buffer.from("foobar2", "utf-8");
|
||||||
|
|
||||||
|
fse.writeFileSync(cacheFile, data);
|
||||||
|
|
||||||
|
const result = await cachingImageFetcher(dir.name, delegate)(url);
|
||||||
|
|
||||||
|
expect(result!.contentType).toEqual("image/png");
|
||||||
|
expect(result!.data).toEqual(data);
|
||||||
|
|
||||||
|
expect(delegate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the delegate returns undefined", () => {
|
||||||
|
it.only("should return undefined", async () => {
|
||||||
|
const dir = tmp.dirSync();
|
||||||
|
const cacheFile = path.join(dir.name, `${Md5.hashStr(url)}.png`);
|
||||||
|
|
||||||
|
delegate.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const result = await cachingImageFetcher(dir.name, delegate)(url);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
|
||||||
|
expect(delegate).toHaveBeenCalledWith(url);
|
||||||
|
expect(fse.existsSync(cacheFile)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const ok = (data: string) => ({
|
const ok = (data: string) => ({
|
||||||
status: 200,
|
status: 200,
|
||||||
data,
|
data,
|
||||||
|
|||||||
62
yarn.lock
62
yarn.lock
@@ -1131,6 +1131,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/fs-extra@npm:^9.0.13":
|
||||||
|
version: 9.0.13
|
||||||
|
resolution: "@types/fs-extra@npm:9.0.13"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "*"
|
||||||
|
checksum: add79e212acd5ac76b97b9045834e03a7996aef60a814185e0459088fd290519a3c1620865d588fa36c4498bf614210d2a703af5cf80aa1dbc125db78f6edac3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/graceful-fs@npm:^4.1.2":
|
"@types/graceful-fs@npm:^4.1.2":
|
||||||
version: 4.1.5
|
version: 4.1.5
|
||||||
resolution: "@types/graceful-fs@npm:4.1.5"
|
resolution: "@types/graceful-fs@npm:4.1.5"
|
||||||
@@ -1303,6 +1312,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/tmp@npm:^0.2.1":
|
||||||
|
version: 0.2.1
|
||||||
|
resolution: "@types/tmp@npm:0.2.1"
|
||||||
|
checksum: 2617d2a04811ca78a8d21f5ffc3bd7c392e03c440053a615b091f3e3726540d36babffc750614a803c81b9f2c5f218cdafc748d8cf4638eade2962f8ccddd2fa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/underscore@npm:^1.11.3":
|
"@types/underscore@npm:^1.11.3":
|
||||||
version: 1.11.3
|
version: 1.11.3
|
||||||
resolution: "@types/underscore@npm:1.11.3"
|
resolution: "@types/underscore@npm:1.11.3"
|
||||||
@@ -1764,12 +1780,14 @@ __metadata:
|
|||||||
"@svrooij/sonos": ^2.4.0
|
"@svrooij/sonos": ^2.4.0
|
||||||
"@types/chai": ^4.2.21
|
"@types/chai": ^4.2.21
|
||||||
"@types/express": ^4.17.13
|
"@types/express": ^4.17.13
|
||||||
|
"@types/fs-extra": ^9.0.13
|
||||||
"@types/jest": ^27.0.1
|
"@types/jest": ^27.0.1
|
||||||
"@types/mocha": ^9.0.0
|
"@types/mocha": ^9.0.0
|
||||||
"@types/morgan": ^1.9.3
|
"@types/morgan": ^1.9.3
|
||||||
"@types/node": ^16.7.13
|
"@types/node": ^16.7.13
|
||||||
"@types/sharp": ^0.28.6
|
"@types/sharp": ^0.28.6
|
||||||
"@types/supertest": ^2.0.11
|
"@types/supertest": ^2.0.11
|
||||||
|
"@types/tmp": ^0.2.1
|
||||||
"@types/underscore": ^1.11.3
|
"@types/underscore": ^1.11.3
|
||||||
"@types/uuid": ^8.3.1
|
"@types/uuid": ^8.3.1
|
||||||
axios: ^0.21.4
|
axios: ^0.21.4
|
||||||
@@ -1778,6 +1796,7 @@ __metadata:
|
|||||||
eta: ^1.12.3
|
eta: ^1.12.3
|
||||||
express: ^4.17.1
|
express: ^4.17.1
|
||||||
fp-ts: ^2.11.1
|
fp-ts: ^2.11.1
|
||||||
|
fs-extra: ^10.0.0
|
||||||
get-port: ^5.1.1
|
get-port: ^5.1.1
|
||||||
image-js: ^0.33.0
|
image-js: ^0.33.0
|
||||||
jest: ^27.1.0
|
jest: ^27.1.0
|
||||||
@@ -1788,6 +1807,7 @@ __metadata:
|
|||||||
sharp: ^0.29.1
|
sharp: ^0.29.1
|
||||||
soap: ^0.42.0
|
soap: ^0.42.0
|
||||||
supertest: ^6.1.6
|
supertest: ^6.1.6
|
||||||
|
tmp: ^0.2.1
|
||||||
ts-jest: ^27.0.5
|
ts-jest: ^27.0.5
|
||||||
ts-md5: ^1.2.9
|
ts-md5: ^1.2.9
|
||||||
ts-mockito: ^2.6.1
|
ts-mockito: ^2.6.1
|
||||||
@@ -3162,6 +3182,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"fs-extra@npm:^10.0.0":
|
||||||
|
version: 10.0.0
|
||||||
|
resolution: "fs-extra@npm:10.0.0"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: ^4.2.0
|
||||||
|
jsonfile: ^6.0.1
|
||||||
|
universalify: ^2.0.0
|
||||||
|
checksum: 5285a3d8f34b917cf2b66af8c231a40c1623626e9d701a20051d3337be16c6d7cac94441c8b3732d47a92a2a027886ca93c69b6a4ae6aee3c89650d2a8880c0a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"fs-minipass@npm:^2.0.0":
|
"fs-minipass@npm:^2.0.0":
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
resolution: "fs-minipass@npm:2.1.0"
|
resolution: "fs-minipass@npm:2.1.0"
|
||||||
@@ -3362,7 +3393,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"graceful-fs@npm:^4.2.6":
|
"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
|
||||||
version: 4.2.8
|
version: 4.2.8
|
||||||
resolution: "graceful-fs@npm:4.2.8"
|
resolution: "graceful-fs@npm:4.2.8"
|
||||||
checksum: 5d224c8969ad0581d551dfabdb06882706b31af2561bd5e2034b4097e67cc27d05232849b8643866585fd0a41c7af152950f8776f4dd5579e9853733f31461c6
|
checksum: 5d224c8969ad0581d551dfabdb06882706b31af2561bd5e2034b4097e67cc27d05232849b8643866585fd0a41c7af152950f8776f4dd5579e9853733f31461c6
|
||||||
@@ -4598,6 +4629,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsonfile@npm:^6.0.1":
|
||||||
|
version: 6.1.0
|
||||||
|
resolution: "jsonfile@npm:6.1.0"
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: ^4.1.6
|
||||||
|
universalify: ^2.0.0
|
||||||
|
dependenciesMeta:
|
||||||
|
graceful-fs:
|
||||||
|
optional: true
|
||||||
|
checksum: 7af3b8e1ac8fe7f1eccc6263c6ca14e1966fcbc74b618d3c78a0a2075579487547b94f72b7a1114e844a1e15bb00d440e5d1720bfc4612d790a6f285d5ea8354
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"keyv@npm:^3.0.0":
|
"keyv@npm:^3.0.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "keyv@npm:3.1.0"
|
resolution: "keyv@npm:3.1.0"
|
||||||
@@ -6685,6 +6729,15 @@ resolve@^1.20.0:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tmp@npm:^0.2.1":
|
||||||
|
version: 0.2.1
|
||||||
|
resolution: "tmp@npm:0.2.1"
|
||||||
|
dependencies:
|
||||||
|
rimraf: ^3.0.0
|
||||||
|
checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tmpl@npm:1.0.x":
|
"tmpl@npm:1.0.x":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "tmpl@npm:1.0.4"
|
resolution: "tmpl@npm:1.0.4"
|
||||||
@@ -6992,6 +7045,13 @@ typescript@^4.4.2:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"universalify@npm:^2.0.0":
|
||||||
|
version: 2.0.0
|
||||||
|
resolution: "universalify@npm:2.0.0"
|
||||||
|
checksum: 2406a4edf4a8830aa6813278bab1f953a8e40f2f63a37873ffa9a3bc8f9745d06cc8e88f3572cb899b7e509013f7f6fcc3e37e8a6d914167a5381d8440518c44
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "unpipe@npm:1.0.0"
|
resolution: "unpipe@npm:1.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user