mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 09:53:32 +01:00
refactor
This commit is contained in:
@@ -5,8 +5,6 @@ import logger from "./logger";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
appendMimeTypeToClientFor,
|
appendMimeTypeToClientFor,
|
||||||
axiosImageFetcher,
|
|
||||||
cachingImageFetcher,
|
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
Subsonic,
|
Subsonic,
|
||||||
} from "./subsonic";
|
} from "./subsonic";
|
||||||
@@ -17,6 +15,7 @@ import sonos, { bonobService } from "./sonos";
|
|||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_service";
|
||||||
import { SystemClock } from "./clock";
|
import { SystemClock } from "./clock";
|
||||||
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
||||||
|
import { axiosImageFetcher, cachingImageFetcher } from "./images";
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
const clock = SystemClock;
|
const clock = SystemClock;
|
||||||
|
|||||||
48
src/images.ts
Normal file
48
src/images.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
import sharp from "sharp";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import { Md5 } from "ts-md5/dist/md5";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { CoverArt } from "./music_service";
|
||||||
|
import { BROWSER_HEADERS } from "./utils";
|
||||||
|
|
||||||
|
export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
|
||||||
|
|
||||||
|
export const cachingImageFetcher =
|
||||||
|
(cacheDir: string, delegate: ImageFetcher) =>
|
||||||
|
async (url: string): Promise<CoverArt | undefined> => {
|
||||||
|
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
||||||
|
return fse
|
||||||
|
.readFile(filename)
|
||||||
|
.then((data) => ({ contentType: "image/png", data }))
|
||||||
|
.catch(() =>
|
||||||
|
delegate(url).then((image) => {
|
||||||
|
if (image) {
|
||||||
|
return sharp(image.data)
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
.then((png) => {
|
||||||
|
return fse
|
||||||
|
.writeFile(filename, png)
|
||||||
|
.then(() => ({ contentType: "image/png", data: png }));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -35,7 +35,7 @@ import _, { shuffle } from "underscore";
|
|||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import { takeWithRepeats } from "./utils";
|
import { takeWithRepeats } from "./utils";
|
||||||
import { parse } from "./burn";
|
import { parse } from "./burn";
|
||||||
import { axiosImageFetcher, ImageFetcher } from "./subsonic";
|
import { axiosImageFetcher, ImageFetcher } from "./images";
|
||||||
import {
|
import {
|
||||||
JWTSmapiLoginTokens,
|
JWTSmapiLoginTokens,
|
||||||
SmapiAuthTokens,
|
SmapiAuthTokens,
|
||||||
|
|||||||
@@ -15,17 +15,13 @@ import {
|
|||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
Genre,
|
Genre,
|
||||||
Track,
|
Track,
|
||||||
CoverArt,
|
|
||||||
Rating,
|
Rating,
|
||||||
AlbumQueryType,
|
AlbumQueryType,
|
||||||
Artist,
|
Artist,
|
||||||
AuthFailure,
|
AuthFailure,
|
||||||
Sortable,
|
Sortable,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
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 randomstring from "randomstring";
|
import randomstring from "randomstring";
|
||||||
@@ -33,16 +29,8 @@ import { b64Encode, b64Decode } from "./b64";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { assertSystem, BUrn } from "./burn";
|
import { assertSystem, BUrn } from "./burn";
|
||||||
import { artist } from "./smapi";
|
import { artist } from "./smapi";
|
||||||
|
import { axiosImageFetcher, ImageFetcher } from "./images";
|
||||||
|
|
||||||
export const BROWSER_HEADERS = {
|
|
||||||
accept:
|
|
||||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
||||||
"accept-encoding": "gzip, deflate, br",
|
|
||||||
"accept-language": "en-GB,en;q=0.5",
|
|
||||||
"upgrade-insecure-requests": "1",
|
|
||||||
"user-agent":
|
|
||||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const t = (password: string, s: string) =>
|
export const t = (password: string, s: string) =>
|
||||||
Md5.hashStr(`${password}${s}`);
|
Md5.hashStr(`${password}${s}`);
|
||||||
@@ -353,45 +341,6 @@ export const asURLSearchParams = (q: any) => {
|
|||||||
return urlSearchParams;
|
return urlSearchParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
|
|
||||||
|
|
||||||
export const cachingImageFetcher =
|
|
||||||
(cacheDir: string, delegate: ImageFetcher) =>
|
|
||||||
async (url: string): Promise<CoverArt | undefined> => {
|
|
||||||
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
|
||||||
return fse
|
|
||||||
.readFile(filename)
|
|
||||||
.then((data) => ({ contentType: "image/png", data }))
|
|
||||||
.catch(() =>
|
|
||||||
delegate(url).then((image) => {
|
|
||||||
if (image) {
|
|
||||||
return sharp(image.data)
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
.then((png) => {
|
|
||||||
return fse
|
|
||||||
.writeFile(filename, png)
|
|
||||||
.then(() => ({ contentType: "image/png", data: png }));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const AlbumQueryTypeToSubsonicType: Record<AlbumQueryType, string> = {
|
const AlbumQueryTypeToSubsonicType: Record<AlbumQueryType, string> = {
|
||||||
alphabeticalByArtist: "alphabeticalByArtist",
|
alphabeticalByArtist: "alphabeticalByArtist",
|
||||||
alphabeticalByName: "alphabeticalByName",
|
alphabeticalByName: "alphabeticalByName",
|
||||||
@@ -893,6 +842,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
|||||||
export class Subsonic implements MusicService {
|
export class Subsonic implements MusicService {
|
||||||
url: string;
|
url: string;
|
||||||
streamClientApplication: StreamClientApplication;
|
streamClientApplication: StreamClientApplication;
|
||||||
|
// todo: why is this in here?
|
||||||
externalImageFetcher: ImageFetcher;
|
externalImageFetcher: ImageFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
10
src/utils.ts
10
src/utils.ts
@@ -1,3 +1,13 @@
|
|||||||
|
export const BROWSER_HEADERS = {
|
||||||
|
accept:
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"accept-language": "en-GB,en;q=0.5",
|
||||||
|
"upgrade-insecure-requests": "1",
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
|
||||||
|
};
|
||||||
|
|
||||||
export function takeWithRepeats<T>(things:T[], count: number) {
|
export function takeWithRepeats<T>(things:T[], count: number) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for(let i = 0; i < count; i++) {
|
for(let i = 0; i < count; i++) {
|
||||||
|
|||||||
78
tests/images.test.ts
Normal file
78
tests/images.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
|
||||||
|
import tmp from "tmp";
|
||||||
|
import fse from "fs-extra";
|
||||||
|
import path from "path";
|
||||||
|
import { Md5 } from "ts-md5";
|
||||||
|
|
||||||
|
import sharp from "sharp";
|
||||||
|
jest.mock("sharp");
|
||||||
|
|
||||||
|
import { cachingImageFetcher } from "../src/images";
|
||||||
|
|
||||||
|
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("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("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("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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
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 { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { option as O, taskEither as TE, task as T, either as E } from "fp-ts";
|
import { option as O, taskEither as TE, task as T, either as E } from "fp-ts";
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
asGenre,
|
asGenre,
|
||||||
appendMimeTypeToClientFor,
|
appendMimeTypeToClientFor,
|
||||||
asURLSearchParams,
|
asURLSearchParams,
|
||||||
cachingImageFetcher,
|
|
||||||
asTrack,
|
asTrack,
|
||||||
artistImageURN,
|
artistImageURN,
|
||||||
images,
|
images,
|
||||||
@@ -29,8 +26,6 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
jest.mock("axios");
|
jest.mock("axios");
|
||||||
|
|
||||||
import sharp from "sharp";
|
|
||||||
jest.mock("sharp");
|
|
||||||
|
|
||||||
import randomstring from "randomstring";
|
import randomstring from "randomstring";
|
||||||
jest.mock("randomstring");
|
jest.mock("randomstring");
|
||||||
@@ -172,74 +167,6 @@ 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("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("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("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 | object) => ({
|
const ok = (data: string | object) => ({
|
||||||
status: 200,
|
status: 200,
|
||||||
data,
|
data,
|
||||||
|
|||||||
Reference in New Issue
Block a user