mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 01:43:29 +01:00
Artist images showing in list
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
144
src/navidrome.ts
144
src/navidrome.ts
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
32
src/smapi.ts
32
src/smapi.ts
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user