Artist images showing in list

This commit is contained in:
simojenki
2021-03-13 16:04:53 +11:00
parent a62abd3888
commit afa8132daa
11 changed files with 1367 additions and 178 deletions

View File

@@ -25,7 +25,6 @@ export type AuthFailure = {
export type ArtistSummary = {
id: string;
name: string;
image: Images;
};
export type Images = {
@@ -41,6 +40,7 @@ export const NO_IMAGES: Images = {
};
export type Artist = ArtistSummary & {
image: Images
albums: AlbumSummary[];
};
@@ -95,7 +95,6 @@ export type AlbumQuery = Paging & {
export const artistToArtistSummary = (it: Artist): ArtistSummary => ({
id: it.id,
name: it.name,
image: it.image,
});
export const albumToAlbumSummary = (it: Album): AlbumSummary => ({
@@ -145,5 +144,5 @@ export interface MusicLibrary {
trackId: string;
range: string | undefined;
}): Promise<Stream>;
coverArt(id: string, size?: number): Promise<CoverArt>;
coverArt(id: string, type: "album" | "artist", size?: number): Promise<CoverArt | undefined>;
}

View File

@@ -19,11 +19,22 @@ import {
NO_IMAGES,
} from "./music_service";
import X2JS from "x2js";
import sharp from "sharp";
import axios, { AxiosRequestConfig } from "axios";
import { Encryption } from "./encryption";
import randomString from "./random_string";
export const BROWSER_HEADERS = {
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-GB,en;q=0.5",
"upgrade-insecure-requests": "1",
"user-agent":
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
};
export const t = (password: string, s: string) =>
Md5.hashStr(`${password}${s}`);
@@ -35,8 +46,12 @@ export const t_and_s = (password: string) => {
};
};
export const isDodgyImage = (url: string) =>
url.endsWith("2a96cbd8b46e442fc41c2b86b821562f.png");
export const DODGY_IMAGE_NAME = "2a96cbd8b46e442fc41c2b86b821562f.png";
export const isDodgyImage = (url: string) => url.endsWith(DODGY_IMAGE_NAME);
export const validate = (url: string | undefined) =>
url && !isDodgyImage(url) ? url : undefined;
export type SubconicEnvelope = {
"subsonic-response": SubsonicResponse;
@@ -282,9 +297,9 @@ export class Navidrome implements MusicService {
id,
}).then((it) => ({
image: {
small: it.artistInfo.smallImageUrl,
medium: it.artistInfo.mediumImageUrl,
large: it.artistInfo.largeImageUrl,
small: validate(it.artistInfo.smallImageUrl),
medium: validate(it.artistInfo.mediumImageUrl),
large: validate(it.artistInfo.largeImageUrl),
},
}));
@@ -309,7 +324,7 @@ export class Navidrome implements MusicService {
.then((it) => ({
id: it._id,
name: it._name,
albums: it.album.map((album) => ({
albums: (it.album || []).map((album) => ({
id: album._id,
name: album._name,
year: album._year,
@@ -317,6 +332,28 @@ export class Navidrome implements MusicService {
})),
}));
getArtistWithInfo = (credentials: Credentials, id: string) =>
Promise.all([
this.getArtist(credentials, id),
this.getArtistInfo(credentials, id),
]).then(([artist, artistInfo]) => ({
id: artist.id,
name: artist.name,
image: artistInfo.image,
albums: artist.albums,
}));
getCoverArt = (credentials: Credentials, id: string, size?: number) =>
this.get(
credentials,
"/rest/getCoverArt",
{ id, size },
{
headers: { "User-Agent": "bonob" },
responseType: "arraybuffer",
}
);
async login(token: string) {
const navidrome = this;
const credentials: Credentials = this.parseToken(token);
@@ -326,36 +363,12 @@ export class Navidrome implements MusicService {
navidrome
.getArtists(credentials)
.then(slice2(q))
.then(([page, total]) =>
Promise.all(
page.map((idName: IdName) =>
navidrome
.getArtistInfo(credentials, idName.id)
.then((artistInfo) => ({
total,
result: {
id: idName.id,
name: idName.name,
image: artistInfo.image,
},
}))
)
)
)
.then((resultWithInfo) => ({
total: resultWithInfo[0]?.total || 0,
results: resultWithInfo.map((it) => it.result),
.then(([page, total]) => ({
total,
results: page.map((it) => ({ id: it.id, name: it.name })),
})),
artist: async (id: string): Promise<Artist> =>
Promise.all([
navidrome.getArtist(credentials, id),
navidrome.getArtistInfo(credentials, id),
]).then(([artist, artistInfo]) => ({
id: artist.id,
name: artist.name,
image: artistInfo.image,
albums: artist.albums,
})),
navidrome.getArtistWithInfo(credentials, id),
albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList", {
@@ -372,7 +385,7 @@ export class Navidrome implements MusicService {
size: MAX_ALBUM_LIST,
offset: 0,
})
.then((response) => response.albumList.album)
.then((response) => response.albumList.album || [])
.then((albumList) =>
albumList.map((album) => ({
id: album._id,
@@ -405,7 +418,7 @@ export class Navidrome implements MusicService {
})
.then((it) => it.album)
.then((album) =>
album.song.map((song) => asTrack(asAlbum(album), song))
(album.song || []).map((song) => asTrack(asAlbum(album), song))
),
track: (trackId: string) =>
navidrome
@@ -455,21 +468,54 @@ 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) => ({
coverArt: async (id: string, type: "album" | "artist", size?: number) => {
if (type == "album") {
return navidrome.getCoverArt(credentials, id, size).then((res) => ({
contentType: res.headers["content-type"],
data: Buffer.from(res.data, "binary"),
})),
}));
} else {
return navidrome.getArtistWithInfo(credentials, id).then((artist) => {
if (artist.image.large) {
console.log(`fetching from ${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 (artist.albums.length > 0) {
console.log(
`gettin cover art for artist album id = ${artist.albums[0]!.id}`
);
return navidrome
.getCoverArt(credentials, artist.albums[0]!.id, size)
.then((res) => ({
contentType: res.headers["content-type"],
data: Buffer.from(res.data, "binary"),
}));
} else {
return undefined;
}
});
}
},
};
return Promise.resolve(musicLibrary);

View File

@@ -149,25 +149,35 @@ function server(
}
});
app.get("/album/:albumId/art/size/:size", (req, res) => {
app.get("/:type/:id/art/size/:size", (req, res) => {
const authToken = accessTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
);
const type = req.params["type"]!;
const id = req.params["id"]!;
const size = Number.parseInt(req.params["size"]!);
if (!authToken) {
return res.status(401).send();
} else if(type != "artist" && type != "album") {
return res.status(400).send();
} else {
return musicService
.login(authToken)
.then((it) =>
it.coverArt(
req.params["albumId"]!,
Number.parseInt(req.params["size"]!)
id,
type,
size
)
)
.then((coverArt) => {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
res.send(coverArt.data);
if(coverArt) {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
res.send(coverArt.data);
} else {
res.status(404).send();
}
});
}
});

View File

@@ -8,6 +8,7 @@ import logger from "./logger";
import { LinkCodes } from "./link_codes";
import {
AlbumSummary,
ArtistSummary,
MusicLibrary,
MusicService,
slice2,
@@ -210,7 +211,19 @@ const genre = (genre: string) => ({
title: genre,
});
export const defaultAlbumArtURI = (webAddress: string, accessToken: string, album: AlbumSummary) => `${webAddress}/album/${album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
export const defaultAlbumArtURI = (
webAddress: string,
accessToken: string,
album: AlbumSummary
) =>
`${webAddress}/album/${album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
export const defaultArtistArtURI = (
webAddress: string,
accessToken: string,
artist: ArtistSummary
) =>
`${webAddress}/artist/${artist.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
const album = (
webAddress: string,
@@ -223,7 +236,11 @@ const album = (
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
});
export const track = (webAddress: string, accessToken: string, track: Track) => ({
export const track = (
webAddress: string,
accessToken: string,
track: Track
) => ({
itemType: "track",
id: `track:${track.id}`,
mimeType: track.mimeType,
@@ -382,19 +399,20 @@ function bindSmapiSoapServiceToExpress(
total: 3,
});
case "artists":
return await musicLibrary.artists(paging).then((result) =>
getMetadataResult({
return await musicLibrary.artists(paging).then((result) => {
const accessToken = accessTokens.mint(authToken);
return getMetadataResult({
mediaCollection: result.results.map((it) => ({
itemType: "artist",
id: `artist:${it.id}`,
artistId: it.id,
title: it.name,
albumArtURI: it.image.small,
albumArtURI: defaultArtistArtURI(webAddress, accessToken, it),
})),
index: paging._index,
total: result.total,
})
);
});
});
case "albums":
return await musicLibrary.albums(paging).then((result) => {
const accessToken = accessTokens.mint(authToken);