mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Scrobbling on play
This commit is contained in:
@@ -146,4 +146,5 @@ export interface MusicLibrary {
|
|||||||
range: string | undefined;
|
range: string | undefined;
|
||||||
}): Promise<Stream>;
|
}): Promise<Stream>;
|
||||||
coverArt(id: string, type: "album" | "artist", size?: number): Promise<CoverArt | undefined>;
|
coverArt(id: string, type: "album" | "artist", size?: number): Promise<CoverArt | undefined>;
|
||||||
|
scrobble(id: string): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,12 +118,12 @@ export type artistInfo = {
|
|||||||
smallImageUrl: string | undefined;
|
smallImageUrl: string | undefined;
|
||||||
mediumImageUrl: string | undefined;
|
mediumImageUrl: string | undefined;
|
||||||
largeImageUrl: string | undefined;
|
largeImageUrl: string | undefined;
|
||||||
similarArtist: artistSummary[]
|
similarArtist: artistSummary[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ArtistInfo = {
|
export type ArtistInfo = {
|
||||||
image: Images;
|
image: Images;
|
||||||
similarArtist: {id:string, name:string}[]
|
similarArtist: { id: string; name: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetArtistInfoResponse = SubsonicResponse & {
|
export type GetArtistInfoResponse = SubsonicResponse & {
|
||||||
@@ -244,6 +244,32 @@ export class Navidrome implements MusicService {
|
|||||||
else return response;
|
else return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
post = async (
|
||||||
|
{ username, password }: Credentials,
|
||||||
|
path: string,
|
||||||
|
q: {} = {},
|
||||||
|
config: AxiosRequestConfig | undefined = {}
|
||||||
|
) =>
|
||||||
|
axios
|
||||||
|
.post(`${this.url}${path}`, {
|
||||||
|
params: {
|
||||||
|
...q,
|
||||||
|
u: username,
|
||||||
|
...t_and_s(password),
|
||||||
|
v: "1.16.1",
|
||||||
|
c: "bonob",
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "bonob",
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status != 200 && response.status != 206)
|
||||||
|
throw `Navidrome failed with a ${response.status}`;
|
||||||
|
else return response;
|
||||||
|
});
|
||||||
|
|
||||||
getJSON = async <T>(
|
getJSON = async <T>(
|
||||||
{ username, password }: Credentials,
|
{ username, password }: Credentials,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -258,7 +284,7 @@ export class Navidrome implements MusicService {
|
|||||||
"subsonic-response.albumList.album",
|
"subsonic-response.albumList.album",
|
||||||
"subsonic-response.album.song",
|
"subsonic-response.album.song",
|
||||||
"subsonic-response.genres.genre",
|
"subsonic-response.genres.genre",
|
||||||
"subsonic-response.artistInfo.similarArtist"
|
"subsonic-response.artistInfo.similarArtist",
|
||||||
],
|
],
|
||||||
}).xml2js(response.data) as SubconicEnvelope
|
}).xml2js(response.data) as SubconicEnvelope
|
||||||
)
|
)
|
||||||
@@ -305,7 +331,10 @@ export class Navidrome implements MusicService {
|
|||||||
medium: validate(it.artistInfo.mediumImageUrl),
|
medium: validate(it.artistInfo.mediumImageUrl),
|
||||||
large: validate(it.artistInfo.largeImageUrl),
|
large: validate(it.artistInfo.largeImageUrl),
|
||||||
},
|
},
|
||||||
similarArtist: (it.artistInfo.similarArtist || []).map(artist => ({id: artist._id, name: artist._name}))
|
similarArtist: (it.artistInfo.similarArtist || []).map((artist) => ({
|
||||||
|
id: artist._id,
|
||||||
|
name: artist._name,
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
getAlbum = (credentials: Credentials, id: string): Promise<Album> =>
|
getAlbum = (credentials: Credentials, id: string): Promise<Album> =>
|
||||||
@@ -346,7 +375,7 @@ export class Navidrome implements MusicService {
|
|||||||
name: artist.name,
|
name: artist.name,
|
||||||
image: artistInfo.image,
|
image: artistInfo.image,
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: artistInfo.similarArtist
|
similarArtists: artistInfo.similarArtist,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
getCoverArt = (credentials: Credentials, id: string, size?: number) =>
|
getCoverArt = (credentials: Credentials, id: string, size?: number) =>
|
||||||
@@ -517,6 +546,11 @@ export class Navidrome implements MusicService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
scrobble: async (id: string) =>
|
||||||
|
navidrome
|
||||||
|
.post(credentials, `/rest/scrobble`, { id })
|
||||||
|
.then((_) => true)
|
||||||
|
.catch(() => false),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(musicLibrary);
|
return Promise.resolve(musicLibrary);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
|||||||
import { MusicService, isSuccess } from "./music_service";
|
import { MusicService, isSuccess } from "./music_service";
|
||||||
import bindSmapiSoapServiceToExpress from "./smapi";
|
import bindSmapiSoapServiceToExpress from "./smapi";
|
||||||
import { AccessTokens, AccessTokenPerAuthToken } from "./access_tokens";
|
import { AccessTokens, AccessTokenPerAuthToken } from "./access_tokens";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
|
||||||
|
|
||||||
@@ -136,6 +137,14 @@ function server(
|
|||||||
} else {
|
} else {
|
||||||
return musicService
|
return musicService
|
||||||
.login(authToken)
|
.login(authToken)
|
||||||
|
.then((it) =>
|
||||||
|
it.scrobble(id).then((scrobbleSuccess) => {
|
||||||
|
if(!scrobbleSuccess) {
|
||||||
|
logger.warn("Failed to scrobble....")
|
||||||
|
}
|
||||||
|
return it;
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
it.stream({ trackId: id, range: req.headers["range"] || undefined })
|
it.stream({ trackId: id, range: req.headers["range"] || undefined })
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
this.users[username] == password
|
this.users[username] == password
|
||||||
) {
|
) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
authToken: Buffer.from(JSON.stringify({ username, password })).toString('base64'),
|
authToken: Buffer.from(JSON.stringify({ username, password })).toString(
|
||||||
|
"base64"
|
||||||
|
),
|
||||||
userId: username,
|
userId: username,
|
||||||
nickname: username,
|
nickname: username,
|
||||||
});
|
});
|
||||||
@@ -52,7 +54,9 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
login(token: string): Promise<MusicLibrary> {
|
login(token: string): Promise<MusicLibrary> {
|
||||||
const credentials = JSON.parse(Buffer.from(token, "base64").toString("ascii")) as Credentials;
|
const credentials = JSON.parse(
|
||||||
|
Buffer.from(token, "base64").toString("ascii")
|
||||||
|
) as Credentials;
|
||||||
if (this.users[credentials.username] != credentials.password)
|
if (this.users[credentials.username] != credentials.password)
|
||||||
return Promise.reject("Invalid auth token");
|
return Promise.reject("Invalid auth token");
|
||||||
|
|
||||||
@@ -103,18 +107,24 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
A.sort(ordString)
|
A.sort(ordString)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
tracks: (albumId: string) => Promise.resolve(this.tracks.filter(it => it.album.id === albumId)),
|
tracks: (albumId: string) =>
|
||||||
track: (trackId: string) => pipe(
|
Promise.resolve(this.tracks.filter((it) => it.album.id === albumId)),
|
||||||
this.tracks.find(it => it.id === trackId),
|
track: (trackId: string) =>
|
||||||
O.fromNullable,
|
pipe(
|
||||||
O.map(it => Promise.resolve(it)),
|
this.tracks.find((it) => it.id === trackId),
|
||||||
O.getOrElse(() => Promise.reject(`Failed to find track with id ${trackId}`))
|
O.fromNullable,
|
||||||
),
|
O.map((it) => Promise.resolve(it)),
|
||||||
stream: (_: {
|
O.getOrElse(() =>
|
||||||
trackId: string;
|
Promise.reject(`Failed to find track with id ${trackId}`)
|
||||||
range: string | undefined;
|
)
|
||||||
}) => Promise.reject("unsupported operation"),
|
),
|
||||||
coverArt: (id: string, _: "album" | "artist", size?: number) => Promise.reject(`Cannot retrieve coverArt for ${id}, size ${size}`)
|
stream: (_: { trackId: string; range: string | undefined }) =>
|
||||||
|
Promise.reject("unsupported operation"),
|
||||||
|
coverArt: (id: string, _: "album" | "artist", size?: number) =>
|
||||||
|
Promise.reject(`Cannot retrieve coverArt for ${id}, size ${size}`),
|
||||||
|
scrobble: async (_: string) => {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const ok = (data: string) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const artistInfoXml = (
|
const artistInfoXml = (
|
||||||
artist: Artist,
|
artist: Artist
|
||||||
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
||||||
<artistInfo>
|
<artistInfo>
|
||||||
<biography></biography>
|
<biography></biography>
|
||||||
@@ -77,7 +77,10 @@ const artistInfoXml = (
|
|||||||
<smallImageUrl>${artist.image.small || ""}</smallImageUrl>
|
<smallImageUrl>${artist.image.small || ""}</smallImageUrl>
|
||||||
<mediumImageUrl>${artist.image.medium || ""}</mediumImageUrl>
|
<mediumImageUrl>${artist.image.medium || ""}</mediumImageUrl>
|
||||||
<largeImageUrl>${artist.image.large || ""}</largeImageUrl>
|
<largeImageUrl>${artist.image.large || ""}</largeImageUrl>
|
||||||
${artist.similarArtists.map(it => `<similarArtist id="${it.id}" name="${it.name}" albumCount="3"></similarArtist>`)}
|
${artist.similarArtists.map(
|
||||||
|
(it) =>
|
||||||
|
`<similarArtist id="${it.id}" name="${it.name}" albumCount="3"></similarArtist>`
|
||||||
|
)}
|
||||||
</artistInfo>
|
</artistInfo>
|
||||||
</subsonic-response>`;
|
</subsonic-response>`;
|
||||||
|
|
||||||
@@ -173,6 +176,8 @@ const getSongXml = (
|
|||||||
)}
|
)}
|
||||||
</subsonic-response>`;
|
</subsonic-response>`;
|
||||||
|
|
||||||
|
const EMPTY = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
||||||
|
|
||||||
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
||||||
|
|
||||||
describe("Navidrome", () => {
|
describe("Navidrome", () => {
|
||||||
@@ -185,12 +190,14 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
const mockedRandomString = (randomString as unknown) as jest.Mock;
|
const mockedRandomString = (randomString as unknown) as jest.Mock;
|
||||||
const mockGET = jest.fn();
|
const mockGET = jest.fn();
|
||||||
|
const mockPOST = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
|
||||||
axios.get = mockGET;
|
axios.get = mockGET;
|
||||||
|
axios.post = mockPOST;
|
||||||
|
|
||||||
mockedRandomString.mockReturnValue(salt);
|
mockedRandomString.mockReturnValue(salt);
|
||||||
});
|
});
|
||||||
@@ -310,7 +317,10 @@ describe("Navidrome", () => {
|
|||||||
medium: `http://localhost:80/${DODGY_IMAGE_NAME}`,
|
medium: `http://localhost:80/${DODGY_IMAGE_NAME}`,
|
||||||
large: `http://localhost:80/${DODGY_IMAGE_NAME}`,
|
large: `http://localhost:80/${DODGY_IMAGE_NAME}`,
|
||||||
},
|
},
|
||||||
similarArtists: [{ id: "similar1.id", name: "similar1" }, { id: "similar2.id", name: "similar2" }],
|
similarArtists: [
|
||||||
|
{ id: "similar1.id", name: "similar1" },
|
||||||
|
{ id: "similar2.id", name: "similar2" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -340,7 +350,7 @@ describe("Navidrome", () => {
|
|||||||
large: undefined,
|
large: undefined,
|
||||||
},
|
},
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: artist.similarArtists
|
similarArtists: artist.similarArtists,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -403,7 +413,7 @@ describe("Navidrome", () => {
|
|||||||
large: undefined,
|
large: undefined,
|
||||||
},
|
},
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: artist.similarArtists
|
similarArtists: artist.similarArtists,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -422,7 +432,7 @@ describe("Navidrome", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and has no similar artists", () => {
|
describe("and has no similar artists", () => {
|
||||||
const album1: Album = anAlbum();
|
const album1: Album = anAlbum();
|
||||||
@@ -466,7 +476,7 @@ describe("Navidrome", () => {
|
|||||||
large: undefined,
|
large: undefined,
|
||||||
},
|
},
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: artist.similarArtists
|
similarArtists: artist.similarArtists,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -485,7 +495,7 @@ describe("Navidrome", () => {
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and has dodgy looking artist image uris", () => {
|
describe("and has dodgy looking artist image uris", () => {
|
||||||
const album1: Album = anAlbum();
|
const album1: Album = anAlbum();
|
||||||
@@ -529,7 +539,7 @@ describe("Navidrome", () => {
|
|||||||
large: undefined,
|
large: undefined,
|
||||||
},
|
},
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -557,7 +567,7 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
const artist: Artist = anArtist({
|
const artist: Artist = anArtist({
|
||||||
albums: [album1, album2],
|
albums: [album1, album2],
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -583,7 +593,7 @@ describe("Navidrome", () => {
|
|||||||
name: artist.name,
|
name: artist.name,
|
||||||
image: artist.image,
|
image: artist.image,
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -609,7 +619,7 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
const artist: Artist = anArtist({
|
const artist: Artist = anArtist({
|
||||||
albums: [album],
|
albums: [album],
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -635,7 +645,7 @@ describe("Navidrome", () => {
|
|||||||
name: artist.name,
|
name: artist.name,
|
||||||
image: artist.image,
|
image: artist.image,
|
||||||
albums: artist.albums,
|
albums: artist.albums,
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -659,7 +669,7 @@ describe("Navidrome", () => {
|
|||||||
describe("and has no albums", () => {
|
describe("and has no albums", () => {
|
||||||
const artist: Artist = anArtist({
|
const artist: Artist = anArtist({
|
||||||
albums: [],
|
albums: [],
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -685,7 +695,7 @@ describe("Navidrome", () => {
|
|||||||
name: artist.name,
|
name: artist.name,
|
||||||
image: artist.image,
|
image: artist.image,
|
||||||
albums: [],
|
albums: [],
|
||||||
similarArtists: []
|
similarArtists: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, {
|
||||||
@@ -1662,7 +1672,7 @@ describe("Navidrome", () => {
|
|||||||
const artist = anArtist({
|
const artist = anArtist({
|
||||||
id: artistId,
|
id: artistId,
|
||||||
albums: [album1, album2],
|
albums: [album1, album2],
|
||||||
image: images
|
image: images,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
@@ -1737,7 +1747,11 @@ describe("Navidrome", () => {
|
|||||||
data: Buffer.from("the image", "ascii"),
|
data: Buffer.from("the image", "ascii"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const artist = anArtist({ id: artistId, albums: [], image: images });
|
const artist = anArtist({
|
||||||
|
id: artistId,
|
||||||
|
albums: [],
|
||||||
|
image: images,
|
||||||
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
@@ -1879,7 +1893,7 @@ describe("Navidrome", () => {
|
|||||||
const artist = anArtist({
|
const artist = anArtist({
|
||||||
id: artistId,
|
id: artistId,
|
||||||
albums: [album1, album2],
|
albums: [album1, album2],
|
||||||
image: images
|
image: images,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
@@ -1955,7 +1969,11 @@ describe("Navidrome", () => {
|
|||||||
data: Buffer.from("the image", "ascii"),
|
data: Buffer.from("the image", "ascii"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const artist = anArtist({ id: artistId, albums: [], image: images });
|
const artist = anArtist({
|
||||||
|
id: artistId,
|
||||||
|
albums: [],
|
||||||
|
image: images,
|
||||||
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
@@ -2022,7 +2040,7 @@ describe("Navidrome", () => {
|
|||||||
const artist = anArtist({
|
const artist = anArtist({
|
||||||
id: artistId,
|
id: artistId,
|
||||||
albums: [album1, album2],
|
albums: [album1, album2],
|
||||||
image: images
|
image: images,
|
||||||
});
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
@@ -2098,7 +2116,11 @@ describe("Navidrome", () => {
|
|||||||
data: Buffer.from("the image", "ascii"),
|
data: Buffer.from("the image", "ascii"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const artist = anArtist({ id: artistId, albums: [], image: images });
|
const artist = anArtist({
|
||||||
|
id: artistId,
|
||||||
|
albums: [],
|
||||||
|
image: images,
|
||||||
|
});
|
||||||
|
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
@@ -2142,4 +2164,63 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("scrobble", () => {
|
||||||
|
describe("when scrobbling succeeds", () => {
|
||||||
|
it("should return true", async () => {
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
mockGET.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)));
|
||||||
|
|
||||||
|
mockPOST.mockImplementationOnce(() => Promise.resolve(ok(EMPTY)));
|
||||||
|
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.scrobble(id));
|
||||||
|
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
|
expect(mockPOST).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when scrobbling fails", () => {
|
||||||
|
it("should return false", async () => {
|
||||||
|
const id = uuid();
|
||||||
|
|
||||||
|
mockGET.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)));
|
||||||
|
|
||||||
|
mockPOST.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 500,
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.scrobble(id));
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
|
||||||
|
expect(mockPOST).toHaveBeenCalledWith(`${url}/rest/scrobble`, {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ describe("server", () => {
|
|||||||
};
|
};
|
||||||
const musicLibrary = {
|
const musicLibrary = {
|
||||||
stream: jest.fn(),
|
stream: jest.fn(),
|
||||||
|
scrobble: jest.fn()
|
||||||
};
|
};
|
||||||
let now = dayjs();
|
let now = dayjs();
|
||||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||||
@@ -239,6 +240,54 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("scrobbling", () => {
|
||||||
|
describe("when scrobbling succeeds", () => {
|
||||||
|
it("should scrobble the track", async () => {
|
||||||
|
const stream = {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"content-type": "audio/mp3",
|
||||||
|
},
|
||||||
|
data: Buffer.from("some track", "ascii"),
|
||||||
|
};
|
||||||
|
|
||||||
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const res = await request(server)
|
||||||
|
.get(`/stream/track/${trackId}`)
|
||||||
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
|
expect(res.status).toEqual(stream.status);
|
||||||
|
expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when scrobbling succeeds", () => {
|
||||||
|
it("should still return the track", async () => {
|
||||||
|
const stream = {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"content-type": "audio/mp3",
|
||||||
|
},
|
||||||
|
data: Buffer.from("some track", "ascii"),
|
||||||
|
};
|
||||||
|
|
||||||
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const res = await request(server)
|
||||||
|
.get(`/stream/track/${trackId}`)
|
||||||
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
|
expect(res.status).toEqual(stream.status);
|
||||||
|
expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when sonos does not ask for a range", () => {
|
describe("when sonos does not ask for a range", () => {
|
||||||
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
|
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
|
||||||
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||||
@@ -252,6 +301,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
@@ -280,6 +330,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
@@ -308,6 +359,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
@@ -344,6 +396,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
@@ -382,6 +435,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
@@ -422,6 +476,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
musicLibrary.scrobble.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(`/stream/track/${trackId}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user