Ability to cache subsonic artist images locally on disk (#61)

This commit is contained in:
Simon J
2021-10-03 16:36:50 +11:00
committed by GitHub
parent da1860d556
commit d7a7747fab
9 changed files with 404 additions and 142 deletions

View File

@@ -2,7 +2,13 @@ import path from "path";
import fs from "fs";
import server from "./server";
import logger from "./logger";
import { appendMimeTypeToClientFor, DEFAULT, Subsonic } from "./subsonic";
import {
appendMimeTypeToClientFor,
axiosImageFetcher,
cachingImageFetcher,
DEFAULT,
Subsonic,
} from "./subsonic";
import encryption from "./encryption";
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
import { InMemoryLinkCodes } from "./link_codes";
@@ -28,10 +34,15 @@ const streamUserAgent = config.subsonic.customClientsFor
? appendMimeTypeToClientFor(config.subsonic.customClientsFor.split(","))
: DEFAULT;
const artistImageFetcher = config.subsonic.artistImageCache
? cachingImageFetcher(config.subsonic.artistImageCache, axiosImageFetcher)
: axiosImageFetcher;
const subsonic = new Subsonic(
config.subsonic.url,
encryption(config.secret),
streamUserAgent
streamUserAgent,
artistImageFetcher
);
const featureFlagAwareMusicService: MusicService = {
@@ -60,7 +71,9 @@ const featureFlagAwareMusicService: MusicService = {
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(
sonosSystem,
@@ -69,12 +82,12 @@ const app = server(
featureFlagAwareMusicService,
{
linkCodes: () => new InMemoryLinkCodes(),
accessTokens: () => new InMemoryAccessTokens(sha256(config.secret)),
accessTokens: () => new InMemoryAccessTokens(sha256(config.secret)),
clock: SystemClock,
iconColors: config.icons,
applyContextPath: true,
logRequests: true,
version
version,
}
);
@@ -90,12 +103,12 @@ if (config.sonos.autoRegister) {
);
}
});
} else if(config.sonos.discovery.enabled) {
sonosSystem.devices().then(devices => {
devices.forEach(d => {
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`)
})
})
} else if (config.sonos.discovery.enabled) {
sonosSystem.devices().then((devices) => {
devices.forEach((d) => {
logger.info(`Found device ${d.name}(${d.group}) @ ${d.ip}:${d.port}`);
});
});
}
export default app;

View File

@@ -84,6 +84,7 @@ export default function () {
subsonic: {
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
artistImageCache: bnbEnvVar("SUBSONIC_ARTIST_IMAGE_CACHE"),
},
scrobbleTracks: bnbEnvVar("SCROBBLE_TRACKS", { default: "true" }) == "true",
reportNowPlaying:

View File

@@ -18,10 +18,13 @@ import {
AlbumSummary,
Genre,
Track,
CoverArt,
} from "./music_service";
import X2JS from "x2js";
import sharp from "sharp";
import _ from "underscore";
import fse from "fs-extra";
import path from "path";
import axios, { AxiosRequestConfig } from "axios";
import { Encryption } from "./encryption";
@@ -311,19 +314,61 @@ export const asURLSearchParams = (q: any) => {
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 {
url: string;
encryption: Encryption;
streamClientApplication: StreamClientApplication;
externalImageFetcher: ImageFetcher;
constructor(
url: string,
encryption: Encryption,
streamClientApplication: StreamClientApplication = DEFAULT
streamClientApplication: StreamClientApplication = DEFAULT,
externalImageFetcher: ImageFetcher = axiosImageFetcher
) {
this.url = url;
this.encryption = encryption;
this.streamClientApplication = streamClientApplication;
this.externalImageFetcher = externalImageFetcher;
}
get = async (
@@ -630,28 +675,21 @@ export class Subsonic implements MusicService {
(it) => it.coverArt
);
if (artist.image.large) {
return axios
.get(artist.image.large!, {
headers: BROWSER_HEADERS,
responseType: "arraybuffer",
})
.then((res) => {
const image = Buffer.from(res.data, "binary");
if (size) {
return sharp(image)
return this.externalImageFetcher(artist.image.large!).then(
(image) => {
if (image && size) {
return sharp(image.data)
.resize(size)
.toBuffer()
.then((resized) => ({
contentType: res.headers["content-type"],
contentType: image.contentType,
data: resized,
}));
} else {
return {
contentType: res.headers["content-type"],
data: image,
};
return image;
}
});
}
);
} else if (albumsWithCoverArt.length > 0) {
return subsonic
.getCoverArt(