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();
|
const trace = uuid();
|
||||||
|
|
||||||
logger.info(
|
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(
|
const authToken = pipe(
|
||||||
req.header(BONOB_ACCESS_TOKEN_HEADER),
|
req.query[BONOB_ACCESS_TOKEN_HEADER] as string,
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.map((accessToken) => accessTokens.authTokenFor(accessToken)),
|
O.map((accessToken) => accessTokens.authTokenFor(accessToken)),
|
||||||
O.getOrElseW(() => undefined)
|
O.getOrElseW(() => undefined)
|
||||||
@@ -340,9 +342,9 @@ function server(
|
|||||||
sendStream: boolean;
|
sendStream: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${trace} bnb-> /stream/track/${id}, status=${status}, headers=${JSON.stringify(
|
`${trace} bnb-> ${
|
||||||
headers
|
req.path
|
||||||
)}`
|
}, status=${status}, headers=${JSON.stringify(headers)}`
|
||||||
);
|
);
|
||||||
res.status(status);
|
res.status(status);
|
||||||
Object.entries(headers)
|
Object.entries(headers)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
Track,
|
Track,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
import { AccessTokens } from "./access_tokens";
|
import { AccessTokens } from "./access_tokens";
|
||||||
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
|
|
||||||
import { Clock } from "./clock";
|
import { Clock } from "./clock";
|
||||||
import { URLBuilder } from "./url_builder";
|
import { URLBuilder } from "./url_builder";
|
||||||
import { asLANGs, I8N } from "./i8n";
|
import { asLANGs, I8N } from "./i8n";
|
||||||
@@ -404,14 +403,9 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
getMediaURIResult: bonobUrl
|
getMediaURIResult: bonobUrl
|
||||||
.append({
|
.append({
|
||||||
pathname: `/stream/${type}/${typeId}`,
|
pathname: `/stream/${type}/${typeId}`,
|
||||||
|
searchParams: { "bat": accessToken }
|
||||||
})
|
})
|
||||||
.href(),
|
.href(),
|
||||||
httpHeaders: [
|
|
||||||
{
|
|
||||||
header: BONOB_ACCESS_TOKEN_HEADER,
|
|
||||||
value: accessToken,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})),
|
})),
|
||||||
getMediaMetadata: async (
|
getMediaMetadata: async (
|
||||||
{ id }: { id: string },
|
{ 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 { Encryption } from "./encryption";
|
||||||
import randomString from "./random_string";
|
import randomString from "./random_string";
|
||||||
import { b64Encode, b64Decode } from "./b64";
|
import { b64Encode, b64Decode } from "./b64";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export const BROWSER_HEADERS = {
|
export const BROWSER_HEADERS = {
|
||||||
accept:
|
accept:
|
||||||
@@ -225,8 +226,8 @@ export function isError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const splitCoverArtId = (coverArt: string): [string, string] => {
|
export const splitCoverArtId = (coverArt: string): [string, string] => {
|
||||||
const parts = coverArt.split(":").filter(it => it.length > 0);
|
const parts = coverArt.split(":").filter((it) => it.length > 0);
|
||||||
if(parts.length < 2) throw `'${coverArt}' is an invalid coverArt id'`
|
if (parts.length < 2) throw `'${coverArt}' is an invalid coverArt id'`;
|
||||||
return [parts[0]!, parts.slice(1).join(":")];
|
return [parts[0]!, parts.slice(1).join(":")];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,7 +247,8 @@ export type getAlbumListParams = {
|
|||||||
|
|
||||||
export const MAX_ALBUM_LIST = 500;
|
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) => ({
|
const asTrack = (album: Album, song: song) => ({
|
||||||
id: song._id,
|
id: song._id,
|
||||||
@@ -270,7 +272,7 @@ const asAlbum = (album: album) => ({
|
|||||||
genre: maybeAsGenre(album._genre),
|
genre: maybeAsGenre(album._genre),
|
||||||
artistId: album._artistId,
|
artistId: album._artistId,
|
||||||
artistName: album._artist,
|
artistName: album._artist,
|
||||||
coverArt: maybeAsCoverArt(album._coverArt)
|
coverArt: maybeAsCoverArt(album._coverArt),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const asGenre = (genreName: string) => ({
|
export const asGenre = (genreName: string) => ({
|
||||||
@@ -438,7 +440,7 @@ export class Subsonic implements MusicService {
|
|||||||
genre: maybeAsGenre(album._genre),
|
genre: maybeAsGenre(album._genre),
|
||||||
artistId: album._artistId,
|
artistId: album._artistId,
|
||||||
artistName: album._artist,
|
artistName: album._artist,
|
||||||
coverArt: maybeAsCoverArt(album._coverArt)
|
coverArt: maybeAsCoverArt(album._coverArt),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
getArtist = (
|
getArtist = (
|
||||||
@@ -492,7 +494,7 @@ export class Subsonic implements MusicService {
|
|||||||
genre: maybeAsGenre(album._genre),
|
genre: maybeAsGenre(album._genre),
|
||||||
artistId: album._artistId,
|
artistId: album._artistId,
|
||||||
artistName: album._artist,
|
artistName: album._artist,
|
||||||
coverArt: maybeAsCoverArt(album._coverArt)
|
coverArt: maybeAsCoverArt(album._coverArt),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
search3 = (credentials: Credentials, q: any) =>
|
search3 = (credentials: Credentials, q: any) =>
|
||||||
@@ -508,12 +510,12 @@ export class Subsonic implements MusicService {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
async login(token: string) {
|
async login(token: string) {
|
||||||
const navidrome = this;
|
const subsonic = this;
|
||||||
const credentials: Credentials = this.parseToken(token);
|
const credentials: Credentials = this.parseToken(token);
|
||||||
|
|
||||||
const musicLibrary: MusicLibrary = {
|
const musicLibrary: MusicLibrary = {
|
||||||
artists: (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
artists: (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
||||||
navidrome
|
subsonic
|
||||||
.getArtists(credentials)
|
.getArtists(credentials)
|
||||||
.then(slice2(q))
|
.then(slice2(q))
|
||||||
.then(([page, total]) => ({
|
.then(([page, total]) => ({
|
||||||
@@ -521,15 +523,15 @@ export class Subsonic implements MusicService {
|
|||||||
results: page.map((it) => ({ id: it.id, name: it.name })),
|
results: page.map((it) => ({ id: it.id, name: it.name })),
|
||||||
})),
|
})),
|
||||||
artist: async (id: string): Promise<Artist> =>
|
artist: async (id: string): Promise<Artist> =>
|
||||||
navidrome.getArtistWithInfo(credentials, id),
|
subsonic.getArtistWithInfo(credentials, id),
|
||||||
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
navidrome
|
subsonic
|
||||||
.getArtists(credentials)
|
.getArtists(credentials)
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
_.inject(it, (total, artist) => total + artist.albumCount, 0)
|
_.inject(it, (total, artist) => total + artist.albumCount, 0)
|
||||||
),
|
),
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
|
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
|
||||||
type: q.type,
|
type: q.type,
|
||||||
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
|
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
|
||||||
@@ -537,15 +539,14 @@ export class Subsonic implements MusicService {
|
|||||||
offset: q._index,
|
offset: q._index,
|
||||||
})
|
})
|
||||||
.then((response) => response.albumList2.album || [])
|
.then((response) => response.albumList2.album || [])
|
||||||
.then(navidrome.toAlbumSummary),
|
.then(subsonic.toAlbumSummary),
|
||||||
]).then(([total, albums]) => ({
|
]).then(([total, albums]) => ({
|
||||||
results: albums.slice(0, q._count),
|
results: albums.slice(0, q._count),
|
||||||
total: albums.length == 500 ? total : q._index + albums.length,
|
total: albums.length == 500 ? total : q._index + albums.length,
|
||||||
})),
|
})),
|
||||||
album: (id: string): Promise<Album> =>
|
album: (id: string): Promise<Album> => subsonic.getAlbum(credentials, id),
|
||||||
navidrome.getAlbum(credentials, id),
|
|
||||||
genres: () =>
|
genres: () =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetGenresResponse>(credentials, "/rest/getGenres")
|
.getJSON<GetGenresResponse>(credentials, "/rest/getGenres")
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -557,7 +558,7 @@ export class Subsonic implements MusicService {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
tracks: (albumId: string) =>
|
tracks: (albumId: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", {
|
.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", {
|
||||||
id: albumId,
|
id: albumId,
|
||||||
})
|
})
|
||||||
@@ -565,7 +566,7 @@ export class Subsonic implements MusicService {
|
|||||||
.then((album) =>
|
.then((album) =>
|
||||||
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
||||||
),
|
),
|
||||||
track: (trackId: string) => navidrome.getTrack(credentials, trackId),
|
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
|
||||||
stream: async ({
|
stream: async ({
|
||||||
trackId,
|
trackId,
|
||||||
range,
|
range,
|
||||||
@@ -573,8 +574,8 @@ export class Subsonic implements MusicService {
|
|||||||
trackId: string;
|
trackId: string;
|
||||||
range: string | undefined;
|
range: string | undefined;
|
||||||
}) =>
|
}) =>
|
||||||
navidrome.getTrack(credentials, trackId).then((track) =>
|
subsonic.getTrack(credentials, trackId).then((track) =>
|
||||||
navidrome
|
subsonic
|
||||||
.get(
|
.get(
|
||||||
credentials,
|
credentials,
|
||||||
`/rest/stream`,
|
`/rest/stream`,
|
||||||
@@ -611,51 +612,69 @@ export class Subsonic implements MusicService {
|
|||||||
coverArt: async (coverArt: string, size?: number) => {
|
coverArt: async (coverArt: string, size?: number) => {
|
||||||
const [type, id] = splitCoverArtId(coverArt);
|
const [type, id] = splitCoverArtId(coverArt);
|
||||||
if (type == "coverArt") {
|
if (type == "coverArt") {
|
||||||
return navidrome.getCoverArt(credentials, id, size).then((res) => ({
|
return subsonic
|
||||||
contentType: res.headers["content-type"],
|
.getCoverArt(credentials, id, size)
|
||||||
data: Buffer.from(res.data, "binary"),
|
.then((res) => ({
|
||||||
}));
|
contentType: res.headers["content-type"],
|
||||||
} else {
|
data: Buffer.from(res.data, "binary"),
|
||||||
return navidrome.getArtistWithInfo(credentials, id).then((artist) => {
|
}))
|
||||||
const albumsWithCoverArt = artist.albums.filter(it => it.coverArt);
|
.catch((e) => {
|
||||||
if (artist.image.large) {
|
logger.error(`Failed getting coverArt ${coverArt}: ${e}`);
|
||||||
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 undefined;
|
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) =>
|
scrobble: async (id: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.get(credentials, `/rest/scrobble`, {
|
.get(credentials, `/rest/scrobble`, {
|
||||||
id,
|
id,
|
||||||
submission: true,
|
submission: true,
|
||||||
@@ -663,7 +682,7 @@ export class Subsonic implements MusicService {
|
|||||||
.then((_) => true)
|
.then((_) => true)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
nowPlaying: async (id: string) =>
|
nowPlaying: async (id: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.get(credentials, `/rest/scrobble`, {
|
.get(credentials, `/rest/scrobble`, {
|
||||||
id,
|
id,
|
||||||
submission: false,
|
submission: false,
|
||||||
@@ -671,7 +690,7 @@ export class Subsonic implements MusicService {
|
|||||||
.then((_) => true)
|
.then((_) => true)
|
||||||
.catch(() => false),
|
.catch(() => false),
|
||||||
searchArtists: async (query: string) =>
|
searchArtists: async (query: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.search3(credentials, { query, artistCount: 20 })
|
.search3(credentials, { query, artistCount: 20 })
|
||||||
.then(({ artists }) =>
|
.then(({ artists }) =>
|
||||||
artists.map((artist) => ({
|
artists.map((artist) => ({
|
||||||
@@ -680,26 +699,26 @@ export class Subsonic implements MusicService {
|
|||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
searchAlbums: async (query: string) =>
|
searchAlbums: async (query: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.search3(credentials, { query, albumCount: 20 })
|
.search3(credentials, { query, albumCount: 20 })
|
||||||
.then(({ albums }) => navidrome.toAlbumSummary(albums)),
|
.then(({ albums }) => subsonic.toAlbumSummary(albums)),
|
||||||
searchTracks: async (query: string) =>
|
searchTracks: async (query: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.search3(credentials, { query, songCount: 20 })
|
.search3(credentials, { query, songCount: 20 })
|
||||||
.then(({ songs }) =>
|
.then(({ songs }) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
songs.map((it) => navidrome.getTrack(credentials, it._id))
|
songs.map((it) => subsonic.getTrack(credentials, it._id))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
playlists: async () =>
|
playlists: async () =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
||||||
.then((it) => it.playlists.playlist || [])
|
.then((it) => it.playlists.playlist || [])
|
||||||
.then((playlists) =>
|
.then((playlists) =>
|
||||||
playlists.map((it) => ({ id: it._id, name: it._name }))
|
playlists.map((it) => ({ id: it._id, name: it._name }))
|
||||||
),
|
),
|
||||||
playlist: async (id: string) =>
|
playlist: async (id: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
@@ -724,7 +743,7 @@ export class Subsonic implements MusicService {
|
|||||||
genre: maybeAsGenre(entry._genre),
|
genre: maybeAsGenre(entry._genre),
|
||||||
artistName: entry._artist,
|
artistName: entry._artist,
|
||||||
artistId: entry._artistId,
|
artistId: entry._artistId,
|
||||||
coverArt: maybeAsCoverArt(entry._coverArt)
|
coverArt: maybeAsCoverArt(entry._coverArt),
|
||||||
},
|
},
|
||||||
artist: {
|
artist: {
|
||||||
id: entry._artistId,
|
id: entry._artistId,
|
||||||
@@ -734,34 +753,34 @@ export class Subsonic implements MusicService {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
createPlaylist: async (name: string) =>
|
createPlaylist: async (name: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/createPlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/createPlaylist", {
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
.then((it) => it.playlist)
|
.then((it) => it.playlist)
|
||||||
.then((it) => ({ id: it._id, name: it._name })),
|
.then((it) => ({ id: it._id, name: it._name })),
|
||||||
deletePlaylist: async (id: string) =>
|
deletePlaylist: async (id: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
.then((_) => true),
|
.then((_) => true),
|
||||||
addToPlaylist: async (playlistId: string, trackId: string) =>
|
addToPlaylist: async (playlistId: string, trackId: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
||||||
playlistId,
|
playlistId,
|
||||||
songIdToAdd: trackId,
|
songIdToAdd: trackId,
|
||||||
})
|
})
|
||||||
.then((_) => true),
|
.then((_) => true),
|
||||||
removeFromPlaylist: async (playlistId: string, indicies: number[]) =>
|
removeFromPlaylist: async (playlistId: string, indicies: number[]) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/updatePlaylist", {
|
||||||
playlistId,
|
playlistId,
|
||||||
songIndexToRemove: indicies,
|
songIndexToRemove: indicies,
|
||||||
})
|
})
|
||||||
.then((_) => true),
|
.then((_) => true),
|
||||||
similarSongs: async (id: string) =>
|
similarSongs: async (id: string) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetSimilarSongsResponse>(
|
.getJSON<GetSimilarSongsResponse>(
|
||||||
credentials,
|
credentials,
|
||||||
"/rest/getSimilarSongs2",
|
"/rest/getSimilarSongs2",
|
||||||
@@ -771,15 +790,15 @@ export class Subsonic implements MusicService {
|
|||||||
.then((songs) =>
|
.then((songs) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
songs.map((song) =>
|
songs.map((song) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getAlbum(credentials, song._albumId)
|
.getAlbum(credentials, song._albumId)
|
||||||
.then((album) => asTrack(album, song))
|
.then((album) => asTrack(album, song))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
topSongs: async (artistId: string) =>
|
topSongs: async (artistId: string) =>
|
||||||
navidrome.getArtist(credentials, artistId).then(({ name }) =>
|
subsonic.getArtist(credentials, artistId).then(({ name }) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
|
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
|
||||||
artist: name,
|
artist: name,
|
||||||
count: 50,
|
count: 50,
|
||||||
@@ -788,7 +807,7 @@ export class Subsonic implements MusicService {
|
|||||||
.then((songs) =>
|
.then((songs) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
songs.map((song) =>
|
songs.map((song) =>
|
||||||
navidrome
|
subsonic
|
||||||
.getAlbum(credentials, song._albumId)
|
.getAlbum(credentials, song._albumId)
|
||||||
.then((album) => asTrack(album, song))
|
.then((album) => asTrack(album, song))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ describe("server", () => {
|
|||||||
bonobUrl,
|
bonobUrl,
|
||||||
new InMemoryMusicService(),
|
new InMemoryMusicService(),
|
||||||
{
|
{
|
||||||
version: "v123.456"
|
version: "v123.456",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -233,8 +233,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
const fakeSonos: Sonos = {
|
const fakeSonos: Sonos = {
|
||||||
devices: () => Promise.resolve([]),
|
devices: () => Promise.resolve([]),
|
||||||
services: () =>
|
services: () => Promise.resolve([]),
|
||||||
Promise.resolve([]),
|
|
||||||
remove: () => Promise.resolve(false),
|
remove: () => Promise.resolve(false),
|
||||||
register: () => Promise.resolve(false),
|
register: () => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
@@ -397,7 +396,8 @@ describe("server", () => {
|
|||||||
|
|
||||||
const fakeSonos: Sonos = {
|
const fakeSonos: Sonos = {
|
||||||
devices: () => Promise.resolve([device1, device2]),
|
devices: () => Promise.resolve([device1, device2]),
|
||||||
services: () => Promise.resolve([service1, service2, bonobService]),
|
services: () =>
|
||||||
|
Promise.resolve([service1, service2, bonobService]),
|
||||||
remove: () => Promise.resolve(false),
|
remove: () => Promise.resolve(false),
|
||||||
register: () => Promise.resolve(false),
|
register: () => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
@@ -755,13 +755,14 @@ describe("server", () => {
|
|||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
now = now.add(1, "day");
|
now = now.add(1, "day");
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server).head(
|
||||||
.head(
|
bonobUrl
|
||||||
bonobUrl
|
.append({
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
pathname: `/stream/track/${trackId}`,
|
||||||
.path()
|
searchParams: { bat: accessToken },
|
||||||
)
|
})
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.path()
|
||||||
|
);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -786,10 +787,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.head(
|
.head(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(trackStream.status);
|
expect(res.status).toEqual(trackStream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -812,8 +812,10 @@ describe("server", () => {
|
|||||||
musicLibrary.stream.mockResolvedValue(trackStream);
|
musicLibrary.stream.mockResolvedValue(trackStream);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.head(`/stream/track/${trackId}`)
|
.head(bonobUrl
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
|
.path()
|
||||||
|
);
|
||||||
|
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
expect(res.body).toEqual({});
|
expect(res.body).toEqual({});
|
||||||
@@ -840,10 +842,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -863,10 +864,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
|
|
||||||
@@ -894,10 +894,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -933,10 +932,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -970,10 +968,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.header["content-type"]).toEqual(
|
expect(res.header["content-type"]).toEqual(
|
||||||
@@ -1008,10 +1005,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
);
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.header["content-type"]).toEqual(
|
expect(res.header["content-type"]).toEqual(
|
||||||
@@ -1051,10 +1047,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
|
||||||
.set("Range", requestedRange);
|
.set("Range", requestedRange);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -1093,10 +1088,9 @@ describe("server", () => {
|
|||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||||
.path()
|
.path()
|
||||||
)
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
|
||||||
.set("Range", "4000-5000");
|
.set("Range", "4000-5000");
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -1242,11 +1236,24 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("fetching multiple images as a collage", () => {
|
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", () => {
|
describe("fetching a collage of 4 when all are available", () => {
|
||||||
it("should return the image and a 200", async () => {
|
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);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
|
||||||
@@ -1258,10 +1265,11 @@ describe("server", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.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);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
@@ -1270,10 +1278,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 200);
|
||||||
id,
|
|
||||||
200
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = await Image.load(res.body);
|
const image = await Image.load(res.body);
|
||||||
@@ -1294,7 +1299,7 @@ describe("server", () => {
|
|||||||
musicLibrary.coverArt.mockResolvedValueOnce(
|
musicLibrary.coverArt.mockResolvedValueOnce(
|
||||||
coverArtResponse({
|
coverArtResponse({
|
||||||
data: png,
|
data: png,
|
||||||
contentType: "image/some-mime-type"
|
contentType: "image/some-mime-type",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1307,7 +1312,9 @@ describe("server", () => {
|
|||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
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", () => {
|
describe("fetching a collage of 9 when all are available", () => {
|
||||||
it("should return the image and a 200", async () => {
|
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);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
|
||||||
@@ -1360,10 +1377,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||||
id,
|
|
||||||
180
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = await Image.load(res.body);
|
const image = await Image.load(res.body);
|
||||||
@@ -1374,7 +1388,17 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("fetching a collage of 9 when only 2 are available", () => {
|
describe("fetching a collage of 9 when only 2 are available", () => {
|
||||||
it("should still return an image and a 200", async () => {
|
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);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
|
||||||
@@ -1409,10 +1433,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||||
id,
|
|
||||||
180
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = await Image.load(res.body);
|
const image = await Image.load(res.body);
|
||||||
@@ -1423,7 +1444,19 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("fetching a collage of 11", () => {
|
describe("fetching a collage of 11", () => {
|
||||||
it("should still return an image and a 200, though will only display 9", async () => {
|
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);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
|
||||||
@@ -1448,10 +1481,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(id, 180);
|
||||||
id,
|
|
||||||
180
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = await Image.load(res.body);
|
const image = await Image.load(res.body);
|
||||||
@@ -1692,7 +1722,13 @@ describe("server", () => {
|
|||||||
expect(svg).toContain(`fill="brightpink"`);
|
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 () => {
|
it(`should return a ${theme} icon on ${date}`, async () => {
|
||||||
const response = await request(
|
const response = await request(
|
||||||
server({ now: () => dayjs(date) })
|
server({ now: () => dayjs(date) })
|
||||||
@@ -1706,14 +1742,50 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
itShouldBeFestive("christmas '22", "2022/12/25", "christmas", "red", "green")
|
itShouldBeFestive(
|
||||||
itShouldBeFestive("christmas '23", "2023/12/25", "christmas", "red", "green")
|
"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(
|
||||||
itShouldBeFestive("halloween", "2023/10/31", "halloween", "black", "orange")
|
"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(
|
||||||
itShouldBeFestive("cny '23", "2023/01/22", "yoRabbit", "red", "yellow")
|
"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
|
getMediaURIResult: bonobUrl
|
||||||
.append({
|
.append({
|
||||||
pathname: `/stream/track/${trackId}`,
|
pathname: `/stream/track/${trackId}`,
|
||||||
|
searchParams: { "bat": accessToken }
|
||||||
})
|
})
|
||||||
.href(),
|
.href(),
|
||||||
httpHeaders: {
|
|
||||||
header: "bat",
|
|
||||||
value: accessToken,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ const getArtistInfoXml = (
|
|||||||
</artistInfo2>
|
</artistInfo2>
|
||||||
</subsonic-response>`;
|
</subsonic-response>`;
|
||||||
|
|
||||||
const maybeIdFromCoverArtId = (coverArt: string | undefined) => coverArt ? splitCoverArtId(coverArt)[1] : "";
|
const maybeIdFromCoverArtId = (coverArt: string | undefined) =>
|
||||||
|
coverArt ? splitCoverArtId(coverArt)[1] : "";
|
||||||
|
|
||||||
const albumXml = (
|
const albumXml = (
|
||||||
artist: Artist,
|
artist: Artist,
|
||||||
@@ -437,15 +438,21 @@ const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="
|
|||||||
|
|
||||||
describe("splitCoverArtId", () => {
|
describe("splitCoverArtId", () => {
|
||||||
it("should split correctly", () => {
|
it("should split correctly", () => {
|
||||||
expect(splitCoverArtId("foo:bar")).toEqual(["foo", "bar"])
|
expect(splitCoverArtId("foo:bar")).toEqual(["foo", "bar"]);
|
||||||
expect(splitCoverArtId("foo:bar:car:jar")).toEqual(["foo", "bar:car:jar"])
|
expect(splitCoverArtId("foo:bar:car:jar")).toEqual(["foo", "bar:car:jar"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should blow up when the id is invalid", () => {
|
it("should blow up when the id is invalid", () => {
|
||||||
expect(() => splitCoverArtId("")).toThrow(`'' 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(
|
||||||
expect(() => splitCoverArtId("foo:")).toThrow(`'foo:' is an invalid coverArt id`)
|
`'foo:' is an invalid coverArt id`
|
||||||
expect(() => splitCoverArtId(":dog")).toThrow(`':dog' 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
|
// the artists have 5 albums in the getArtists endpoint
|
||||||
const artist1 = anArtist({
|
const artist1 = anArtist({
|
||||||
albums: [
|
albums: [album1, album2, album3, album4],
|
||||||
album1,
|
|
||||||
album2,
|
|
||||||
album3,
|
|
||||||
album4,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const artist2 = anArtist({
|
const artist2 = anArtist({
|
||||||
albums: [
|
albums: [album5],
|
||||||
album5,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const artists = [artist1, artist2];
|
const artists = [artist1, artist2];
|
||||||
|
|
||||||
@@ -1727,12 +1727,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album1, album2, album3, album5],
|
||||||
album1,
|
|
||||||
album2,
|
|
||||||
album3,
|
|
||||||
album5,
|
|
||||||
],
|
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1741,15 +1736,18 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
offset: q._index,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1789,10 +1787,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album1, album2],
|
||||||
album1,
|
|
||||||
album2,
|
|
||||||
],
|
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1801,15 +1796,18 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
offset: q._index,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1848,10 +1846,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album3, album5],
|
||||||
album3,
|
|
||||||
album5,
|
|
||||||
],
|
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1860,15 +1855,18 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
offset: q._index,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1879,11 +1877,15 @@ describe("Subsonic", () => {
|
|||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(getArtistsXml([
|
Promise.resolve(
|
||||||
// artist1 has lost 2 albums on the getArtists end point
|
ok(
|
||||||
{ ...artist1, albums: [album1, album2] },
|
getArtistsXml([
|
||||||
artist2,
|
// artist1 has lost 2 albums on the getArtists end point
|
||||||
])))
|
{ ...artist1, albums: [album1, album2] },
|
||||||
|
artist2,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
@@ -1913,13 +1915,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album1, album2, album3, album4, album5],
|
||||||
album1,
|
|
||||||
album2,
|
|
||||||
album3,
|
|
||||||
album4,
|
|
||||||
album5,
|
|
||||||
],
|
|
||||||
total: 5,
|
total: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1928,15 +1924,18 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
offset: q._index,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1945,12 +1944,16 @@ describe("Subsonic", () => {
|
|||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(getArtistsXml([
|
Promise.resolve(
|
||||||
// artist1 has lost 2 albums on the getArtists end point
|
ok(
|
||||||
{ ...artist1, albums: [album1, album2] },
|
getArtistsXml([
|
||||||
artist2,
|
// artist1 has lost 2 albums on the getArtists end point
|
||||||
])))
|
{ ...artist1, albums: [album1, album2] },
|
||||||
)
|
artist2,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
ok(
|
ok(
|
||||||
@@ -1979,10 +1982,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album1, album2],
|
||||||
album1,
|
|
||||||
album2,
|
|
||||||
],
|
|
||||||
total: 5,
|
total: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1991,15 +1991,18 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
offset: q._index,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2008,12 +2011,16 @@ describe("Subsonic", () => {
|
|||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(getArtistsXml([
|
Promise.resolve(
|
||||||
// artist1 has lost 2 albums on the getArtists end point
|
ok(
|
||||||
{ ...artist1, albums: [album1, album2] },
|
getArtistsXml([
|
||||||
artist2,
|
// artist1 has lost 2 albums on the getArtists end point
|
||||||
])))
|
{ ...artist1, albums: [album1, album2] },
|
||||||
)
|
artist2,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
ok(
|
ok(
|
||||||
@@ -2040,11 +2047,7 @@ describe("Subsonic", () => {
|
|||||||
.then((it) => it.albums(q));
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
results: [
|
results: [album3, album4, album5],
|
||||||
album3,
|
|
||||||
album4,
|
|
||||||
album5,
|
|
||||||
],
|
|
||||||
total: 5,
|
total: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2053,19 +2056,21 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList2`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getAlbumList2`,
|
||||||
...authParams,
|
{
|
||||||
type: "alphabeticalByArtist",
|
params: asURLSearchParams({
|
||||||
size: 500,
|
...authParams,
|
||||||
offset: q._index,
|
type: "alphabeticalByArtist",
|
||||||
}),
|
size: 500,
|
||||||
headers,
|
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", () => {
|
describe("fetching artist art", () => {
|
||||||
@@ -2798,6 +2822,38 @@ describe("Subsonic", () => {
|
|||||||
responseType: "arraybuffer",
|
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", () => {
|
describe("when the artist doest not have a valid artist uri", () => {
|
||||||
@@ -2818,140 +2874,6 @@ describe("Subsonic", () => {
|
|||||||
data: Buffer.from("the image", "ascii"),
|
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", () => {
|
describe("no albums have coverArt", () => {
|
||||||
it("should return undefined", async () => {
|
it("should return undefined", async () => {
|
||||||
const album1 = anAlbum({ coverArt: undefined });
|
const album1 = anAlbum({ coverArt: undefined });
|
||||||
@@ -2971,7 +2893,9 @@ describe("Subsonic", () => {
|
|||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(ok(getArtistInfoXml(artist)))
|
Promise.resolve(ok(getArtistInfoXml(artist)))
|
||||||
)
|
)
|
||||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(streamResponse)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
@@ -2981,13 +2905,16 @@ describe("Subsonic", () => {
|
|||||||
|
|
||||||
expect(result).toEqual(undefined);
|
expect(result).toEqual(undefined);
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
params: asURLSearchParams({
|
`${url}/rest/getArtist`,
|
||||||
...authParams,
|
{
|
||||||
id: artistId,
|
params: asURLSearchParams({
|
||||||
}),
|
...authParams,
|
||||||
headers,
|
id: artistId,
|
||||||
});
|
}),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
`${url}/rest/getArtistInfo2`,
|
`${url}/rest/getArtistInfo2`,
|
||||||
@@ -3001,7 +2928,194 @@ describe("Subsonic", () => {
|
|||||||
headers,
|
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"),
|
data: Buffer.from("the image", "ascii"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const album1 = anAlbum({ id: "album1Id", coverArt: "coverArt:album1CoverArt" });
|
const album1 = anAlbum({
|
||||||
const album2 = anAlbum({ id: "album2Id", coverArt: "coverArt:album2CoverArt" });
|
id: "album1Id",
|
||||||
|
coverArt: "coverArt:album1CoverArt",
|
||||||
|
});
|
||||||
|
const album2 = anAlbum({
|
||||||
|
id: "album2Id",
|
||||||
|
coverArt: "coverArt:album2CoverArt",
|
||||||
|
});
|
||||||
|
|
||||||
const artist = anArtist({
|
const artist = anArtist({
|
||||||
id: artistId,
|
id: artistId,
|
||||||
@@ -4046,23 +4166,31 @@ describe("Subsonic", () => {
|
|||||||
const id = uuid();
|
const id = uuid();
|
||||||
const name = "Great Playlist";
|
const name = "Great Playlist";
|
||||||
const artist1 = anArtist();
|
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({
|
const track1 = aTrack({
|
||||||
genre: POP,
|
genre: POP,
|
||||||
number: 66,
|
number: 66,
|
||||||
coverArt: album1.coverArt,
|
coverArt: album1.coverArt,
|
||||||
artist: artistToArtistSummary(artist1),
|
artist: artistToArtistSummary(artist1),
|
||||||
album: albumToAlbumSummary(album1)
|
album: albumToAlbumSummary(album1),
|
||||||
});
|
});
|
||||||
|
|
||||||
const artist2 = anArtist();
|
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({
|
const track2 = aTrack({
|
||||||
genre: ROCK,
|
genre: ROCK,
|
||||||
number: 77,
|
number: 77,
|
||||||
coverArt: album2.coverArt,
|
coverArt: album2.coverArt,
|
||||||
artist: artistToArtistSummary(artist2),
|
artist: artistToArtistSummary(artist2),
|
||||||
album: albumToAlbumSummary(album2)
|
album: albumToAlbumSummary(album2),
|
||||||
});
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
@@ -4487,7 +4615,7 @@ describe("Subsonic", () => {
|
|||||||
const track1 = aTrack({
|
const track1 = aTrack({
|
||||||
artist: artistToArtistSummary(artist),
|
artist: artistToArtistSummary(artist),
|
||||||
album: albumToAlbumSummary(album1),
|
album: albumToAlbumSummary(album1),
|
||||||
genre: POP
|
genre: POP,
|
||||||
});
|
});
|
||||||
|
|
||||||
const track2 = aTrack({
|
const track2 = aTrack({
|
||||||
|
|||||||
Reference in New Issue
Block a user