Album art displaying for artists

This commit is contained in:
simojenki
2021-03-12 15:16:44 +11:00
parent f38e4cab88
commit 3d1e8a48c9
8 changed files with 339 additions and 93 deletions

View File

@@ -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>;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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!)