mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
1012 lines
28 KiB
TypeScript
1012 lines
28 KiB
TypeScript
|
|
import { v4 as uuid } from "uuid";
|
|
import { Md5 } from "ts-md5";
|
|
import tmp from "tmp";
|
|
import fse from "fs-extra";
|
|
import path from "path";
|
|
import { pipe } from "fp-ts/lib/function";
|
|
import { option as O } from "fp-ts";
|
|
|
|
import sharp from "sharp";
|
|
jest.mock("sharp");
|
|
|
|
import axios from "axios";
|
|
jest.mock("axios");
|
|
|
|
import randomstring from "randomstring";
|
|
jest.mock("randomstring");
|
|
|
|
import { URLBuilder } from "../src/url_builder";
|
|
import {
|
|
isValidImage,
|
|
t,
|
|
DODGY_IMAGE_NAME,
|
|
asURLSearchParams,
|
|
cachingImageFetcher,
|
|
asTrack,
|
|
artistImageURN,
|
|
song,
|
|
TranscodingCustomPlayers,
|
|
CustomPlayers,
|
|
NO_CUSTOM_PLAYERS,
|
|
Subsonic,
|
|
asGenre
|
|
} from "../src/subsonic";
|
|
|
|
import { b64Encode } from "../src/b64";
|
|
|
|
import { Album, Artist, Track, AlbumSummary } from "../src/music_library";
|
|
import { anAlbum, aTrack, anAlbumSummary, anArtistSummary } from "./builders";
|
|
import { BUrn } from "../src/burn";
|
|
|
|
|
|
|
|
describe("t", () => {
|
|
it("should be an md5 of the password and the salt", () => {
|
|
const p = "password123";
|
|
const s = "saltydog";
|
|
expect(t(p, s)).toEqual(Md5.hashStr(`${p}${s}`));
|
|
});
|
|
});
|
|
|
|
describe("isValidImage", () => {
|
|
describe("when ends with 2a96cbd8b46e442fc41c2b86b821562f.png", () => {
|
|
it("is dodgy", () => {
|
|
expect(
|
|
isValidImage("http://something/2a96cbd8b46e442fc41c2b86b821562f.png")
|
|
).toEqual(false);
|
|
});
|
|
});
|
|
describe("when does not end with 2a96cbd8b46e442fc41c2b86b821562f.png", () => {
|
|
it("is dodgy", () => {
|
|
expect(isValidImage("http://something/somethingelse.png")).toEqual(true);
|
|
expect(
|
|
isValidImage(
|
|
"http://something/2a96cbd8b46e442fc41c2b86b821562f.png?withsomequerystring=true"
|
|
)
|
|
).toEqual(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("StreamClient(s)", () => {
|
|
describe("CustomStreamClientApplications", () => {
|
|
const customClients = TranscodingCustomPlayers.from(
|
|
"audio/flac,audio/mp3>audio/ogg"
|
|
);
|
|
|
|
describe("clientFor", () => {
|
|
describe("when there is a match", () => {
|
|
it("should return the match", () => {
|
|
expect(customClients.encodingFor({ mimeType: "audio/flac" })).toEqual(
|
|
O.of({ player: "bonob+audio/flac", mimeType: "audio/flac" })
|
|
);
|
|
expect(customClients.encodingFor({ mimeType: "audio/mp3" })).toEqual(
|
|
O.of({ player: "bonob+audio/mp3", mimeType: "audio/ogg" })
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("when there is no match", () => {
|
|
it("should return undefined", () => {
|
|
expect(customClients.encodingFor({ mimeType: "audio/bob" })).toEqual(
|
|
O.none
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("asURLSearchParams", () => {
|
|
describe("empty q", () => {
|
|
it("should return empty params", () => {
|
|
const q = {};
|
|
const expected = new URLSearchParams();
|
|
expect(asURLSearchParams(q)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("singular params", () => {
|
|
it("should append each", () => {
|
|
const q = {
|
|
a: 1,
|
|
b: "bee",
|
|
c: false,
|
|
d: true,
|
|
};
|
|
const expected = new URLSearchParams();
|
|
expected.append("a", "1");
|
|
expected.append("b", "bee");
|
|
expected.append("c", "false");
|
|
expected.append("d", "true");
|
|
|
|
expect(asURLSearchParams(q)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("list params", () => {
|
|
it("should append each", () => {
|
|
const q = {
|
|
a: [1, "two", false, true],
|
|
b: "yippee",
|
|
};
|
|
|
|
const expected = new URLSearchParams();
|
|
expected.append("a", "1");
|
|
expected.append("a", "two");
|
|
expected.append("a", "false");
|
|
expected.append("a", "true");
|
|
expected.append("b", "yippee");
|
|
|
|
expect(asURLSearchParams(q)).toEqual(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
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),
|
|
});
|
|
|
|
// todo: the fact that I need to pass the sharp mock in here isnt correct
|
|
const result = await cachingImageFetcher(dir.name, delegate, sharp)(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 maybeIdFromCoverArtUrn = (coverArt: BUrn | undefined) =>
|
|
pipe(
|
|
coverArt,
|
|
O.fromNullable,
|
|
O.map((it) => it.resource.split(":")[1]),
|
|
O.getOrElseW(() => "")
|
|
);
|
|
|
|
const asSongJson = (track: Track) => ({
|
|
id: track.id,
|
|
parent: track.album.id,
|
|
title: track.name,
|
|
album: track.album.name,
|
|
artist: track.artist.name,
|
|
track: track.number,
|
|
genre: track.genre?.name,
|
|
isDir: "false",
|
|
coverArt: maybeIdFromCoverArtUrn(track.coverArt),
|
|
created: "2004-11-08T23:36:11",
|
|
duration: track.duration,
|
|
bitRate: 128,
|
|
size: "5624132",
|
|
suffix: "mp3",
|
|
contentType: track.encoding.mimeType,
|
|
transcodedContentType: undefined,
|
|
isVideo: "false",
|
|
path: "ACDC/High voltage/ACDC - The Jack.mp3",
|
|
albumId: track.album.id,
|
|
artistId: track.artist.id,
|
|
type: "music",
|
|
starred: track.rating.love ? "sometime" : undefined,
|
|
userRating: track.rating.stars,
|
|
year: "",
|
|
});
|
|
|
|
export type ArtistWithAlbum = {
|
|
artist: Artist;
|
|
album: Album;
|
|
};
|
|
|
|
describe("artistURN", () => {
|
|
describe("when artist URL is", () => {
|
|
describe("a valid external URL", () => {
|
|
it("should return an external URN", () => {
|
|
expect(
|
|
artistImageURN({
|
|
artistId: "someArtistId",
|
|
artistImageURL: "http://example.com/image.jpg",
|
|
})
|
|
).toEqual({
|
|
system: "external",
|
|
resource: "http://example.com/image.jpg",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("an invalid external URL", () => {
|
|
describe("and artistId is valid", () => {
|
|
it("should return an external URN", () => {
|
|
expect(
|
|
artistImageURN({
|
|
artistId: "someArtistId",
|
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
|
})
|
|
).toEqual({ system: "subsonic", resource: "art:someArtistId" });
|
|
});
|
|
});
|
|
|
|
describe("and artistId is -1", () => {
|
|
it("should return an error icon urn", () => {
|
|
expect(
|
|
artistImageURN({
|
|
artistId: "-1",
|
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
|
})
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("and artistId is undefined", () => {
|
|
it("should return an error icon urn", () => {
|
|
expect(
|
|
artistImageURN({
|
|
artistId: undefined,
|
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
|
})
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("undefined", () => {
|
|
describe("and artistId is valid", () => {
|
|
it("should return artist art by artist id URN", () => {
|
|
expect(
|
|
artistImageURN({
|
|
artistId: "someArtistId",
|
|
artistImageURL: undefined,
|
|
})
|
|
).toEqual({ system: "subsonic", resource: "art:someArtistId" });
|
|
});
|
|
});
|
|
|
|
describe("and artistId is -1", () => {
|
|
it("should return error icon", () => {
|
|
expect(
|
|
artistImageURN({ artistId: "-1", artistImageURL: undefined })
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("and artistId is undefined", () => {
|
|
it("should return error icon", () => {
|
|
expect(
|
|
artistImageURN({ artistId: undefined, artistImageURL: undefined })
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("asTrack", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
describe("when the song has no artistId", () => {
|
|
const album = anAlbum();
|
|
const track = aTrack({
|
|
artist: {
|
|
id: undefined,
|
|
name: "Not in library so no id",
|
|
image: undefined,
|
|
},
|
|
});
|
|
|
|
it("should provide no artistId", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{ ...asSongJson(track) },
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
expect(result.artist.id).toBeUndefined();
|
|
expect(result.artist.name).toEqual("Not in library so no id");
|
|
expect(result.artist.image).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("when the song has no artist name", () => {
|
|
const album = anAlbum();
|
|
|
|
it("should provide a ? to sonos", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{ id: "1" } as any as song,
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
expect(result.artist.id).toBeUndefined();
|
|
expect(result.artist.name).toEqual("?");
|
|
expect(result.artist.image).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("invalid rating.stars values", () => {
|
|
const album = anAlbum();
|
|
const track = aTrack();
|
|
|
|
describe("a value greater than 5", () => {
|
|
it("should be returned as 0", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{ ...asSongJson(track), userRating: 6 },
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
expect(result.rating.stars).toEqual(0);
|
|
});
|
|
});
|
|
|
|
describe("a value less than 0", () => {
|
|
it("should be returned as 0", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{ ...asSongJson(track), userRating: -1 },
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
expect(result.rating.stars).toEqual(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("content types", () => {
|
|
const album = anAlbum();
|
|
const track = aTrack();
|
|
|
|
describe("when there are no custom players", () => {
|
|
describe("when subsonic reports no transcodedContentType", () => {
|
|
it("should use the default client and default contentType", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{
|
|
...asSongJson(track),
|
|
contentType: "nonTranscodedContentType",
|
|
transcodedContentType: undefined,
|
|
},
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
|
|
expect(result.encoding).toEqual({
|
|
player: "bonob",
|
|
mimeType: "nonTranscodedContentType",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when subsonic reports a transcodedContentType", () => {
|
|
it("should use the default client and transcodedContentType", () => {
|
|
const result = asTrack(
|
|
album,
|
|
{
|
|
...asSongJson(track),
|
|
contentType: "nonTranscodedContentType",
|
|
transcodedContentType: "transcodedContentType",
|
|
},
|
|
NO_CUSTOM_PLAYERS
|
|
);
|
|
|
|
expect(result.encoding).toEqual({
|
|
player: "bonob",
|
|
mimeType: "transcodedContentType",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when there are custom players registered", () => {
|
|
const streamClient = {
|
|
encodingFor: jest.fn(),
|
|
};
|
|
|
|
describe("however no player is found for the default mimeType", () => {
|
|
describe("and there is no transcodedContentType", () => {
|
|
it("should use the default player with the default content type", () => {
|
|
streamClient.encodingFor.mockReturnValue(O.none);
|
|
|
|
const result = asTrack(
|
|
album,
|
|
{
|
|
...asSongJson(track),
|
|
contentType: "nonTranscodedContentType",
|
|
transcodedContentType: undefined,
|
|
},
|
|
streamClient as unknown as CustomPlayers
|
|
);
|
|
|
|
expect(result.encoding).toEqual({
|
|
player: "bonob",
|
|
mimeType: "nonTranscodedContentType",
|
|
});
|
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
|
mimeType: "nonTranscodedContentType",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("and there is a transcodedContentType", () => {
|
|
it("should use the default player with the transcodedContentType", () => {
|
|
streamClient.encodingFor.mockReturnValue(O.none);
|
|
|
|
const result = asTrack(
|
|
album,
|
|
{
|
|
...asSongJson(track),
|
|
contentType: "nonTranscodedContentType",
|
|
transcodedContentType: "transcodedContentType1",
|
|
},
|
|
streamClient as unknown as CustomPlayers
|
|
);
|
|
|
|
expect(result.encoding).toEqual({
|
|
player: "bonob",
|
|
mimeType: "transcodedContentType1",
|
|
});
|
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
|
mimeType: "nonTranscodedContentType",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("there is a player with the matching content type", () => {
|
|
it("should use it", () => {
|
|
const customEncoding = {
|
|
player: "custom-player",
|
|
mimeType: "audio/some-mime-type",
|
|
};
|
|
streamClient.encodingFor.mockReturnValue(O.of(customEncoding));
|
|
|
|
const result = asTrack(
|
|
album,
|
|
{
|
|
...asSongJson(track),
|
|
contentType: "sourced-from/subsonic",
|
|
transcodedContentType: "sourced-from/subsonic2",
|
|
},
|
|
streamClient as unknown as CustomPlayers
|
|
);
|
|
|
|
expect(result.encoding).toEqual(customEncoding);
|
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
|
mimeType: "sourced-from/subsonic",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
const subsonicOK = (body: any = {}) => ({
|
|
"subsonic-response": {
|
|
status: "ok",
|
|
version: "1.16.1",
|
|
type: "subsonic",
|
|
serverVersion: "0.45.1 (c55e6590)",
|
|
...body,
|
|
},
|
|
});
|
|
|
|
const asGenreJson = (genre: { name: string; albumCount: number }) => ({
|
|
songCount: 1475,
|
|
albumCount: genre.albumCount,
|
|
value: genre.name,
|
|
});
|
|
|
|
const getGenresJson = (genres: { name: string; albumCount: number }[]) =>
|
|
subsonicOK({
|
|
genres: {
|
|
genre: genres.map(asGenreJson),
|
|
},
|
|
});
|
|
|
|
const ok = (data: string | object) => ({
|
|
status: 200,
|
|
data,
|
|
});
|
|
|
|
export const asArtistAlbumJson = (
|
|
artist: { id: string | undefined; name: string | undefined },
|
|
album: AlbumSummary
|
|
) => ({
|
|
id: album.id,
|
|
parent: artist.id,
|
|
isDir: "true",
|
|
title: album.name,
|
|
name: album.name,
|
|
album: album.name,
|
|
artist: artist.name,
|
|
genre: album.genre?.name,
|
|
duration: "123",
|
|
playCount: "4",
|
|
year: album.year,
|
|
created: "2021-01-07T08:19:55.834207205Z",
|
|
artistId: artist.id,
|
|
songCount: "19",
|
|
});
|
|
|
|
export const asAlbumJson = (
|
|
artist: { id: string | undefined; name: string | undefined },
|
|
album: Album
|
|
) => ({
|
|
id: album.id,
|
|
parent: artist.id,
|
|
isDir: "true",
|
|
title: album.name,
|
|
name: album.name,
|
|
album: album.name,
|
|
artist: artist.name,
|
|
genre: album.genre?.name,
|
|
coverArt: maybeIdFromCoverArtUrn(album.coverArt),
|
|
duration: "123",
|
|
playCount: "4",
|
|
year: album.year,
|
|
created: "2021-01-07T08:19:55.834207205Z",
|
|
artistId: artist.id,
|
|
songCount: "19",
|
|
isVideo: false,
|
|
song: album.tracks.map(asSongJson),
|
|
});
|
|
|
|
|
|
export const getAlbumJson = (album: Album) =>
|
|
subsonicOK({ album: {
|
|
id: album.id,
|
|
parent: album.artistId,
|
|
album: album.name,
|
|
title: album.name,
|
|
name: album.name,
|
|
isDir: true,
|
|
coverArt: maybeIdFromCoverArtUrn(album.coverArt),
|
|
songCount: 19,
|
|
created: "2021-01-07T08:19:55.834207205Z",
|
|
duration: 123,
|
|
playCount: 4,
|
|
artistId: album.artistId,
|
|
artist: album.artistName,
|
|
year: album.year,
|
|
genre: album.genre?.name,
|
|
song: album.tracks.map(track => ({
|
|
id: track.id,
|
|
parent: track.album.id,
|
|
title: track.name,
|
|
isDir: false,
|
|
isVideo: false,
|
|
type: "music",
|
|
albumId: track.album.id,
|
|
album: track.album.name,
|
|
artistId: track.artist.id,
|
|
artist: track.artist.name,
|
|
coverArt: maybeIdFromCoverArtUrn(track.coverArt),
|
|
duration: track.duration,
|
|
bitRate: 128,
|
|
bitDepth: 16,
|
|
samplingRate: 555,
|
|
channelCount: 2,
|
|
track: track.number,
|
|
year: 1900,
|
|
genre: track.genre?.name,
|
|
size: 5624132,
|
|
discNumer: 1,
|
|
suffix: "mp3",
|
|
contentType: track.encoding.mimeType,
|
|
path: "ACDC/High voltage/ACDC - The Jack.mp3"
|
|
})),
|
|
} });
|
|
|
|
describe("subsonic", () => {
|
|
const url = new URLBuilder("http://127.0.0.22:4567/some-context-path");
|
|
const customPlayers = {
|
|
encodingFor: jest.fn(),
|
|
};
|
|
const username = `user1-${uuid()}`;
|
|
const password = `pass1-${uuid()}`;
|
|
const credentials = { username, password };
|
|
const subsonic = new Subsonic(url, customPlayers);
|
|
|
|
const mockRandomstring = jest.fn();
|
|
const mockGET = jest.fn();
|
|
const mockPOST = jest.fn();
|
|
|
|
const salt = "saltysalty";
|
|
|
|
const authParams = {
|
|
u: username,
|
|
v: "1.16.1",
|
|
c: "bonob",
|
|
t: t(password, salt),
|
|
s: salt,
|
|
};
|
|
|
|
const authParamsPlusJson = {
|
|
...authParams,
|
|
f: "json",
|
|
};
|
|
|
|
const headers = {
|
|
"User-Agent": "bonob",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
jest.resetAllMocks();
|
|
|
|
randomstring.generate = mockRandomstring;
|
|
axios.get = mockGET;
|
|
axios.post = mockPOST;
|
|
|
|
mockRandomstring.mockReturnValue(salt);
|
|
});
|
|
|
|
describe("getting genres", () => {
|
|
describe("when there are none", () => {
|
|
beforeEach(() => {
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getGenresJson([])))
|
|
);
|
|
});
|
|
|
|
it("should return empty array", async () => {
|
|
const result = await subsonic.getGenres(credentials);
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getGenres" }).href(),
|
|
{
|
|
params: asURLSearchParams(authParamsPlusJson),
|
|
headers,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("when there is only 1 that has an albumCount > 0", () => {
|
|
const genres = [
|
|
{ name: "genre1", albumCount: 1 },
|
|
{ name: "genreWithNoAlbums", albumCount: 0 },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getGenresJson(genres)))
|
|
);
|
|
});
|
|
|
|
it("should return them alphabetically sorted", async () => {
|
|
const result = await subsonic.getGenres(credentials);
|
|
|
|
expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]);
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getGenres" }).href(),
|
|
{
|
|
params: asURLSearchParams(authParamsPlusJson),
|
|
headers,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("when there are many that have an albumCount > 0", () => {
|
|
const genres = [
|
|
{ name: "g1", albumCount: 1 },
|
|
{ name: "g2", albumCount: 1 },
|
|
{ name: "g3", albumCount: 1 },
|
|
{ name: "g4", albumCount: 1 },
|
|
{ name: "someGenreWithNoAlbums", albumCount: 0 },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getGenresJson(genres)))
|
|
);
|
|
});
|
|
|
|
it("should return them alphabetically sorted", async () => {
|
|
const result = await subsonic.getGenres(credentials);
|
|
|
|
expect(result).toEqual([
|
|
{ id: b64Encode("g1"), name: "g1" },
|
|
{ id: b64Encode("g2"), name: "g2" },
|
|
{ id: b64Encode("g3"), name: "g3" },
|
|
{ id: b64Encode("g4"), name: "g4" },
|
|
]);
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getGenres" }).href(),
|
|
{
|
|
params: asURLSearchParams(authParamsPlusJson),
|
|
headers,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getting an album", () => {
|
|
describe("when there are no custom players", () => {
|
|
beforeEach(() => {
|
|
customPlayers.encodingFor.mockReturnValue(O.none);
|
|
});
|
|
|
|
describe("when the album has some tracks", () => {
|
|
const artistId = "artist6677"
|
|
const artistName = "Fizzy Wizzy"
|
|
|
|
const albumSummary = anAlbumSummary({ artistId, artistName })
|
|
const artistSumamry = anArtistSummary({ id: artistId, name: artistName })
|
|
|
|
// todo: fix these ratings
|
|
const tracks = [
|
|
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
|
|
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
|
|
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
|
|
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
|
|
];
|
|
|
|
const album = anAlbum({
|
|
...albumSummary,
|
|
tracks,
|
|
artistId,
|
|
artistName,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getAlbumJson(album)))
|
|
);
|
|
});
|
|
|
|
it("should return the album", async () => {
|
|
const result = await subsonic.getAlbum(credentials, album.id);
|
|
|
|
expect(result).toEqual(album);
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getAlbum" }).href(),
|
|
{
|
|
params: asURLSearchParams({
|
|
...authParamsPlusJson,
|
|
id: album.id,
|
|
}),
|
|
headers,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("when the album has no tracks", () => {
|
|
const artistId = "artist6677"
|
|
const artistName = "Fizzy Wizzy"
|
|
|
|
const albumSummary = anAlbumSummary({ artistId, artistName })
|
|
|
|
const album = anAlbum({
|
|
...albumSummary,
|
|
tracks: [],
|
|
artistId,
|
|
artistName,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getAlbumJson(album)))
|
|
);
|
|
});
|
|
|
|
it("should return the album", async () => {
|
|
const result = await subsonic.getAlbum(credentials, album.id);
|
|
|
|
expect(result).toEqual(album);
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getAlbum" }).href(),
|
|
{
|
|
params: asURLSearchParams({
|
|
...authParamsPlusJson,
|
|
id: album.id,
|
|
}),
|
|
headers,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe("when a custom player is configured for the mime type", () => {
|
|
const hipHop = asGenre("Hip-Hop");
|
|
const tripHop = asGenre("Trip-Hop");
|
|
|
|
const albumSummary = anAlbumSummary({ id: "album1", name: "Burnin", genre: hipHop });
|
|
|
|
const artistSummary = anArtistSummary({
|
|
id: "artist1",
|
|
name: "Bob Marley"
|
|
});
|
|
|
|
const alac = aTrack({
|
|
artist: artistSummary,
|
|
album: albumSummary,
|
|
encoding: {
|
|
player: "bonob",
|
|
mimeType: "audio/alac",
|
|
},
|
|
genre: hipHop,
|
|
rating: {
|
|
love: true,
|
|
stars: 3,
|
|
},
|
|
});
|
|
const m4a = aTrack({
|
|
artist: artistSummary,
|
|
album: albumSummary,
|
|
encoding: {
|
|
player: "bonob",
|
|
mimeType: "audio/m4a",
|
|
},
|
|
genre: hipHop,
|
|
rating: {
|
|
love: false,
|
|
stars: 0,
|
|
},
|
|
});
|
|
const mp3 = aTrack({
|
|
artist: artistSummary,
|
|
album: albumSummary,
|
|
encoding: {
|
|
player: "bonob",
|
|
mimeType: "audio/mp3",
|
|
},
|
|
genre: tripHop,
|
|
rating: {
|
|
love: true,
|
|
stars: 5,
|
|
},
|
|
});
|
|
|
|
const album = anAlbum({
|
|
...albumSummary,
|
|
tracks: [alac, m4a, mp3]
|
|
})
|
|
|
|
beforeEach(() => {
|
|
customPlayers.encodingFor
|
|
.mockReturnValueOnce(
|
|
O.of({ player: "bonob+audio/alac", mimeType: "audio/flac" })
|
|
)
|
|
.mockReturnValueOnce(
|
|
O.of({ player: "bonob+audio/m4a", mimeType: "audio/opus" })
|
|
)
|
|
.mockReturnValueOnce(O.none);
|
|
|
|
mockGET.mockImplementationOnce(() =>
|
|
Promise.resolve(ok(getAlbumJson(album)))
|
|
);
|
|
});
|
|
|
|
it("should return the album with custom players applied", async () => {
|
|
const result = await subsonic.getAlbum(credentials, album.id);
|
|
|
|
expect(result).toEqual({
|
|
...album,
|
|
tracks: [
|
|
{
|
|
...alac,
|
|
encoding: {
|
|
player: "bonob+audio/alac",
|
|
mimeType: "audio/flac",
|
|
},
|
|
// todo: this doesnt seem right? why dont the ratings come back?
|
|
rating: {
|
|
love: false,
|
|
stars: 0
|
|
}
|
|
},
|
|
{
|
|
...m4a,
|
|
encoding: {
|
|
player: "bonob+audio/m4a",
|
|
mimeType: "audio/opus",
|
|
},
|
|
rating: {
|
|
love: false,
|
|
stars: 0
|
|
}
|
|
},
|
|
{
|
|
...mp3,
|
|
encoding: {
|
|
player: "bonob",
|
|
mimeType: "audio/mp3",
|
|
},
|
|
rating: {
|
|
love: false,
|
|
stars: 0
|
|
}
|
|
},
|
|
]
|
|
});
|
|
|
|
expect(axios.get).toHaveBeenCalledWith(
|
|
url.append({ pathname: "/rest/getAlbum" }).href(),
|
|
{
|
|
params: asURLSearchParams({
|
|
...authParamsPlusJson,
|
|
id: album.id,
|
|
}),
|
|
headers,
|
|
}
|
|
);
|
|
|
|
expect(customPlayers.encodingFor).toHaveBeenCalledTimes(3);
|
|
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(1, {
|
|
mimeType: "audio/alac",
|
|
});
|
|
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(2, {
|
|
mimeType: "audio/m4a",
|
|
});
|
|
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(3, {
|
|
mimeType: "audio/mp3",
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|