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:
@@ -293,10 +293,12 @@ function server(
|
||||
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",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -233,8 +233,7 @@ describe("server", () => {
|
||||
|
||||
const fakeSonos: Sonos = {
|
||||
devices: () => Promise.resolve([]),
|
||||
services: () =>
|
||||
Promise.resolve([]),
|
||||
services: () => Promise.resolve([]),
|
||||
remove: () => Promise.resolve(false),
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
@@ -397,7 +396,8 @@ describe("server", () => {
|
||||
|
||||
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),
|
||||
};
|
||||
@@ -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,7 +1312,9 @@ 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,10 +1433,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);
|
||||
@@ -1423,7 +1444,19 @@ 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 = ["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,10 +1481,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);
|
||||
@@ -1692,7 +1722,13 @@ 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) })
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1789,10 +1787,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
],
|
||||
results: [album1, album2],
|
||||
total: 4,
|
||||
});
|
||||
|
||||
@@ -1801,15 +1796,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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1848,10 +1846,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album3,
|
||||
album5,
|
||||
],
|
||||
results: [album3, album5],
|
||||
total: 4,
|
||||
});
|
||||
|
||||
@@ -1860,15 +1855,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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
@@ -1979,10 +1982,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
],
|
||||
results: [album1, album2],
|
||||
total: 5,
|
||||
});
|
||||
|
||||
@@ -1991,15 +1991,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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2008,12 +2011,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(
|
||||
@@ -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,140 +2874,6 @@ 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 });
|
||||
@@ -2971,7 +2893,9 @@ describe("Subsonic", () => {
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(streamResponse)
|
||||
);
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
@@ -2981,13 +2905,16 @@ describe("Subsonic", () => {
|
||||
|
||||
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