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 },
|
||||
|
||||
107
src/subsonic.ts
107
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,13 +612,23 @@ 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) => ({
|
||||
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 navidrome.getArtistWithInfo(credentials, id).then((artist) => {
|
||||
const albumsWithCoverArt = artist.albums.filter(it => it.coverArt);
|
||||
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!, {
|
||||
@@ -642,8 +653,12 @@ export class Subsonic implements MusicService {
|
||||
}
|
||||
});
|
||||
} else if (albumsWithCoverArt.length > 0) {
|
||||
return navidrome
|
||||
.getCoverArt(credentials, splitCoverArtId(albumsWithCoverArt[0]!.coverArt!)[1], size)
|
||||
return subsonic
|
||||
.getCoverArt(
|
||||
credentials,
|
||||
splitCoverArtId(albumsWithCoverArt[0]!.coverArt!)[1],
|
||||
size
|
||||
)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
@@ -651,11 +666,15 @@ export class Subsonic implements MusicService {
|
||||
} 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(
|
||||
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(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,7 +1736,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -1749,7 +1746,8 @@ describe("Subsonic", () => {
|
||||
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,7 +1796,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -1809,7 +1806,8 @@ describe("Subsonic", () => {
|
||||
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,7 +1855,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -1868,7 +1865,8 @@ describe("Subsonic", () => {
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1879,11 +1877,15 @@ describe("Subsonic", () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
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,7 +1924,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -1936,7 +1934,8 @@ describe("Subsonic", () => {
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1945,11 +1944,15 @@ describe("Subsonic", () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])))
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
@@ -1979,10 +1982,7 @@ describe("Subsonic", () => {
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
album1,
|
||||
album2,
|
||||
],
|
||||
results: [album1, album2],
|
||||
total: 5,
|
||||
});
|
||||
|
||||
@@ -1991,7 +1991,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -1999,7 +2001,8 @@ describe("Subsonic", () => {
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2008,11 +2011,15 @@ describe("Subsonic", () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistsXml([
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getArtistsXml([
|
||||
// artist1 has lost 2 albums on the getArtists end point
|
||||
{ ...artist1, albums: [album1, album2] },
|
||||
artist2,
|
||||
])))
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
@@ -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,7 +2056,9 @@ describe("Subsonic", () => {
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getAlbumList2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
type: "alphabeticalByArtist",
|
||||
@@ -2061,12 +2066,12 @@ describe("Subsonic", () => {
|
||||
offset: q._index,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("getting an album", () => {
|
||||
@@ -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,10 +2874,10 @@ 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` });
|
||||
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,
|
||||
@@ -2837,7 +2893,71 @@ 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/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
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 })
|
||||
@@ -2850,13 +2970,16 @@ describe("Subsonic", () => {
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtist`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
@@ -2888,7 +3011,9 @@ describe("Subsonic", () => {
|
||||
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 album2 = anAlbum({
|
||||
coverArt: `coverArt:album2CoverArt`,
|
||||
});
|
||||
|
||||
const artist = anArtist({
|
||||
id: artistId,
|
||||
@@ -2904,7 +3029,9 @@ describe("Subsonic", () => {
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(streamResponse)
|
||||
);
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
@@ -2917,13 +3044,16 @@ describe("Subsonic", () => {
|
||||
data: streamResponse.data,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtist`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
@@ -2952,10 +3082,14 @@ describe("Subsonic", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("no albums have coverArt", () => {
|
||||
it("should return undefined", async () => {
|
||||
const album1 = anAlbum({ coverArt: undefined });
|
||||
const album2 = anAlbum({ coverArt: undefined });
|
||||
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,
|
||||
@@ -2971,7 +3105,7 @@ describe("Subsonic", () => {
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||
)
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
.mockImplementationOnce(() => Promise.reject("BOOOM"));
|
||||
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
@@ -2979,28 +3113,8 @@ describe("Subsonic", () => {
|
||||
.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(result).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
`${url}/rest/getArtistInfo2`,
|
||||
{
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
id: artistId,
|
||||
count: 50,
|
||||
includeNotPresent: true,
|
||||
}),
|
||||
headers,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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