mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
URN for image info (#78)
* Allow music service to return a URN identifying cover art for an entity * Fix bug with playlist cover art rending same album multiple times
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { SonosDevice } from "@svrooij/sonos/lib";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import randomstring from "randomstring";
|
||||
|
||||
import { Credentials } from "../src/smapi";
|
||||
import { Service, Device } from "../src/sonos";
|
||||
@@ -11,9 +12,12 @@ import {
|
||||
artistToArtistSummary,
|
||||
PlaylistSummary,
|
||||
Playlist,
|
||||
SimilarArtist,
|
||||
AlbumSummary,
|
||||
} from "../src/music_service";
|
||||
import randomString from "../src/random_string";
|
||||
|
||||
import { b64Encode } from "../src/b64";
|
||||
import { artistImageURN } from "../src/subsonic";
|
||||
|
||||
const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max));
|
||||
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`;
|
||||
@@ -42,7 +46,7 @@ export function aPlaylistSummary(
|
||||
): PlaylistSummary {
|
||||
return {
|
||||
id: `playlist-${uuid()}`,
|
||||
name: `playlistname-${randomString()}`,
|
||||
name: `playlistname-${randomstring.generate()}`,
|
||||
...fields,
|
||||
};
|
||||
}
|
||||
@@ -50,7 +54,7 @@ export function aPlaylistSummary(
|
||||
export function aPlaylist(fields: Partial<Playlist> = {}): Playlist {
|
||||
return {
|
||||
id: `playlist-${uuid()}`,
|
||||
name: `playlist-${randomString()}`,
|
||||
name: `playlist-${randomstring.generate()}`,
|
||||
entries: [aTrack(), aTrack()],
|
||||
...fields,
|
||||
};
|
||||
@@ -97,21 +101,34 @@ export function someCredentials(token: string): Credentials {
|
||||
};
|
||||
}
|
||||
|
||||
export function aSimilarArtist(
|
||||
fields: Partial<SimilarArtist> = {}
|
||||
): SimilarArtist {
|
||||
const id = fields.id || uuid();
|
||||
return {
|
||||
id,
|
||||
name: `Similar Artist ${id}`,
|
||||
image: artistImageURN({ artistId: id }),
|
||||
inLibrary: true,
|
||||
...fields,
|
||||
};
|
||||
}
|
||||
|
||||
export function anArtist(fields: Partial<Artist> = {}): Artist {
|
||||
const id = uuid();
|
||||
const id = fields.id || uuid();
|
||||
const artist = {
|
||||
id,
|
||||
name: `Artist ${id}`,
|
||||
albums: [anAlbum(), anAlbum(), anAlbum()],
|
||||
image: {
|
||||
small: `/artist/art/${id}/small`,
|
||||
medium: `/artist/art/${id}/small`,
|
||||
large: `/artist/art/${id}/large`,
|
||||
},
|
||||
image: { system: "subsonic", resource: `art:${id}` },
|
||||
similarArtists: [
|
||||
{ id: uuid(), name: "Similar artist1", inLibrary: true },
|
||||
{ id: uuid(), name: "Similar artist2", inLibrary: true },
|
||||
{ id: "-1", name: "Artist not in library", inLibrary: false },
|
||||
aSimilarArtist({ id: uuid(), name: "Similar artist1", inLibrary: true }),
|
||||
aSimilarArtist({ id: uuid(), name: "Similar artist2", inLibrary: true }),
|
||||
aSimilarArtist({
|
||||
id: "-1",
|
||||
name: "Artist not in library",
|
||||
inLibrary: false,
|
||||
}),
|
||||
],
|
||||
...fields,
|
||||
};
|
||||
@@ -163,11 +180,11 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
||||
album: albumToAlbumSummary(
|
||||
anAlbum({ artistId: artist.id, artistName: artist.name, genre })
|
||||
),
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
rating,
|
||||
...fields,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||
const id = uuid();
|
||||
@@ -177,11 +194,25 @@ export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||
genre: randomGenre(),
|
||||
year: `19${randomInt(99)}`,
|
||||
artistId: `Artist ${uuid()}`,
|
||||
artistName: `Artist ${randomString()}`,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
artistName: `Artist ${randomstring.generate()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}` },
|
||||
...fields,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function anAlbumSummary(fields: Partial<AlbumSummary> = {}): AlbumSummary {
|
||||
const id = uuid();
|
||||
return {
|
||||
id,
|
||||
name: `Album ${id}`,
|
||||
year: `19${randomInt(99)}`,
|
||||
genre: randomGenre(),
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}` },
|
||||
artistId: `Artist ${uuid()}`,
|
||||
artistName: `Artist ${randomstring.generate()}`,
|
||||
...fields
|
||||
}
|
||||
};
|
||||
|
||||
export const BLONDIE_ID = uuid();
|
||||
export const BLONDIE_NAME = "Blondie";
|
||||
@@ -196,7 +227,7 @@ export const BLONDIE: Artist = {
|
||||
genre: NEW_WAVE,
|
||||
artistId: BLONDIE_ID,
|
||||
artistName: BLONDIE_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
@@ -205,14 +236,10 @@ export const BLONDIE: Artist = {
|
||||
genre: POP_ROCK,
|
||||
artistId: BLONDIE_ID,
|
||||
artistName: BLONDIE_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
],
|
||||
image: {
|
||||
small: undefined,
|
||||
medium: undefined,
|
||||
large: undefined,
|
||||
},
|
||||
image: { system: "external", resource: "http://localhost:1234/images/blondie.jpg" },
|
||||
similarArtists: [],
|
||||
};
|
||||
|
||||
@@ -229,7 +256,7 @@ export const BOB_MARLEY: Artist = {
|
||||
genre: REGGAE,
|
||||
artistId: BOB_MARLEY_ID,
|
||||
artistName: BOB_MARLEY_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
@@ -238,7 +265,7 @@ export const BOB_MARLEY: Artist = {
|
||||
genre: REGGAE,
|
||||
artistId: BOB_MARLEY_ID,
|
||||
artistName: BOB_MARLEY_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
@@ -247,14 +274,10 @@ export const BOB_MARLEY: Artist = {
|
||||
genre: SKA,
|
||||
artistId: BOB_MARLEY_ID,
|
||||
artistName: BOB_MARLEY_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
],
|
||||
image: {
|
||||
small: "http://localhost/BOB_MARLEY/sml",
|
||||
medium: "http://localhost/BOB_MARLEY/med",
|
||||
large: "http://localhost/BOB_MARLEY/lge",
|
||||
},
|
||||
image: { system: "subsonic", resource: BOB_MARLEY_ID },
|
||||
similarArtists: [],
|
||||
};
|
||||
|
||||
@@ -265,9 +288,8 @@ export const MADONNA: Artist = {
|
||||
name: MADONNA_NAME,
|
||||
albums: [],
|
||||
image: {
|
||||
small: "http://localhost/MADONNA/sml",
|
||||
medium: undefined,
|
||||
large: "http://localhost/MADONNA/lge",
|
||||
system: "external",
|
||||
resource: "http://localhost:1234/images/madonna.jpg",
|
||||
},
|
||||
similarArtists: [],
|
||||
};
|
||||
@@ -285,7 +307,7 @@ export const METALLICA: Artist = {
|
||||
genre: METAL,
|
||||
artistId: METALLICA_ID,
|
||||
artistName: METALLICA_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
{
|
||||
id: uuid(),
|
||||
@@ -294,18 +316,13 @@ export const METALLICA: Artist = {
|
||||
genre: METAL,
|
||||
artistId: METALLICA_ID,
|
||||
artistName: METALLICA_NAME,
|
||||
coverArt: `coverArt:${uuid()}`,
|
||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||
},
|
||||
],
|
||||
image: {
|
||||
small: "http://localhost/METALLICA/sml",
|
||||
medium: "http://localhost/METALLICA/med",
|
||||
large: "http://localhost/METALLICA/lge",
|
||||
},
|
||||
image: { system: "subsonic", resource: METALLICA_ID },
|
||||
similarArtists: [],
|
||||
};
|
||||
|
||||
export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA];
|
||||
|
||||
export const ALL_ALBUMS = ALL_ARTISTS.flatMap((it) => it.albums || []);
|
||||
|
||||
|
||||
114
tests/burn.test.ts
Normal file
114
tests/burn.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { assertSystem, BUrn, format, formatForURL, parse } from "../src/burn";
|
||||
|
||||
type BUrnSpec = {
|
||||
burn: BUrn;
|
||||
asString: string;
|
||||
shorthand: string;
|
||||
};
|
||||
|
||||
describe("BUrn", () => {
|
||||
describe("format", () => {
|
||||
(
|
||||
[
|
||||
{
|
||||
burn: { system: "internal", resource: "icon:error" },
|
||||
asString: "bnb:internal:icon:error",
|
||||
shorthand: "bnb:i:icon:error",
|
||||
},
|
||||
{
|
||||
burn: {
|
||||
system: "external",
|
||||
resource: "http://example.com/widget.jpg",
|
||||
},
|
||||
asString: "bnb:external:http://example.com/widget.jpg",
|
||||
shorthand: "bnb:e:http://example.com/widget.jpg",
|
||||
},
|
||||
{
|
||||
burn: { system: "subsonic", resource: "art:1234" },
|
||||
asString: "bnb:subsonic:art:1234",
|
||||
shorthand: "bnb:s:art:1234",
|
||||
},
|
||||
{
|
||||
burn: { system: "navidrome", resource: "art:1234" },
|
||||
asString: "bnb:navidrome:art:1234",
|
||||
shorthand: "bnb:n:art:1234",
|
||||
},
|
||||
] as BUrnSpec[]
|
||||
).forEach(({ burn, asString, shorthand }) => {
|
||||
describe(asString, () => {
|
||||
it("can be formatted as string and then roundtripped back into BUrn", () => {
|
||||
const stringValue = format(burn);
|
||||
expect(stringValue).toEqual(asString);
|
||||
expect(parse(stringValue)).toEqual(burn);
|
||||
});
|
||||
|
||||
it("can be formatted as shorthand string and then roundtripped back into BUrn", () => {
|
||||
const stringValue = format(burn, { shorthand: true });
|
||||
expect(stringValue).toEqual(shorthand);
|
||||
expect(parse(stringValue)).toEqual(burn);
|
||||
});
|
||||
|
||||
describe(`encrypted ${asString}`, () => {
|
||||
it("can be formatted as an encrypted string and then roundtripped back into BUrn", () => {
|
||||
const stringValue = format(burn, { encrypt: true });
|
||||
expect(stringValue.startsWith("bnb:encrypted:")).toBeTruthy();
|
||||
expect(stringValue).not.toContain(burn.system);
|
||||
expect(stringValue).not.toContain(burn.resource);
|
||||
expect(parse(stringValue)).toEqual(burn);
|
||||
});
|
||||
|
||||
it("can be formatted as an encrypted shorthand string and then roundtripped back into BUrn", () => {
|
||||
const stringValue = format(burn, {
|
||||
shorthand: true,
|
||||
encrypt: true,
|
||||
});
|
||||
expect(stringValue.startsWith("bnb:x:")).toBeTruthy();
|
||||
expect(stringValue).not.toContain(burn.system);
|
||||
expect(stringValue).not.toContain(burn.resource);
|
||||
expect(parse(stringValue)).toEqual(burn);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatForURL", () => {
|
||||
describe("external", () => {
|
||||
it("should be encrypted", () => {
|
||||
const burn = {
|
||||
system: "external",
|
||||
resource: "http://example.com/foo.jpg",
|
||||
};
|
||||
const formatted = formatForURL(burn);
|
||||
expect(formatted.startsWith("bnb:x:")).toBeTruthy();
|
||||
expect(formatted).not.toContain("http://example.com/foo.jpg");
|
||||
|
||||
expect(parse(formatted)).toEqual(burn);
|
||||
});
|
||||
});
|
||||
|
||||
describe("not external", () => {
|
||||
it("should be shorthand form", () => {
|
||||
expect(formatForURL({ system: "internal", resource: "foo" })).toEqual(
|
||||
"bnb:i:foo"
|
||||
);
|
||||
expect(
|
||||
formatForURL({ system: "subsonic", resource: "foo:bar" })
|
||||
).toEqual("bnb:s:foo:bar");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertSystem", () => {
|
||||
it("should fail if the system is not equal", () => {
|
||||
const burn = { system: "external", resource: "something"};
|
||||
expect(() => assertSystem(burn, "subsonic")).toThrow(`Unsupported urn: '${format(burn)}'`)
|
||||
});
|
||||
|
||||
it("should pass if the system is equal", () => {
|
||||
const burn = { system: "external", resource: "something"};
|
||||
expect(assertSystem(burn, "external")).toEqual(burn);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "./builders";
|
||||
import _ from "underscore";
|
||||
|
||||
|
||||
describe("InMemoryMusicService", () => {
|
||||
const service = new InMemoryMusicService();
|
||||
|
||||
@@ -51,25 +52,7 @@ describe("InMemoryMusicService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("artistToArtistSummary", () => {
|
||||
it("should map fields correctly", () => {
|
||||
const artist = anArtist({
|
||||
id: uuid(),
|
||||
name: "The Artist",
|
||||
image: {
|
||||
small: "/path/to/small/jpg",
|
||||
medium: "/path/to/medium/jpg",
|
||||
large: "/path/to/large/jpg",
|
||||
},
|
||||
});
|
||||
expect(artistToArtistSummary(artist)).toEqual({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Music Library", () => {
|
||||
const user = { username: "user100", password: "password100" };
|
||||
let musicLibrary: MusicLibrary;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Genre,
|
||||
Rating,
|
||||
} from "../src/music_service";
|
||||
import { BUrn } from "../src/burn";
|
||||
|
||||
export class InMemoryMusicService implements MusicService {
|
||||
users: Record<string, string> = {};
|
||||
@@ -131,8 +132,8 @@ export class InMemoryMusicService implements MusicService {
|
||||
),
|
||||
stream: (_: { trackId: string; range: string | undefined }) =>
|
||||
Promise.reject("unsupported operation"),
|
||||
coverArt: (id: string, size?: number) =>
|
||||
Promise.reject(`Cannot retrieve coverArt for ${id}, size ${size}`),
|
||||
coverArt: (coverArtURN: BUrn, size?: number) =>
|
||||
Promise.reject(`Cannot retrieve coverArt for ${coverArtURN}, size ${size}`),
|
||||
scrobble: async (_: string) => {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
|
||||
22
tests/music_service.test.ts
Normal file
22
tests/music_service.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { anArtist } from "./builders";
|
||||
import { artistToArtistSummary } from "../src/music_service";
|
||||
|
||||
describe("artistToArtistSummary", () => {
|
||||
it("should map fields correctly", () => {
|
||||
const artist = anArtist({
|
||||
id: uuid(),
|
||||
name: "The Artist",
|
||||
image: {
|
||||
system: "external",
|
||||
resource: "http://example.com:1234/image.jpg",
|
||||
},
|
||||
});
|
||||
expect(artistToArtistSummary(artist)).toEqual({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
image: artist.image,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import randomString from "../src/random_string";
|
||||
|
||||
describe('randomString', () => {
|
||||
it('should produce different strings...', () => {
|
||||
const s1 = randomString()
|
||||
const s2 = randomString()
|
||||
const s3 = randomString()
|
||||
const s4 = randomString()
|
||||
|
||||
expect(s1.length).toEqual(64)
|
||||
|
||||
expect(s1).not.toEqual(s2);
|
||||
expect(s1).not.toEqual(s3);
|
||||
expect(s1).not.toEqual(s4);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import url from "../src/url_builder";
|
||||
import i8n, { randomLang } from "../src/i8n";
|
||||
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
|
||||
import { Clock, SystemClock } from "../src/clock";
|
||||
import { formatForURL } from "../src/burn";
|
||||
|
||||
describe("rangeFilterFor", () => {
|
||||
describe("invalid range header string", () => {
|
||||
@@ -1190,7 +1191,7 @@ describe("server", () => {
|
||||
|
||||
describe("when there is no access-token", () => {
|
||||
it("should return a 401", async () => {
|
||||
const res = await request(server).get(`/art/coverArt:123/size/180`);
|
||||
const res = await request(server).get(`/art/${encodeURIComponent(formatForURL({ system: "subsonic", resource: "art:whatever" }))}/size/180`);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -1201,7 +1202,7 @@ describe("server", () => {
|
||||
now = now.add(1, "day");
|
||||
|
||||
const res = await request(server).get(
|
||||
`/art/coverArt:123/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL({ system: "subsonic", resource: "art:whatever" }))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
@@ -1209,14 +1210,16 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("when there is a valid access token", () => {
|
||||
describe("artist art", () => {
|
||||
describe("art", () => {
|
||||
["0", "-1", "foo"].forEach((size) => {
|
||||
describe(`invalid size of ${size}`, () => {
|
||||
it(`should return a 400`, async () => {
|
||||
const coverArtURN = { system: "subsonic", resource: "art:400" };
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/artist:${albumId}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1228,6 +1231,8 @@ describe("server", () => {
|
||||
describe("fetching a single image", () => {
|
||||
describe("when the images is available", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const coverArtURN = { system: "subsonic", resource: "art:200" };
|
||||
|
||||
const coverArt = coverArtResponse({});
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
@@ -1236,7 +1241,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/artist:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1247,7 +1252,7 @@ describe("server", () => {
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
`artist:${albumId}`,
|
||||
coverArtURN,
|
||||
180
|
||||
);
|
||||
});
|
||||
@@ -1255,13 +1260,14 @@ describe("server", () => {
|
||||
|
||||
describe("when the image is not available", () => {
|
||||
it("should return a 404", async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const coverArtURN = { system: "subsonic", resource: "art:404" };
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.coverArt.mockResolvedValue(undefined);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/artist:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1283,16 +1289,16 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 4 when all are available", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const ids = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"coverArt:3",
|
||||
"coverArt:4",
|
||||
];
|
||||
const urns = [
|
||||
"art:1",
|
||||
"art:2",
|
||||
"art:3",
|
||||
"art:4",
|
||||
].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
ids.forEach((_) => {
|
||||
urns.forEach((_) => {
|
||||
musicLibrary.coverArt.mockResolvedValueOnce(
|
||||
coverArtResponse({
|
||||
data: png,
|
||||
@@ -1302,7 +1308,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1312,8 +1318,8 @@ describe("server", () => {
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 200);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 200);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1324,7 +1330,7 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 4, however only 1 is available", () => {
|
||||
it("should return the single image", async () => {
|
||||
const ids = ["artist:1", "artist:2", "artist:3", "artist:4"];
|
||||
const urns = ["art:1", "art:2", "art:3", "art:4"].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1340,7 +1346,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1355,17 +1361,17 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 4 and all are missing", () => {
|
||||
it("should return a 404", async () => {
|
||||
const ids = ["artist:1", "artist:2", "artist:3", "artist:4"];
|
||||
const urns = ["art:1", "art:2", "art:3", "art:4"].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
ids.forEach((_) => {
|
||||
urns.forEach((_) => {
|
||||
musicLibrary.coverArt.mockResolvedValueOnce(undefined);
|
||||
});
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1377,7 +1383,7 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 9 when all are available", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const ids = [
|
||||
const urns = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"coverArt:3",
|
||||
@@ -1387,11 +1393,11 @@ describe("server", () => {
|
||||
"artist:7",
|
||||
"artist:8",
|
||||
"artist:9",
|
||||
];
|
||||
].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
ids.forEach((_) => {
|
||||
urns.forEach((_) => {
|
||||
musicLibrary.coverArt.mockResolvedValueOnce(
|
||||
coverArtResponse({
|
||||
data: png,
|
||||
@@ -1401,7 +1407,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1411,8 +1417,8 @@ describe("server", () => {
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1423,7 +1429,7 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 9 when only 2 are available", () => {
|
||||
it("should still return an image and a 200", async () => {
|
||||
const ids = [
|
||||
const urns = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"artist:3",
|
||||
@@ -1433,7 +1439,7 @@ describe("server", () => {
|
||||
"artist:7",
|
||||
"artist:8",
|
||||
"artist:9",
|
||||
];
|
||||
].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1457,7 +1463,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1467,8 +1473,8 @@ describe("server", () => {
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
urns.forEach((urn) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(urn, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1479,7 +1485,7 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 11", () => {
|
||||
it("should still return an image and a 200, though will only display 9", async () => {
|
||||
const ids = [
|
||||
const urns = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"artist:3",
|
||||
@@ -1491,11 +1497,11 @@ describe("server", () => {
|
||||
"artist:9",
|
||||
"artist:10",
|
||||
"artist:11",
|
||||
];
|
||||
].map(resource => ({ system:"subsonic", resource }));
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
ids.forEach((_) => {
|
||||
urns.forEach((_) => {
|
||||
musicLibrary.coverArt.mockResolvedValueOnce(
|
||||
coverArtResponse({
|
||||
data: png,
|
||||
@@ -1505,7 +1511,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
@@ -1515,8 +1521,8 @@ describe("server", () => {
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1527,13 +1533,14 @@ describe("server", () => {
|
||||
|
||||
describe("when the image is not available", () => {
|
||||
it("should return a 404", async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const coverArtURN = { system:"subsonic", resource:"art:404"};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.coverArt.mockResolvedValue(undefined);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/coverArt:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1558,83 +1565,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/coverArt:${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 = {
|
||||
status: 200,
|
||||
contentType: "image/jpeg",
|
||||
data: Buffer.from("some image", "ascii"),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.coverArt.mockResolvedValue(coverArt);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/coverArt:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(coverArt.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
coverArt.contentType
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
`coverArt:${albumId}`,
|
||||
180
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there isnt any", () => {
|
||||
it("should return a 404", async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.coverArt.mockResolvedValue(undefined);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/album:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is an error", () => {
|
||||
it("should return a 500", async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.coverArt.mockRejectedValue("Boooooom");
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/album:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
TRIP_HOP,
|
||||
PUNK,
|
||||
aPlaylist,
|
||||
anAlbumSummary,
|
||||
} from "./builders";
|
||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||
import supersoap from "./supersoap";
|
||||
@@ -56,6 +57,8 @@ import dayjs from "dayjs";
|
||||
import url, { URLBuilder } from "../src/url_builder";
|
||||
import { iconForGenre } from "../src/icon";
|
||||
import { jwtSigner } from "../src/encryption";
|
||||
import { formatForURL } from "../src/burn";
|
||||
import { range } from "underscore";
|
||||
|
||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||
|
||||
@@ -359,7 +362,7 @@ describe("track", () => {
|
||||
genre: { id: "genre101", name: "some genre" },
|
||||
}),
|
||||
artist: anArtist({ name: "great artist", id: uuid() }),
|
||||
coverArt: "coverArt:887766",
|
||||
coverArt: {system: "subsonic", resource: "887766"},
|
||||
rating: {
|
||||
love: true,
|
||||
stars: 5
|
||||
@@ -377,7 +380,7 @@ describe("track", () => {
|
||||
albumId: `album:${someTrack.album.id}`,
|
||||
albumArtist: someTrack.artist.name,
|
||||
albumArtistId: `artist:${someTrack.artist.id}`,
|
||||
albumArtURI: `http://localhost:4567/foo/art/${someTrack.coverArt}/size/180?access-token=1234`,
|
||||
albumArtURI: `http://localhost:4567/foo/art/${encodeURIComponent(formatForURL(someTrack.coverArt!))}/size/180?access-token=1234`,
|
||||
artist: someTrack.artist.name,
|
||||
artistId: `artist:${someTrack.artist.id}`,
|
||||
duration: someTrack.duration,
|
||||
@@ -431,6 +434,12 @@ describe("sonosifyMimeType", () => {
|
||||
});
|
||||
|
||||
describe("playlistAlbumArtURL", () => {
|
||||
const coverArt1 = { system: "subsonic", resource: "1" };
|
||||
const coverArt2 = { system: "subsonic", resource: "2" };
|
||||
const coverArt3 = { system: "subsonic", resource: "3" };
|
||||
const coverArt4 = { system: "subsonic", resource: "4" };
|
||||
const coverArt5 = { system: "subsonic", resource: "5" };
|
||||
|
||||
describe("when the playlist has no coverArt ids", () => {
|
||||
it("should return question mark icon", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
@@ -447,39 +456,97 @@ describe("playlistAlbumArtURL", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 2 distinct coverArt ids", () => {
|
||||
it("should return them on the url to the image", () => {
|
||||
describe("when the playlist has external ids", () => {
|
||||
it("should format the url with encrypted urn", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const externalArt1 = { system: "external", resource: "http://example.com/image1.jpg" };
|
||||
const externalArt2 = { system: "external", resource: "http://example.com/image2.jpg" };
|
||||
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: "1" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "1" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: externalArt1, album: anAlbumSummary({id: "album1"}) }),
|
||||
aTrack({ coverArt: externalArt2, album: anAlbumSummary({id: "album2"}) }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/1&2/size/180?search=yes`
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(formatForURL(externalArt1))}&${encodeURIComponent(formatForURL(externalArt2))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 4 distinct albumIds", () => {
|
||||
it("should return them on the url to the image", () => {
|
||||
describe("when the playlist has 4 tracks from 2 different albums, including some tracks that are missing coverArt urns", () => {
|
||||
it("should use the cover art once per album", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: "1" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "3" }),
|
||||
aTrack({ coverArt: "4" }),
|
||||
aTrack({ coverArt: undefined, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt1, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt2, album: anAlbumSummary({id: "album2" }) }),
|
||||
aTrack({ coverArt: undefined, album: anAlbumSummary({id: "album2" }) }),
|
||||
aTrack({ coverArt: coverArt3, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt4, album: anAlbumSummary({id: "album2" }) }),
|
||||
aTrack({ coverArt: undefined, album: anAlbumSummary({id: "album2" }) }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/1&2&3&4/size/180?search=yes`
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(formatForURL(coverArt1))}&${encodeURIComponent(formatForURL(coverArt2))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 4 tracks from 2 different albums", () => {
|
||||
it("should use the cover art once per album", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: coverArt1, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt2, album: anAlbumSummary({id: "album2" }) }),
|
||||
aTrack({ coverArt: coverArt3, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt4, album: anAlbumSummary({id: "album2" }) }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(formatForURL(coverArt1))}&${encodeURIComponent(formatForURL(coverArt2))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 4 tracks from 3 different albums", () => {
|
||||
it("should use the cover art once per album", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: coverArt1, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt2, album: anAlbumSummary({id: "album2" }) }),
|
||||
aTrack({ coverArt: coverArt3, album: anAlbumSummary({id: "album1" }) }),
|
||||
aTrack({ coverArt: coverArt4, album: anAlbumSummary({id: "album3" }) }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(formatForURL(coverArt1))}&${encodeURIComponent(formatForURL(coverArt2))}&${encodeURIComponent(formatForURL(coverArt4))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the playlist has 4 tracks from 4 different albums", () => {
|
||||
it("should return them on the url to the image", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: coverArt1, album: anAlbumSummary({id: "album1"} ) }),
|
||||
aTrack({ coverArt: coverArt2, album: anAlbumSummary({id: "album2"} ) }),
|
||||
aTrack({ coverArt: coverArt3, album: anAlbumSummary({id: "album3"} ) }),
|
||||
aTrack({ coverArt: coverArt4, album: anAlbumSummary({id: "album4"} ) }),
|
||||
aTrack({ coverArt: coverArt5, album: anAlbumSummary({id: "album1"} ) }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/${encodeURIComponent(formatForURL(coverArt1))}&${encodeURIComponent(formatForURL(coverArt2))}&${encodeURIComponent(formatForURL(coverArt3))}&${encodeURIComponent(formatForURL(coverArt4))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -489,24 +556,23 @@ describe("playlistAlbumArtURL", () => {
|
||||
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||
const playlist = aPlaylist({
|
||||
entries: [
|
||||
aTrack({ coverArt: "1" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "2" }),
|
||||
aTrack({ coverArt: "3" }),
|
||||
aTrack({ coverArt: "4" }),
|
||||
aTrack({ coverArt: "5" }),
|
||||
aTrack({ coverArt: "6" }),
|
||||
aTrack({ coverArt: "7" }),
|
||||
aTrack({ coverArt: "8" }),
|
||||
aTrack({ coverArt: "9" }),
|
||||
aTrack({ coverArt: "10" }),
|
||||
aTrack({ coverArt: "11" }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "1" }, album: anAlbumSummary({ id:"1" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "2" }, album: anAlbumSummary({ id:"2" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "3" }, album: anAlbumSummary({ id:"3" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "4" }, album: anAlbumSummary({ id:"4" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "5" }, album: anAlbumSummary({ id:"5" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "6" }, album: anAlbumSummary({ id:"6" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "7" }, album: anAlbumSummary({ id:"7" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "8" }, album: anAlbumSummary({ id:"8" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "9" }, album: anAlbumSummary({ id:"9" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "10" }, album: anAlbumSummary({ id:"10" }) }),
|
||||
aTrack({ coverArt: { system: "subsonic", resource: "11" }, album: anAlbumSummary({ id:"11" }) }),
|
||||
],
|
||||
});
|
||||
|
||||
const burns = range(1, 10).map(i => encodeURIComponent(formatForURL({ system: "subsonic", resource: `${i}` }))).join("&")
|
||||
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
|
||||
`http://localhost:1234/context-path/art/1&2&3&4&5&6&7&8&9/size/180?search=yes`
|
||||
`http://localhost:1234/context-path/art/${burns}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -518,15 +584,32 @@ describe("defaultAlbumArtURI", () => {
|
||||
);
|
||||
|
||||
describe("when there is an album coverArt", () => {
|
||||
it("should use it in the image url", () => {
|
||||
expect(
|
||||
defaultAlbumArtURI(
|
||||
bonobUrl,
|
||||
anAlbum({ coverArt: "coverArt:123" })
|
||||
).href()
|
||||
).toEqual(
|
||||
"http://bonob.example.com:8080/context/art/coverArt:123/size/180?search=yes"
|
||||
);
|
||||
describe("from subsonic", () => {
|
||||
it("should use it", () => {
|
||||
const coverArt = { system: "subsonic", resource: "12345" }
|
||||
expect(
|
||||
defaultAlbumArtURI(
|
||||
bonobUrl,
|
||||
anAlbum({ coverArt })
|
||||
).href()
|
||||
).toEqual(
|
||||
`http://bonob.example.com:8080/context/art/${encodeURIComponent(formatForURL(coverArt))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("that is external", () => {
|
||||
it("should use encrypt it", () => {
|
||||
const coverArt = { system: "external", resource: "http://example.com/someimage.jpg" }
|
||||
expect(
|
||||
defaultAlbumArtURI(
|
||||
bonobUrl,
|
||||
anAlbum({ coverArt })
|
||||
).href()
|
||||
).toEqual(
|
||||
`http://bonob.example.com:8080/context/art/${encodeURIComponent(formatForURL(coverArt))}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -542,13 +625,39 @@ describe("defaultAlbumArtURI", () => {
|
||||
});
|
||||
|
||||
describe("defaultArtistArtURI", () => {
|
||||
it("should create the correct URI", () => {
|
||||
const bonobUrl = url("http://localhost:1234/something?s=123");
|
||||
const artist = anArtist();
|
||||
describe("when the artist has no image", () => {
|
||||
it("should return an icon", () => {
|
||||
const bonobUrl = url("http://localhost:1234/something?s=123");
|
||||
const artist = anArtist({ image: undefined });
|
||||
|
||||
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||
`http://localhost:1234/something/icon/vinyl/size/legacy?s=123`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||
`http://localhost:1234/something/art/artist:${artist.id}/size/180?s=123`
|
||||
);
|
||||
describe("when the resource is subsonic", () => {
|
||||
it("should use the resource", () => {
|
||||
const bonobUrl = url("http://localhost:1234/something?s=123");
|
||||
const image = { system:"subsonic", resource: "art:1234"};
|
||||
const artist = anArtist({ image });
|
||||
|
||||
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||
`http://localhost:1234/something/art/${encodeURIComponent(formatForURL(image))}/size/180?s=123`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the resource is external", () => {
|
||||
it("should encrypt the resource", () => {
|
||||
const bonobUrl = url("http://localhost:1234/something?s=123");
|
||||
const image = { system:"external", resource: "http://example.com/something.jpg"};
|
||||
const artist = anArtist({ image });
|
||||
|
||||
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||
`http://localhost:1234/something/art/${encodeURIComponent(formatForURL(image))}/size/180?s=123`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user