Feature/no more sharp (#193)

* Playlist icons working as rendered by ND

* remove duplication in cover art image url creation

* Remove unused ability to create collages of images
This commit is contained in:
Simon J
2024-02-05 17:22:27 +11:00
committed by GitHub
parent 66c248fe44
commit 6bf89b87e2
6 changed files with 74 additions and 752 deletions

View File

@@ -113,7 +113,8 @@ export const albumToAlbumSummary = (it: Album): AlbumSummary => ({
export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({
id: it.id,
name: it.name
name: it.name,
coverArt: it.coverArt
})
export type StreamingHeader = "content-type" | "content-length" | "content-range" | "accept-ranges";
@@ -131,7 +132,8 @@ export type CoverArt = {
export type PlaylistSummary = {
id: string,
name: string
name: string,
coverArt?: BUrn | undefined
}
export type Playlist = PlaylistSummary & {

View File

@@ -31,9 +31,8 @@ import { pipe } from "fp-ts/lib/function";
import { URLBuilder } from "./url_builder";
import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n";
import { Icon, ICONS, festivals, features } from "./icon";
import _, { shuffle } from "underscore";
import _ from "underscore";
import morgan from "morgan";
import { takeWithRepeats } from "./utils";
import { parse } from "./burn";
import { axiosImageFetcher, ImageFetcher } from "./subsonic";
import {
@@ -558,23 +557,11 @@ function server(
});
});
const GRAVITY_9 = [
"north",
"northeast",
"east",
"southeast",
"south",
"southwest",
"west",
"northwest",
"centre",
];
app.get("/art/:burns/size/:size", (req, res) => {
app.get("/art/:burn/size/:size", (req, res) => {
const serviceToken = apiTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
);
const urns = req.params["burns"]!.split("&").map(parse);
const urn = parse(req.params["burn"]!);
const size = Number.parseInt(req.params["size"]!);
if (!serviceToken) {
@@ -585,55 +572,24 @@ function server(
return musicService
.login(serviceToken)
.then((musicLibrary) =>
Promise.all(
urns.map((it) => {
if (it.system == "external") {
return serverOpts.externalImageResolver(it.resource);
} else {
return musicLibrary.coverArt(it, size);
}
})
)
)
.then((coverArts) => coverArts.filter((it) => it))
.then(shuffle)
.then((coverArts) => {
if (coverArts.length == 1) {
const coverArt = coverArts[0]!;
.then((musicLibrary) => {
if (urn.system == "external") {
return serverOpts.externalImageResolver(urn.resource);
} else {
return musicLibrary.coverArt(urn, size);
}
})
.then((coverArt) => {
if(coverArt) {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
return res.send(coverArt.data);
} else if (coverArts.length > 1) {
const gravity = [...GRAVITY_9];
return sharp({
create: {
width: size * 3,
height: size * 3,
channels: 3,
background: { r: 255, g: 255, b: 255 },
},
})
.composite(
takeWithRepeats(coverArts, 9).map((art) => ({
input: art?.data,
gravity: gravity.pop(),
}))
)
.png()
.toBuffer()
.then((image) => sharp(image).resize(size).png().toBuffer())
.then((image) => {
res.status(200);
res.setHeader("content-type", "image/png");
return res.send(image);
});
} else {
return res.status(404).send();
}
})
})
.catch((e: Error) => {
logger.error(`Failed fetching image ${urns.join("&")}/size/${size}`, {
logger.error(`Failed fetching image ${urn}/size/${size}`, {
cause: e,
});
return res.status(500).send();

View File

@@ -26,7 +26,7 @@ import { Clock } from "./clock";
import { URLBuilder } from "./url_builder";
import { asLANGs, I8N } from "./i8n";
import { ICON, iconForGenre } from "./icon";
import _, { uniq } from "underscore";
import _ from "underscore";
import { BUrn, formatForURL } from "./burn";
import {
isExpiredTokenError,
@@ -253,7 +253,7 @@ const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
itemType: "playlist",
id: `playlist:${playlist.id}`,
title: playlist.name,
albumArtURI: playlistAlbumArtURL(bonobUrl, playlist).href(),
albumArtURI: coverArtURI(bonobUrl, playlist).href(),
canPlay: true,
attributes: {
readOnly: false,
@@ -262,32 +262,9 @@ const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
},
});
export const playlistAlbumArtURL = (
export const coverArtURI = (
bonobUrl: URLBuilder,
playlist: Playlist
) => {
// todo: this should be put into config, or even just removed for the ND music source
if(process.env["BNB_DISABLE_PLAYLIST_ART"]) return iconArtURI(bonobUrl, "music");
const burns: BUrn[] = uniq(
playlist.entries.filter((it) => it.coverArt != undefined),
(it) => it.album.id
).map((it) => it.coverArt!);
if (burns.length == 0) {
return iconArtURI(bonobUrl, "error");
} else {
return bonobUrl.append({
pathname: `/art/${burns
.slice(0, 9)
.map((it) => encodeURIComponent(formatForURL(it)))
.join("&")}/size/180`,
});
}
};
export const defaultAlbumArtURI = (
bonobUrl: URLBuilder,
{ coverArt }: { coverArt: BUrn | undefined }
{ coverArt }: { coverArt?: BUrn | undefined }
) =>
pipe(
coverArt,
@@ -305,21 +282,6 @@ export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON) =>
pathname: `/icon/${icon}/size/legacy`,
});
export const defaultArtistArtURI = (
bonobUrl: URLBuilder,
artist: ArtistSummary
) =>
pipe(
artist.image,
O.fromNullable,
O.map((it) =>
bonobUrl.append({
pathname: `/art/${encodeURIComponent(formatForURL(it))}/size/180`,
})
),
O.getOrElseW(() => iconArtURI(bonobUrl, "vinyl"))
);
export const sonosifyMimeType = (mimeType: string) =>
mimeType == "audio/x-flac" ? "audio/flac" : mimeType;
@@ -329,7 +291,7 @@ export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
artist: album.artistName,
artistId: `artist:${album.artistId}`,
title: album.name,
albumArtURI: defaultAlbumArtURI(bonobUrl, album).href(),
albumArtURI: coverArtURI(bonobUrl, album).href(),
canPlay: true,
// defaults
// canScroll: false,
@@ -348,7 +310,7 @@ export const track = (bonobUrl: URLBuilder, track: Track) => ({
albumId: `album:${track.album.id}`,
albumArtist: track.artist.name,
albumArtistId: track.artist.id ? `artist:${track.artist.id}` : undefined,
albumArtURI: defaultAlbumArtURI(bonobUrl, track).href(),
albumArtURI: coverArtURI(bonobUrl, track).href(),
artist: track.artist.name,
artistId: track.artist.id ? `artist:${track.artist.id}` : undefined,
duration: track.duration,
@@ -366,7 +328,7 @@ export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({
id: `artist:${artist.id}`,
artistId: artist.id,
title: artist.name,
albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(),
albumArtURI: coverArtURI(bonobUrl, { coverArt: artist.image }).href(),
});
function splitId<T>(id: string) {
@@ -872,9 +834,12 @@ function bindSmapiSoapServiceToExpress(
.then((it) =>
Promise.all(
it.map((playlist) => {
// todo: whats this odd copy all about, can we just delete it?
return {
id: playlist.id,
name: playlist.name,
coverArt: playlist.coverArt,
// todo: are these every important?
entries: []
};
}

View File

@@ -20,6 +20,7 @@ import {
AlbumQueryType,
Artist,
AuthFailure,
PlaylistSummary
} from "./music_service";
import sharp from "sharp";
import _ from "underscore";
@@ -178,12 +179,15 @@ type GetAlbumResponse = {
type playlist = {
id: string;
name: string;
coverArt: string | undefined;
};
type GetPlaylistResponse = {
// todo: isnt the type here a composite? playlistSummary && { entry: song[]; }
playlist: {
id: string;
name: string;
coverArt: string | undefined;
entry: song[];
};
};
@@ -306,6 +310,13 @@ const asAlbum = (album: album): Album => ({
coverArt: coverArtURN(album.coverArt),
});
// coverArtURN
const asPlayListSummary = (playlist: playlist): PlaylistSummary => ({
id: playlist.id,
name: playlist.name,
coverArt: coverArtURN(playlist.coverArt)
})
export const asGenre = (genreName: string) => ({
id: b64Encode(genreName),
name: genreName,
@@ -862,7 +873,7 @@ export class Subsonic implements MusicService {
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
.then((it) => it.playlists.playlist || [])
.then((playlists) =>
playlists.map((it) => ({ id: it.id, name: it.name }))
playlists.map(asPlayListSummary)
),
playlist: async (id: string) =>
subsonic
@@ -875,6 +886,7 @@ export class Subsonic implements MusicService {
return {
id: playlist.id,
name: playlist.name,
coverArt: coverArtURN(playlist.coverArt),
entries: (playlist.entry || []).map((entry) => ({
...asTrack(
{
@@ -898,7 +910,8 @@ export class Subsonic implements MusicService {
name,
})
.then((it) => it.playlist)
.then((it) => ({ id: it.id, name: it.name })),
// todo: why is this line so similar to other playlist lines??
.then((it) => ({ id: it.id, name: it.name, coverArt: coverArtURN(it.coverArt) })),
deletePlaylist: async (id: string) =>
subsonic
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {