mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 01:43:29 +01:00
Album art displaying for artists
This commit is contained in:
@@ -113,6 +113,11 @@ export type Stream = {
|
||||
data: Buffer;
|
||||
};
|
||||
|
||||
export type CoverArt = {
|
||||
contentType: string;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
export const range = (size: number) => [...Array(size).keys()];
|
||||
|
||||
export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] =>
|
||||
@@ -140,4 +145,5 @@ export interface MusicLibrary {
|
||||
trackId: string;
|
||||
range: string | undefined;
|
||||
}): Promise<Stream>;
|
||||
coverArt(id: string, size?: number): Promise<CoverArt>;
|
||||
}
|
||||
|
||||
@@ -450,6 +450,21 @@ export class Navidrome implements MusicService {
|
||||
},
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
})),
|
||||
coverArt: async (id: string, size?: number) =>
|
||||
navidrome
|
||||
.get(
|
||||
credentials,
|
||||
"/rest/getCoverArt",
|
||||
{ id, size },
|
||||
{
|
||||
headers: { "User-Agent": "bonob" },
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
})),
|
||||
};
|
||||
|
||||
return Promise.resolve(musicLibrary);
|
||||
|
||||
@@ -11,15 +11,18 @@ import {
|
||||
} from "./smapi";
|
||||
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||
import { MusicService, isSuccess } from "./music_service";
|
||||
// import logger from "./logger";
|
||||
import bindSmapiSoapServiceToExpress from "./smapi";
|
||||
import { AccessTokens, ExpiringAccessTokens } from "./access_tokens";
|
||||
|
||||
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
|
||||
|
||||
function server(
|
||||
sonos: Sonos,
|
||||
bonobService: Service,
|
||||
webAddress: string | "http://localhost:4534",
|
||||
musicService: MusicService,
|
||||
linkCodes: LinkCodes = new InMemoryLinkCodes()
|
||||
linkCodes: LinkCodes = new InMemoryLinkCodes(),
|
||||
accessTokens: AccessTokens = new ExpiringAccessTokens()
|
||||
): Express {
|
||||
const app = express();
|
||||
|
||||
@@ -115,47 +118,51 @@ function server(
|
||||
|
||||
app.get("/stream/track/:id", async (req, res) => {
|
||||
const id = req.params["id"]!;
|
||||
const token = req.headers["bonob-token"] as string;
|
||||
return musicService
|
||||
.login(token)
|
||||
.then((it) =>
|
||||
it.stream({ trackId: id, range: req.headers["range"] || undefined })
|
||||
)
|
||||
.then((stream) => {
|
||||
res.status(stream.status);
|
||||
Object.entries(stream.headers).forEach(([header, value]) =>
|
||||
res.setHeader(header, value)
|
||||
);
|
||||
res.send(stream.data);
|
||||
});
|
||||
const accessToken = req.headers[BONOB_ACCESS_TOKEN_HEADER] as string;
|
||||
const authToken = accessTokens.authTokenFor(accessToken);
|
||||
if (!authToken) {
|
||||
return res.status(401).send();
|
||||
} else {
|
||||
return musicService
|
||||
.login(authToken)
|
||||
.then((it) =>
|
||||
it.stream({ trackId: id, range: req.headers["range"] || undefined })
|
||||
)
|
||||
.then((stream) => {
|
||||
res.status(stream.status);
|
||||
Object.entries(stream.headers).forEach(([header, value]) =>
|
||||
res.setHeader(header, value)
|
||||
);
|
||||
res.send(stream.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// app.get("/album/:albumId/art", (req, res) => {
|
||||
// console.log(`Trying to load image for ${req.params["albumId"]}, token ${JSON.stringify(req.cookies)}`)
|
||||
// const authToken = req.headers["X-AuthToken"]! as string;
|
||||
// const albumId = req.params["albumId"]!;
|
||||
// musicService
|
||||
// .login(authToken)
|
||||
// // .then((it) => it.artist(artistId))
|
||||
// // .then(artist => artist.image.small)
|
||||
// .then((url) => {
|
||||
// if (url) {
|
||||
// console.log(`${albumId} sending 307 -> ${url}`)
|
||||
// // res.setHeader("Location", url);
|
||||
// res.status(307).send();
|
||||
// } else {
|
||||
// console.log(`${albumId} sending 404`)
|
||||
// res.status(404).send();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
app.get("/album/:albumId/art", (req, res) => {
|
||||
const authToken = accessTokens.authTokenFor(
|
||||
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
|
||||
);
|
||||
if (!authToken) {
|
||||
return res.status(401).send();
|
||||
} else {
|
||||
return musicService
|
||||
.login(authToken)
|
||||
.then((it) => it.coverArt(req.params["albumId"]!, 200))
|
||||
.then((coverArt) => {
|
||||
res.status(200);
|
||||
res.setHeader("content-type", coverArt.contentType);
|
||||
res.send(coverArt.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
bindSmapiSoapServiceToExpress(
|
||||
app,
|
||||
SOAP_PATH,
|
||||
webAddress,
|
||||
linkCodes,
|
||||
musicService
|
||||
musicService,
|
||||
accessTokens
|
||||
);
|
||||
|
||||
return app;
|
||||
|
||||
69
src/smapi.ts
69
src/smapi.ts
@@ -13,6 +13,8 @@ import {
|
||||
slice2,
|
||||
Track,
|
||||
} from "./music_service";
|
||||
import { AccessTokens } from "./access_tokens";
|
||||
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
|
||||
|
||||
export const LOGIN_ROUTE = "/login";
|
||||
export const SOAP_PATH = "/ws/sonos";
|
||||
@@ -193,16 +195,15 @@ const genre = (genre: string) => ({
|
||||
title: genre,
|
||||
});
|
||||
|
||||
const album = (album: AlbumSummary) => ({
|
||||
const album = (
|
||||
webAddress: string,
|
||||
accessToken: string,
|
||||
album: AlbumSummary
|
||||
) => ({
|
||||
itemType: "album",
|
||||
id: `album:${album.id}`,
|
||||
title: album.name,
|
||||
// albumArtURI: {
|
||||
// attributes: {
|
||||
// requiresAuthentication: "true"
|
||||
// },
|
||||
// $value: `${webAddress}/album/${album.id}/art`
|
||||
// }
|
||||
albumArtURI: `${webAddress}/album/${album.id}/art?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`,
|
||||
});
|
||||
|
||||
const track = (track: Track) => ({
|
||||
@@ -235,7 +236,8 @@ function bindSmapiSoapServiceToExpress(
|
||||
soapPath: string,
|
||||
webAddress: string,
|
||||
linkCodes: LinkCodes,
|
||||
musicService: MusicService
|
||||
musicService: MusicService,
|
||||
accessTokens: AccessTokens
|
||||
) {
|
||||
const sonosSoap = new SonosSoap(webAddress, linkCodes);
|
||||
const soapyService = listen(
|
||||
@@ -276,8 +278,10 @@ function bindSmapiSoapServiceToExpress(
|
||||
getMediaURIResult: `${webAddress}/stream/${type}/${typeId}`,
|
||||
httpHeaders: [
|
||||
{
|
||||
header: "bonob-token",
|
||||
value: headers?.credentials?.loginToken.token,
|
||||
header: BONOB_ACCESS_TOKEN_HEADER,
|
||||
value: accessTokens.mint(
|
||||
headers?.credentials?.loginToken.token
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -330,16 +334,15 @@ function bindSmapiSoapServiceToExpress(
|
||||
},
|
||||
};
|
||||
}
|
||||
const login = await musicService
|
||||
.login(headers.credentials.loginToken.token)
|
||||
.catch((_) => {
|
||||
throw {
|
||||
Fault: {
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
},
|
||||
};
|
||||
});
|
||||
const authToken = headers.credentials.loginToken.token;
|
||||
const login = await musicService.login(authToken).catch((_) => {
|
||||
throw {
|
||||
Fault: {
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const musicLibrary = login as MusicLibrary;
|
||||
|
||||
@@ -372,13 +375,16 @@ function bindSmapiSoapServiceToExpress(
|
||||
})
|
||||
);
|
||||
case "albums":
|
||||
return await musicLibrary.albums(paging).then((result) =>
|
||||
getMetadataResult({
|
||||
mediaCollection: result.results.map(album),
|
||||
return await musicLibrary.albums(paging).then((result) => {
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
return getMetadataResult({
|
||||
mediaCollection: result.results.map((it) =>
|
||||
album(webAddress, accessToken, it)
|
||||
),
|
||||
index: paging._index,
|
||||
total: result.total,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
case "genres":
|
||||
return await musicLibrary
|
||||
.genres()
|
||||
@@ -395,13 +401,16 @@ function bindSmapiSoapServiceToExpress(
|
||||
.artist(typeId!)
|
||||
.then((artist) => artist.albums)
|
||||
.then(slice2(paging))
|
||||
.then(([page, total]) =>
|
||||
getMetadataResult({
|
||||
mediaCollection: page.map(album),
|
||||
.then(([page, total]) => {
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
return getMetadataResult({
|
||||
mediaCollection: page.map((it) =>
|
||||
album(webAddress, accessToken, it)
|
||||
),
|
||||
index: paging._index,
|
||||
total,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
case "album":
|
||||
return await musicLibrary
|
||||
.tracks(typeId!)
|
||||
|
||||
Reference in New Issue
Block a user