mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Use bat query param rather than header when streaming as headers not passed in HEAD requests from sonos. Improve handling of failures when fetching coverArt to return undefined rather than throwing exception (#59)
This commit is contained in:
@@ -291,12 +291,14 @@ function server(
|
||||
app.get("/stream/track/:id", async (req, res) => {
|
||||
const id = req.params["id"]!;
|
||||
const trace = uuid();
|
||||
|
||||
|
||||
logger.info(
|
||||
`${trace} bnb<- ${req.method} /stream/track/${id}, headers=${JSON.stringify(req.headers)}`
|
||||
`${trace} bnb<- ${req.method} ${req.path}?${
|
||||
JSON.stringify(req.query)
|
||||
}, headers=${JSON.stringify(req.headers)}`
|
||||
);
|
||||
const authToken = pipe(
|
||||
req.header(BONOB_ACCESS_TOKEN_HEADER),
|
||||
req.query[BONOB_ACCESS_TOKEN_HEADER] as string,
|
||||
O.fromNullable,
|
||||
O.map((accessToken) => accessTokens.authTokenFor(accessToken)),
|
||||
O.getOrElseW(() => undefined)
|
||||
@@ -340,9 +342,9 @@ function server(
|
||||
sendStream: boolean;
|
||||
}) => {
|
||||
logger.info(
|
||||
`${trace} bnb-> /stream/track/${id}, status=${status}, headers=${JSON.stringify(
|
||||
headers
|
||||
)}`
|
||||
`${trace} bnb-> ${
|
||||
req.path
|
||||
}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||
);
|
||||
res.status(status);
|
||||
Object.entries(headers)
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Track,
|
||||
} from "./music_service";
|
||||
import { AccessTokens } from "./access_tokens";
|
||||
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
|
||||
import { Clock } from "./clock";
|
||||
import { URLBuilder } from "./url_builder";
|
||||
import { asLANGs, I8N } from "./i8n";
|
||||
@@ -404,14 +403,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
getMediaURIResult: bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/${type}/${typeId}`,
|
||||
searchParams: { "bat": accessToken }
|
||||
})
|
||||
.href(),
|
||||
httpHeaders: [
|
||||
{
|
||||
header: BONOB_ACCESS_TOKEN_HEADER,
|
||||
value: accessToken,
|
||||
},
|
||||
],
|
||||
})),
|
||||
getMediaMetadata: async (
|
||||
{ id }: { id: string },
|
||||
|
||||
175
src/subsonic.ts
175
src/subsonic.ts
@@ -27,6 +27,7 @@ import axios, { AxiosRequestConfig } from "axios";
|
||||
import { Encryption } from "./encryption";
|
||||
import randomString from "./random_string";
|
||||
import { b64Encode, b64Decode } from "./b64";
|
||||
import logger from "./logger";
|
||||
|
||||
export const BROWSER_HEADERS = {
|
||||
accept:
|
||||
@@ -225,8 +226,8 @@ export function isError(
|
||||
}
|
||||
|
||||
export const splitCoverArtId = (coverArt: string): [string, string] => {
|
||||
const parts = coverArt.split(":").filter(it => it.length > 0);
|
||||
if(parts.length < 2) throw `'${coverArt}' is an invalid coverArt id'`
|
||||
const parts = coverArt.split(":").filter((it) => it.length > 0);
|
||||
if (parts.length < 2) throw `'${coverArt}' is an invalid coverArt id'`;
|
||||
return [parts[0]!, parts.slice(1).join(":")];
|
||||
};
|
||||
|
||||
@@ -246,7 +247,8 @@ export type getAlbumListParams = {
|
||||
|
||||
export const MAX_ALBUM_LIST = 500;
|
||||
|
||||
const maybeAsCoverArt = (coverArt: string | undefined) => coverArt ? `coverArt:${coverArt}` : undefined
|
||||
const maybeAsCoverArt = (coverArt: string | undefined) =>
|
||||
coverArt ? `coverArt:${coverArt}` : undefined;
|
||||
|
||||
const asTrack = (album: Album, song: song) => ({
|
||||
id: song._id,
|
||||
@@ -270,7 +272,7 @@ const asAlbum = (album: album) => ({
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
coverArt: maybeAsCoverArt(album._coverArt),
|
||||
});
|
||||
|
||||
export const asGenre = (genreName: string) => ({
|
||||
@@ -438,7 +440,7 @@ export class Subsonic implements MusicService {
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
coverArt: maybeAsCoverArt(album._coverArt),
|
||||
}));
|
||||
|
||||
getArtist = (
|
||||
@@ -492,7 +494,7 @@ export class Subsonic implements MusicService {
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
coverArt: maybeAsCoverArt(album._coverArt),
|
||||
}));
|
||||
|
||||
search3 = (credentials: Credentials, q: any) =>
|
||||
@@ -508,12 +510,12 @@ export class Subsonic implements MusicService {
|
||||
}));
|
||||
|
||||
async login(token: string) {
|
||||
const navidrome = this;
|
||||
const subsonic = this;
|
||||
const credentials: Credentials = this.parseToken(token);
|
||||
|
||||
const musicLibrary: MusicLibrary = {
|
||||
artists: (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getArtists(credentials)
|
||||
.then(slice2(q))
|
||||
.then(([page, total]) => ({
|
||||
@@ -521,15 +523,15 @@ export class Subsonic implements MusicService {
|
||||
results: page.map((it) => ({ id: it.id, name: it.name })),
|
||||
})),
|
||||
artist: async (id: string): Promise<Artist> =>
|
||||
navidrome.getArtistWithInfo(credentials, id),
|
||||
subsonic.getArtistWithInfo(credentials, id),
|
||||
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
||||
Promise.all([
|
||||
navidrome
|
||||
subsonic
|
||||
.getArtists(credentials)
|
||||
.then((it) =>
|
||||
_.inject(it, (total, artist) => total + artist.albumCount, 0)
|
||||
),
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
|
||||
type: q.type,
|
||||
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
|
||||
@@ -537,15 +539,14 @@ export class Subsonic implements MusicService {
|
||||
offset: q._index,
|
||||
})
|
||||
.then((response) => response.albumList2.album || [])
|
||||
.then(navidrome.toAlbumSummary),
|
||||
.then(subsonic.toAlbumSummary),
|
||||
]).then(([total, albums]) => ({
|
||||
results: albums.slice(0, q._count),
|
||||
total: albums.length == 500 ? total : q._index + albums.length,
|
||||
})),
|
||||
album: (id: string): Promise<Album> =>
|
||||
navidrome.getAlbum(credentials, id),
|
||||
album: (id: string): Promise<Album> => subsonic.getAlbum(credentials, id),
|
||||
genres: () =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetGenresResponse>(credentials, "/rest/getGenres")
|
||||
.then((it) =>
|
||||
pipe(
|
||||
@@ -557,7 +558,7 @@ export class Subsonic implements MusicService {
|
||||
)
|
||||
),
|
||||
tracks: (albumId: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", {
|
||||
id: albumId,
|
||||
})
|
||||
@@ -565,7 +566,7 @@ export class Subsonic implements MusicService {
|
||||
.then((album) =>
|
||||
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
||||
),
|
||||
track: (trackId: string) => navidrome.getTrack(credentials, trackId),
|
||||
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
|
||||
stream: async ({
|
||||
trackId,
|
||||
range,
|
||||
@@ -573,8 +574,8 @@ export class Subsonic implements MusicService {
|
||||
trackId: string;
|
||||
range: string | undefined;
|
||||
}) =>
|
||||
navidrome.getTrack(credentials, trackId).then((track) =>
|
||||
navidrome
|
||||
subsonic.getTrack(credentials, trackId).then((track) =>
|
||||
subsonic
|
||||
.get(
|
||||
credentials,
|
||||
`/rest/stream`,
|
||||
@@ -611,51 +612,69 @@ export class Subsonic implements MusicService {
|
||||
coverArt: async (coverArt: string, size?: number) => {
|
||||
const [type, id] = splitCoverArtId(coverArt);
|
||||
if (type == "coverArt") {
|
||||
return navidrome.getCoverArt(credentials, id, size).then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
}));
|
||||
} else {
|
||||
return navidrome.getArtistWithInfo(credentials, id).then((artist) => {
|
||||
const albumsWithCoverArt = artist.albums.filter(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)
|
||||
.resize(size)
|
||||
.toBuffer()
|
||||
.then((resized) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: resized,
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
contentType: res.headers["content-type"],
|
||||
data: image,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else if (albumsWithCoverArt.length > 0) {
|
||||
return navidrome
|
||||
.getCoverArt(credentials, splitCoverArtId(albumsWithCoverArt[0]!.coverArt!)[1], size)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
}));
|
||||
} else {
|
||||
return subsonic
|
||||
.getCoverArt(credentials, id, size)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
}))
|
||||
.catch((e) => {
|
||||
logger.error(`Failed getting coverArt ${coverArt}: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return subsonic
|
||||
.getArtistWithInfo(credentials, id)
|
||||
.then((artist) => {
|
||||
const albumsWithCoverArt = artist.albums.filter(
|
||||
(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)
|
||||
.resize(size)
|
||||
.toBuffer()
|
||||
.then((resized) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: resized,
|
||||
}));
|
||||
} else {
|
||||
return {
|
||||
contentType: res.headers["content-type"],
|
||||
data: image,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else if (albumsWithCoverArt.length > 0) {
|
||||
return subsonic
|
||||
.getCoverArt(
|
||||
credentials,
|
||||
splitCoverArtId(albumsWithCoverArt[0]!.coverArt!)[1],
|
||||
size
|
||||
)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
}));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(`Failed getting coverArt ${coverArt}: ${e}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
},
|
||||
scrobble: async (id: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.get(credentials, `/rest/scrobble`, {
|
||||
id,
|
||||
submission: true,
|
||||
@@ -663,7 +682,7 @@ export class Subsonic implements MusicService {
|
||||
.then((_) => true)
|
||||
.catch(() => false),
|
||||
nowPlaying: async (id: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.get(credentials, `/rest/scrobble`, {
|
||||
id,
|
||||
submission: false,
|
||||
@@ -671,7 +690,7 @@ export class Subsonic implements MusicService {
|
||||
.then((_) => true)
|
||||
.catch(() => false),
|
||||
searchArtists: async (query: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.search3(credentials, { query, artistCount: 20 })
|
||||
.then(({ artists }) =>
|
||||
artists.map((artist) => ({
|
||||
@@ -680,26 +699,26 @@ export class Subsonic implements MusicService {
|
||||
}))
|
||||
),
|
||||
searchAlbums: async (query: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.search3(credentials, { query, albumCount: 20 })
|
||||
.then(({ albums }) => navidrome.toAlbumSummary(albums)),
|
||||
.then(({ albums }) => subsonic.toAlbumSummary(albums)),
|
||||
searchTracks: async (query: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.search3(credentials, { query, songCount: 20 })
|
||||
.then(({ songs }) =>
|
||||
Promise.all(
|
||||
songs.map((it) => navidrome.getTrack(credentials, it._id))
|
||||
songs.map((it) => subsonic.getTrack(credentials, it._id))
|
||||
)
|
||||
),
|
||||
playlists: async () =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
||||
.then((it) => it.playlists.playlist || [])
|
||||
.then((playlists) =>
|
||||
playlists.map((it) => ({ id: it._id, name: it._name }))
|
||||
),
|
||||
playlist: async (id: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
||||
id,
|
||||
})
|
||||
@@ -724,7 +743,7 @@ export class Subsonic implements MusicService {
|
||||
genre: maybeAsGenre(entry._genre),
|
||||
artistName: entry._artist,
|
||||
artistId: entry._artistId,
|
||||
coverArt: maybeAsCoverArt(entry._coverArt)
|
||||
coverArt: maybeAsCoverArt(entry._coverArt),
|
||||
},
|
||||
artist: {
|
||||
id: entry._artistId,
|
||||
@@ -734,34 +753,34 @@ export class Subsonic implements MusicService {
|
||||
};
|
||||
}),
|
||||
createPlaylist: async (name: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/createPlaylist", {
|
||||
name,
|
||||
})
|
||||
.then((it) => it.playlist)
|
||||
.then((it) => ({ id: it._id, name: it._name })),
|
||||
deletePlaylist: async (id: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
||||
id,
|
||||
})
|
||||
.then((_) => true),
|
||||
addToPlaylist: async (playlistId: string, trackId: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
||||
playlistId,
|
||||
songIdToAdd: trackId,
|
||||
})
|
||||
.then((_) => true),
|
||||
removeFromPlaylist: async (playlistId: string, indicies: number[]) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
||||
playlistId,
|
||||
songIndexToRemove: indicies,
|
||||
})
|
||||
.then((_) => true),
|
||||
similarSongs: async (id: string) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getJSON<GetSimilarSongsResponse>(
|
||||
credentials,
|
||||
"/rest/getSimilarSongs2",
|
||||
@@ -771,15 +790,15 @@ export class Subsonic implements MusicService {
|
||||
.then((songs) =>
|
||||
Promise.all(
|
||||
songs.map((song) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getAlbum(credentials, song._albumId)
|
||||
.then((album) => asTrack(album, song))
|
||||
)
|
||||
)
|
||||
),
|
||||
topSongs: async (artistId: string) =>
|
||||
navidrome.getArtist(credentials, artistId).then(({ name }) =>
|
||||
navidrome
|
||||
subsonic.getArtist(credentials, artistId).then(({ name }) =>
|
||||
subsonic
|
||||
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
|
||||
artist: name,
|
||||
count: 50,
|
||||
@@ -788,7 +807,7 @@ export class Subsonic implements MusicService {
|
||||
.then((songs) =>
|
||||
Promise.all(
|
||||
songs.map((song) =>
|
||||
navidrome
|
||||
subsonic
|
||||
.getAlbum(credentials, song._albumId)
|
||||
.then((album) => asTrack(album, song))
|
||||
)
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("server", () => {
|
||||
bonobUrl,
|
||||
new InMemoryMusicService(),
|
||||
{
|
||||
version: "v123.456"
|
||||
version: "v123.456",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -230,49 +230,48 @@ describe("server", () => {
|
||||
name: "bonobMissing",
|
||||
sid: 88,
|
||||
});
|
||||
|
||||
|
||||
const fakeSonos: Sonos = {
|
||||
devices: () => Promise.resolve([]),
|
||||
services: () =>
|
||||
Promise.resolve([]),
|
||||
services: () => Promise.resolve([]),
|
||||
remove: () => Promise.resolve(false),
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
|
||||
const server = makeServer(
|
||||
fakeSonos,
|
||||
missingBonobService,
|
||||
bonobUrl,
|
||||
new InMemoryMusicService()
|
||||
);
|
||||
|
||||
|
||||
describe("devices list", () => {
|
||||
it("should be empty", async () => {
|
||||
const res = await request(server)
|
||||
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(0\)</h2>`);
|
||||
expect(res.text).not.toMatch(/class=device/);
|
||||
expect(res.text).toContain(lang("noSonosDevices"));
|
||||
expect(res.text).toContain(lang("noSonosDevices"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("services", () => {
|
||||
it("should be empty", async () => {
|
||||
const res = await request(server)
|
||||
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("services")} \(0\)</h2>`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("there are 2 devices and bonob is not registered", () => {
|
||||
const service1 = aService({
|
||||
name: "s1",
|
||||
@@ -294,19 +293,19 @@ describe("server", () => {
|
||||
name: "bonobMissing",
|
||||
sid: 88,
|
||||
});
|
||||
|
||||
|
||||
const device1: Device = aDevice({
|
||||
name: "device1",
|
||||
ip: "172.0.0.1",
|
||||
port: 4301,
|
||||
});
|
||||
|
||||
|
||||
const device2: Device = aDevice({
|
||||
name: "device2",
|
||||
ip: "172.0.0.2",
|
||||
port: 4302,
|
||||
});
|
||||
|
||||
|
||||
const fakeSonos: Sonos = {
|
||||
devices: () => Promise.resolve([device1, device2]),
|
||||
services: () =>
|
||||
@@ -314,35 +313,35 @@ describe("server", () => {
|
||||
remove: () => Promise.resolve(false),
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
|
||||
const server = makeServer(
|
||||
fakeSonos,
|
||||
missingBonobService,
|
||||
bonobUrl,
|
||||
new InMemoryMusicService()
|
||||
);
|
||||
|
||||
|
||||
describe("devices list", () => {
|
||||
it("should contain the devices returned from sonos", async () => {
|
||||
const res = await request(server)
|
||||
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("devices")} \(2\)</h2>`);
|
||||
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
||||
expect(res.text).toMatch(/device2\s+\(172.0.0.2:4302\)/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("services", () => {
|
||||
it("should contain a list of services returned from sonos", async () => {
|
||||
const res = await request(server)
|
||||
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<h2>${lang("services")} \(4\)</h2>`);
|
||||
expect(res.text).toMatch(/s1\s+\(1\)/);
|
||||
@@ -351,7 +350,7 @@ describe("server", () => {
|
||||
expect(res.text).toMatch(/s4\s+\(4\)/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("registration status", () => {
|
||||
it("should be not-registered", async () => {
|
||||
const res = await request(server)
|
||||
@@ -372,10 +371,10 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("there are 2 devices and bonob is registered", () => {
|
||||
const service1 = aService();
|
||||
|
||||
|
||||
const service2 = aService();
|
||||
|
||||
const device1: Device = aDevice({
|
||||
@@ -383,32 +382,33 @@ describe("server", () => {
|
||||
ip: "172.0.0.1",
|
||||
port: 4301,
|
||||
});
|
||||
|
||||
|
||||
const device2: Device = aDevice({
|
||||
name: "device2",
|
||||
ip: "172.0.0.2",
|
||||
port: 4302,
|
||||
});
|
||||
|
||||
|
||||
const bonobService = aService({
|
||||
name: "bonobNotMissing",
|
||||
sid: 99,
|
||||
});
|
||||
|
||||
|
||||
const fakeSonos: Sonos = {
|
||||
devices: () => Promise.resolve([device1, device2]),
|
||||
services: () => Promise.resolve([service1, service2, bonobService]),
|
||||
services: () =>
|
||||
Promise.resolve([service1, service2, bonobService]),
|
||||
remove: () => Promise.resolve(false),
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
|
||||
const server = makeServer(
|
||||
fakeSonos,
|
||||
bonobService,
|
||||
bonobUrl,
|
||||
new InMemoryMusicService()
|
||||
);
|
||||
|
||||
|
||||
describe("registration status", () => {
|
||||
it("should be registered", async () => {
|
||||
const res = await request(server)
|
||||
@@ -755,13 +755,14 @@ describe("server", () => {
|
||||
it("should return a 401", async () => {
|
||||
now = now.add(1, "day");
|
||||
|
||||
const res = await request(server)
|
||||
.head(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
const res = await request(server).head(
|
||||
bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/track/${trackId}`,
|
||||
searchParams: { bat: accessToken },
|
||||
})
|
||||
.path()
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -786,10 +787,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.head(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(trackStream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
@@ -812,8 +812,10 @@ describe("server", () => {
|
||||
musicLibrary.stream.mockResolvedValue(trackStream);
|
||||
|
||||
const res = await request(server)
|
||||
.head(`/stream/track/${trackId}`)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.head(bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
expect(res.body).toEqual({});
|
||||
@@ -840,10 +842,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -863,10 +864,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
|
||||
@@ -894,10 +894,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
@@ -933,10 +932,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
@@ -970,10 +968,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
@@ -1008,10 +1005,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
@@ -1051,10 +1047,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
||||
.set("Range", requestedRange);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
@@ -1093,10 +1088,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
||||
.set("Range", "4000-5000");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
@@ -1242,11 +1236,24 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("fetching multiple images as a collage", () => {
|
||||
const png = fs.readFileSync(path.join(__dirname, '..', 'docs', 'images', 'chartreuseFuchsia.png'));
|
||||
const png = fs.readFileSync(
|
||||
path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"docs",
|
||||
"images",
|
||||
"chartreuseFuchsia.png"
|
||||
)
|
||||
);
|
||||
|
||||
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 ids = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"coverArt:3",
|
||||
"coverArt:4",
|
||||
];
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1258,10 +1265,11 @@ describe("server", () => {
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${ids.join("&")}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${ids.join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1270,10 +1278,7 @@ describe("server", () => {
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
id,
|
||||
200
|
||||
);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 200);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1294,7 +1299,7 @@ describe("server", () => {
|
||||
musicLibrary.coverArt.mockResolvedValueOnce(
|
||||
coverArtResponse({
|
||||
data: png,
|
||||
contentType: "image/some-mime-type"
|
||||
contentType: "image/some-mime-type",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1307,10 +1312,12 @@ describe("server", () => {
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual("image/some-mime-type");
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
"image/some-mime-type"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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"];
|
||||
@@ -1335,7 +1342,17 @@ describe("server", () => {
|
||||
|
||||
describe("fetching a collage of 9 when all are available", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const ids = ["artist:1", "artist:2", "coverArt:3", "artist:4", "artist:5", "artist:6", "artist:7", "artist:8", "artist:9"];
|
||||
const ids = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"coverArt:3",
|
||||
"artist:4",
|
||||
"artist:5",
|
||||
"artist:6",
|
||||
"artist:7",
|
||||
"artist:8",
|
||||
"artist:9",
|
||||
];
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1360,10 +1377,7 @@ describe("server", () => {
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
id,
|
||||
180
|
||||
);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
@@ -1374,7 +1388,17 @@ 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 = ["artist:1", "artist:2", "artist:3", "artist:4", "artist:5", "artist:6", "artist:7", "artist:8", "artist:9"];
|
||||
const ids = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"artist:3",
|
||||
"artist:4",
|
||||
"artist:5",
|
||||
"artist:6",
|
||||
"artist:7",
|
||||
"artist:8",
|
||||
"artist:9",
|
||||
];
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1409,21 +1433,30 @@ describe("server", () => {
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
id,
|
||||
180
|
||||
);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
expect(image.width).toEqual(180);
|
||||
expect(image.height).toEqual(180);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("fetching a collage of 11", () => {
|
||||
it("should still return an image and a 200, though will only display 9", async () => {
|
||||
const ids = ["artist:1", "artist:2", "artist:3", "artist:4", "artist:5", "artist:6", "artist:7", "artist:8", "artist:9", "artist:10", "artist:11"];
|
||||
const ids = [
|
||||
"artist:1",
|
||||
"artist:2",
|
||||
"artist:3",
|
||||
"artist:4",
|
||||
"artist:5",
|
||||
"artist:6",
|
||||
"artist:7",
|
||||
"artist:8",
|
||||
"artist:9",
|
||||
"artist:10",
|
||||
"artist:11",
|
||||
];
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
|
||||
@@ -1448,17 +1481,14 @@ describe("server", () => {
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
ids.forEach((id) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
id,
|
||||
180
|
||||
);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||
});
|
||||
|
||||
const image = await Image.load(res.body);
|
||||
expect(image.width).toEqual(180);
|
||||
expect(image.height).toEqual(180);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the image is not available", () => {
|
||||
it("should return a 404", async () => {
|
||||
@@ -1692,12 +1722,18 @@ describe("server", () => {
|
||||
expect(svg).toContain(`fill="brightpink"`);
|
||||
});
|
||||
|
||||
function itShouldBeFestive(theme: string, date: string, id: string, color1: string, color2: string) {
|
||||
function itShouldBeFestive(
|
||||
theme: string,
|
||||
date: string,
|
||||
id: string,
|
||||
color1: string,
|
||||
color2: string
|
||||
) {
|
||||
it(`should return a ${theme} icon on ${date}`, async () => {
|
||||
const response = await request(
|
||||
server({ now: () => dayjs(date) })
|
||||
).get(`/icon/${type}/size/180`);
|
||||
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
const svg = Buffer.from(response.body).toString();
|
||||
expect(svg).toContain(`id="${id}"`);
|
||||
@@ -1706,14 +1742,50 @@ describe("server", () => {
|
||||
});
|
||||
}
|
||||
|
||||
itShouldBeFestive("christmas '22", "2022/12/25", "christmas", "red", "green")
|
||||
itShouldBeFestive("christmas '23", "2023/12/25", "christmas", "red", "green")
|
||||
itShouldBeFestive(
|
||||
"christmas '22",
|
||||
"2022/12/25",
|
||||
"christmas",
|
||||
"red",
|
||||
"green"
|
||||
);
|
||||
itShouldBeFestive(
|
||||
"christmas '23",
|
||||
"2023/12/25",
|
||||
"christmas",
|
||||
"red",
|
||||
"green"
|
||||
);
|
||||
|
||||
itShouldBeFestive("halloween", "2022/10/31", "halloween", "black", "orange")
|
||||
itShouldBeFestive("halloween", "2023/10/31", "halloween", "black", "orange")
|
||||
itShouldBeFestive(
|
||||
"halloween",
|
||||
"2022/10/31",
|
||||
"halloween",
|
||||
"black",
|
||||
"orange"
|
||||
);
|
||||
itShouldBeFestive(
|
||||
"halloween",
|
||||
"2023/10/31",
|
||||
"halloween",
|
||||
"black",
|
||||
"orange"
|
||||
);
|
||||
|
||||
itShouldBeFestive("cny '22", "2022/02/01", "yoTiger", "red", "yellow")
|
||||
itShouldBeFestive("cny '23", "2023/01/22", "yoRabbit", "red", "yellow")
|
||||
itShouldBeFestive(
|
||||
"cny '22",
|
||||
"2022/02/01",
|
||||
"yoTiger",
|
||||
"red",
|
||||
"yellow"
|
||||
);
|
||||
itShouldBeFestive(
|
||||
"cny '23",
|
||||
"2023/01/22",
|
||||
"yoRabbit",
|
||||
"red",
|
||||
"yellow"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2471,12 +2471,9 @@ describe("api", () => {
|
||||
getMediaURIResult: bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/track/${trackId}`,
|
||||
searchParams: { "bat": accessToken }
|
||||
})
|
||||
.href(),
|
||||
httpHeaders: {
|
||||
header: "bat",
|
||||
value: accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
|
||||
@@ -184,7 +184,8 @@ const getArtistInfoXml = (
|
||||
</artistInfo2>
|
||||
</subsonic-response>`;
|
||||
|
||||
const maybeIdFromCoverArtId = (coverArt: string | undefined) => coverArt ? splitCoverArtId(coverArt)[1] : "";
|
||||
const maybeIdFromCoverArtId = (coverArt: string | undefined) =>
|
||||
coverArt ? splitCoverArtId(coverArt)[1] : "";
|
||||
|
||||
const albumXml = (
|
||||
artist: Artist,
|
||||
@@ -437,15 +438,21 @@ const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="
|
||||
|
||||
describe("splitCoverArtId", () => {
|
||||
it("should split correctly", () => {
|
||||
expect(splitCoverArtId("foo:bar")).toEqual(["foo", "bar"])
|
||||
expect(splitCoverArtId("foo:bar:car:jar")).toEqual(["foo", "bar:car:jar"])
|
||||
expect(splitCoverArtId("foo:bar")).toEqual(["foo", "bar"]);
|
||||
expect(splitCoverArtId("foo:bar:car:jar")).toEqual(["foo", "bar:car:jar"]);
|
||||
});
|
||||
|
||||
it("should blow up when the id is invalid", () => {
|
||||
expect(() => splitCoverArtId("")).toThrow(`'' is an invalid coverArt id`)
|
||||
expect(() => splitCoverArtId("foo:")).toThrow(`'foo:' is an invalid coverArt id`)
|
||||
expect(() => splitCoverArtId("foo:")).toThrow(`'foo:' is an invalid coverArt id`)
|
||||
expect(() => splitCoverArtId(":dog")).toThrow(`':dog' is an invalid coverArt id`)
|
||||
expect(() => splitCoverArtId("")).toThrow(`'' is an invalid coverArt id`);
|
||||
expect(() => splitCoverArtId("foo:")).toThrow(
|
||||
`'foo:' is an invalid coverArt id`
|
||||
);
|
||||
expect(() => splitCoverArtId("foo:")).toThrow(
|
||||
`'foo:' is an invalid coverArt id`
|
||||
);
|
||||
expect(() => splitCoverArtId(":dog")).toThrow(
|
||||
`':dog' is an invalid coverArt id`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1677,17 +1684,10 @@ describe("Subsonic", () => {
|
||||
|
||||
// the artists have 5 albums in the getArtists endpoint
|
||||
const artist1 = anArtist({
|
||||
albums: [
|
||||
album1,
|
||||
album2,
|
||||
album3,
|
||||
album4,
|
||||
],
|
||||
albums: [album1, album2, album3, album4],
|
||||
});
|
||||
const artist2 = anArtist({
|
||||
albums: [
|
||||
album5,
|
||||
],
|
||||
albums: [album5],
|
||||
});
|
||||
const artists = [artist1, artist2];
|
||||
|
||||
@@ -1713,7 +1713,7 @@ describe("Subsonic", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should return the page of albums, updating the total to be accurate", async () => {
|
||||
const q: AlbumQuery = {
|
||||
_index: 0,
|
||||
@@ -1727,12 +1727,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
album3,
|
||||
album5,
|
||||
],
|
||||
results: [album1, album2, album3, album5],
|
||||
total: 4,
|
||||
});
|
||||
|
||||
@@ -1741,15 +1736,18 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1775,7 +1773,7 @@ describe("Subsonic", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should filter out the pre-fetched albums", async () => {
|
||||
const q: AlbumQuery = {
|
||||
_index: 0,
|
||||
@@ -1789,10 +1787,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
],
|
||||
results: [album1, album2],
|
||||
total: 4,
|
||||
});
|
||||
|
||||
@@ -1801,17 +1796,20 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the query is for the last page only", () => {
|
||||
beforeEach(() => {
|
||||
@@ -1834,7 +1832,7 @@ describe("Subsonic", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should return the last page of albums, updating the total to be accurate", async () => {
|
||||
const q: AlbumQuery = {
|
||||
_index: 2,
|
||||
@@ -1848,10 +1846,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album3,
|
||||
album5,
|
||||
],
|
||||
results: [album3, album5],
|
||||
total: 4,
|
||||
});
|
||||
|
||||
@@ -1860,17 +1855,20 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the number of albums returned from getAlbums is more than the number of albums in the getArtists endpoint", () => {
|
||||
@@ -1879,11 +1877,15 @@ describe("Subsonic", () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])))
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
@@ -1899,7 +1901,7 @@ describe("Subsonic", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should return the page of albums, updating the total to be accurate", async () => {
|
||||
const q: AlbumQuery = {
|
||||
_index: 0,
|
||||
@@ -1913,13 +1915,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
album3,
|
||||
album4,
|
||||
album5,
|
||||
],
|
||||
results: [album1, album2, album3, album4, album5],
|
||||
total: 5,
|
||||
});
|
||||
|
||||
@@ -1928,15 +1924,18 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1945,12 +1944,16 @@ describe("Subsonic", () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])))
|
||||
)
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
@@ -1965,7 +1968,7 @@ describe("Subsonic", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should filter out the pre-fetched albums", async () => {
|
||||
const q: AlbumQuery = {
|
||||
_index: 0,
|
||||
@@ -1979,10 +1982,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
],
|
||||
results: [album1, album2],
|
||||
total: 5,
|
||||
});
|
||||
|
||||
@@ -1991,29 +1991,36 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the query is for the last page only", () => {
|
||||
beforeEach(() => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])))
|
||||
)
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
@@ -2040,11 +2047,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album3,
|
||||
album4,
|
||||
album5,
|
||||
],
|
||||
results: [album3, album4, album5],
|
||||
total: 5,
|
||||
});
|
||||
|
||||
@@ -2053,19 +2056,21 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
size: 500,
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2735,6 +2740,25 @@ describe("Subsonic", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when an unexpected error occurs", () => {
|
||||
it("should return undefined", async () => {
|
||||
const coverArtId = "someCoverArt";
|
||||
const size = 1879;
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() => Promise.reject("BOOOM"));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`coverArt:${coverArtId}`, size));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching artist art", () => {
|
||||
@@ -2798,6 +2822,38 @@ describe("Subsonic", () => {
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
});
|
||||
|
||||
describe("and an error occurs fetching the uri", () => {
|
||||
it("should return undefined", async () => {
|
||||
const artistId = "someArtist123";
|
||||
|
||||
const images: Images = {
|
||||
small: "http://example.com/images/small",
|
||||
medium: "http://example.com/images/medium",
|
||||
large: "http://example.com/images/large",
|
||||
};
|
||||
|
||||
const artist = anArtist({ id: artistId, image: images });
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.reject("BOOOM"));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the artist doest not have a valid artist uri", () => {
|
||||
@@ -2818,151 +2874,17 @@ describe("Subsonic", () => {
|
||||
data: Buffer.from("the image", "ascii"),
|
||||
};
|
||||
|
||||
describe("all albums have coverArt", () => {
|
||||
it("should fetch the coverArt from the first album", async () => {
|
||||
const album1 = anAlbum({ coverArt: `coverArt:album1CoverArt` });
|
||||
const album2 = anAlbum({ coverArt: `coverArt:album2CoverArt` });
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toEqual({
|
||||
contentType: streamResponse.headers["content-type"],
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getCoverArt`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: splitCoverArtId(album1.coverArt!)[1],
|
||||
}),
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the first album does not have coverArt", () => {
|
||||
it("should fetch the coverArt from the first album with coverArt", async () => {
|
||||
const album1 = anAlbum({ coverArt: undefined });
|
||||
const album2 = anAlbum({ coverArt: `coverArt:album2CoverArt` });
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toEqual({
|
||||
contentType: streamResponse.headers["content-type"],
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getCoverArt`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: splitCoverArtId(album2.coverArt!)[1],
|
||||
}),
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no albums have coverArt", () => {
|
||||
it("should return undefined", async () => {
|
||||
const album1 = anAlbum({ coverArt: undefined });
|
||||
const album2 = anAlbum({ coverArt: undefined });
|
||||
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
@@ -2971,24 +2893,29 @@ describe("Subsonic", () => {
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(streamResponse)
|
||||
);
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
|
||||
expect(result).toEqual(undefined);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtist`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
@@ -3001,7 +2928,194 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("some albums have coverArt", () => {
|
||||
describe("all albums have coverArt", () => {
|
||||
it("should fetch the coverArt from the first album", async () => {
|
||||
const album1 = anAlbum({
|
||||
coverArt: `coverArt:album1CoverArt`,
|
||||
});
|
||||
const album2 = anAlbum({
|
||||
coverArt: `coverArt:album2CoverArt`,
|
||||
});
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(streamResponse)
|
||||
);
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toEqual({
|
||||
contentType: streamResponse.headers["content-type"],
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtist`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getCoverArt`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: splitCoverArtId(album1.coverArt!)[1],
|
||||
}),
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the first album does not have coverArt", () => {
|
||||
it("should fetch the coverArt from the first album with coverArt", async () => {
|
||||
const album1 = anAlbum({ coverArt: undefined });
|
||||
const album2 = anAlbum({
|
||||
coverArt: `coverArt:album2CoverArt`,
|
||||
});
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(streamResponse)
|
||||
);
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toEqual({
|
||||
contentType: streamResponse.headers["content-type"],
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtist`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getCoverArt`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: splitCoverArtId(album2.coverArt!)[1],
|
||||
}),
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("an unexpected error occurs getting the albums coverArt", () => {
|
||||
it("should fetch the coverArt from the first album", async () => {
|
||||
const album1 = anAlbum({
|
||||
coverArt: `coverArt:album1CoverArt`,
|
||||
});
|
||||
const album2 = anAlbum({
|
||||
coverArt: `coverArt:album2CoverArt`,
|
||||
});
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
albums: [album1, album2],
|
||||
image: images,
|
||||
});
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.reject("BOOOM"));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.coverArt(`artist:${artistId}`));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3318,8 +3432,14 @@ describe("Subsonic", () => {
|
||||
data: Buffer.from("the image", "ascii"),
|
||||
};
|
||||
|
||||
const album1 = anAlbum({ id: "album1Id", coverArt: "coverArt:album1CoverArt" });
|
||||
const album2 = anAlbum({ id: "album2Id", coverArt: "coverArt:album2CoverArt" });
|
||||
const album1 = anAlbum({
|
||||
id: "album1Id",
|
||||
coverArt: "coverArt:album1CoverArt",
|
||||
});
|
||||
const album2 = anAlbum({
|
||||
id: "album2Id",
|
||||
coverArt: "coverArt:album2CoverArt",
|
||||
});
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
@@ -4046,23 +4166,31 @@ describe("Subsonic", () => {
|
||||
const id = uuid();
|
||||
const name = "Great Playlist";
|
||||
const artist1 = anArtist();
|
||||
const album1 = anAlbum({ artistId: artist1.id, artistName: artist1.name, genre: POP });
|
||||
const album1 = anAlbum({
|
||||
artistId: artist1.id,
|
||||
artistName: artist1.name,
|
||||
genre: POP,
|
||||
});
|
||||
const track1 = aTrack({
|
||||
genre: POP,
|
||||
number: 66,
|
||||
coverArt: album1.coverArt,
|
||||
artist: artistToArtistSummary(artist1),
|
||||
album: albumToAlbumSummary(album1)
|
||||
album: albumToAlbumSummary(album1),
|
||||
});
|
||||
|
||||
const artist2 = anArtist();
|
||||
const album2 = anAlbum({ artistId: artist2.id, artistName: artist2.name, genre: ROCK });
|
||||
const album2 = anAlbum({
|
||||
artistId: artist2.id,
|
||||
artistName: artist2.name,
|
||||
genre: ROCK,
|
||||
});
|
||||
const track2 = aTrack({
|
||||
genre: ROCK,
|
||||
number: 77,
|
||||
coverArt: album2.coverArt,
|
||||
artist: artistToArtistSummary(artist2),
|
||||
album: albumToAlbumSummary(album2)
|
||||
album: albumToAlbumSummary(album2),
|
||||
});
|
||||
|
||||
mockGET
|
||||
@@ -4487,7 +4615,7 @@ describe("Subsonic", () => {
|
||||
const track1 = aTrack({
|
||||
artist: artistToArtistSummary(artist),
|
||||
album: albumToAlbumSummary(album1),
|
||||
genre: POP
|
||||
genre: POP,
|
||||
});
|
||||
|
||||
const track2 = aTrack({
|
||||
|
||||
Reference in New Issue
Block a user