mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Migrate Navidrome support to generic subsonic clone support (#55)
Renaming BONOB_* env vars to BNB_*
This commit is contained in:
14
src/app.ts
14
src/app.ts
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import fs from "fs";
|
||||
import server from "./server";
|
||||
import logger from "./logger";
|
||||
import { appendMimeTypeToClientFor, DEFAULT, Navidrome } from "./navidrome";
|
||||
import { appendMimeTypeToClientFor, DEFAULT, Subsonic } from "./subsonic";
|
||||
import encryption from "./encryption";
|
||||
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
|
||||
import { InMemoryLinkCodes } from "./link_codes";
|
||||
@@ -24,20 +24,20 @@ const bonob = bonobService(
|
||||
|
||||
const sonosSystem = sonos(config.sonos.discovery);
|
||||
|
||||
const streamUserAgent = config.navidrome.customClientsFor
|
||||
? appendMimeTypeToClientFor(config.navidrome.customClientsFor.split(","))
|
||||
const streamUserAgent = config.subsonic.customClientsFor
|
||||
? appendMimeTypeToClientFor(config.subsonic.customClientsFor.split(","))
|
||||
: DEFAULT;
|
||||
|
||||
const navidrome = new Navidrome(
|
||||
config.navidrome.url,
|
||||
const subsonic = new Subsonic(
|
||||
config.subsonic.url,
|
||||
encryption(config.secret),
|
||||
streamUserAgent
|
||||
);
|
||||
|
||||
const featureFlagAwareMusicService: MusicService = {
|
||||
generateToken: navidrome.generateToken,
|
||||
generateToken: subsonic.generateToken,
|
||||
login: (authToken: string) =>
|
||||
navidrome.login(authToken).then((library) => {
|
||||
subsonic.login(authToken).then((library) => {
|
||||
return {
|
||||
...library,
|
||||
scrobble: (id: string) => {
|
||||
|
||||
@@ -2,56 +2,91 @@ import { hostname } from "os";
|
||||
import logger from "./logger";
|
||||
import url from "./url_builder";
|
||||
|
||||
export const WORD = /^\w+$/;
|
||||
|
||||
type EnvVarOpts = {
|
||||
default: string | undefined;
|
||||
legacy: string[] | undefined;
|
||||
validationPattern: RegExp | undefined;
|
||||
};
|
||||
|
||||
export function envVar(
|
||||
name: string,
|
||||
opts: Partial<EnvVarOpts> = {
|
||||
default: undefined,
|
||||
legacy: undefined,
|
||||
validationPattern: undefined,
|
||||
}
|
||||
) {
|
||||
const result = [name, ...(opts.legacy || [])]
|
||||
.map((it) => ({ key: it, value: process.env[it] }))
|
||||
.find((it) => it.value);
|
||||
|
||||
if (
|
||||
result &&
|
||||
result.value &&
|
||||
opts.validationPattern &&
|
||||
!result.value.match(opts.validationPattern)
|
||||
) {
|
||||
throw `Invalid value specified for '${name}', must match ${opts.validationPattern}`;
|
||||
}
|
||||
|
||||
if(result && result.value && result.key != name) {
|
||||
logger.warn(`Configuration key '${result.key}' is deprecated, replace with '${name}'`)
|
||||
}
|
||||
|
||||
return result?.value || opts.default;
|
||||
}
|
||||
|
||||
export const bnbEnvVar = (key: string, opts: Partial<EnvVarOpts> = {}) =>
|
||||
envVar(`BNB_${key}`, {
|
||||
...opts,
|
||||
legacy: [`BONOB_${key}`, ...(opts.legacy || [])],
|
||||
});
|
||||
|
||||
export default function () {
|
||||
const port = +(process.env["BONOB_PORT"] || 4534);
|
||||
const bonobUrl =
|
||||
process.env["BONOB_URL"] ||
|
||||
process.env["BONOB_WEB_ADDRESS"] ||
|
||||
`http://${hostname()}:${port}`;
|
||||
const port = +bnbEnvVar("PORT", { default: "4534" })!;
|
||||
const bonobUrl = bnbEnvVar("URL", {
|
||||
legacy: ["BONOB_WEB_ADDRESS"],
|
||||
default: `http://${hostname()}:${port}`,
|
||||
})!;
|
||||
|
||||
if (bonobUrl.match("localhost")) {
|
||||
logger.error(
|
||||
"BONOB_URL containing localhost is almost certainly incorrect, sonos devices will not be able to communicate with bonob using localhost, please specify either public IP or DNS entry"
|
||||
"BNB_URL containing localhost is almost certainly incorrect, sonos devices will not be able to communicate with bonob using localhost, please specify either public IP or DNS entry"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const wordFrom = (envVar: string) => {
|
||||
const value = process.env[envVar];
|
||||
if (value && value != "") {
|
||||
if (value.match(/^\w+$/)) return value;
|
||||
else throw `Invalid color specified for ${envVar}`;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
port,
|
||||
bonobUrl: url(bonobUrl),
|
||||
secret: process.env["BONOB_SECRET"] || "bonob",
|
||||
secret: bnbEnvVar("SECRET", { default: "bonob" })!,
|
||||
icons: {
|
||||
foregroundColor: wordFrom("BONOB_ICON_FOREGROUND_COLOR"),
|
||||
backgroundColor: wordFrom("BONOB_ICON_BACKGROUND_COLOR"),
|
||||
foregroundColor: bnbEnvVar("ICON_FOREGROUND_COLOR", {
|
||||
validationPattern: WORD,
|
||||
}),
|
||||
backgroundColor: bnbEnvVar("ICON_BACKGROUND_COLOR", {
|
||||
validationPattern: WORD,
|
||||
}),
|
||||
},
|
||||
sonos: {
|
||||
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
||||
serviceName: bnbEnvVar("SONOS_SERVICE_NAME", { default: "bonob" })!,
|
||||
discovery: {
|
||||
enabled:
|
||||
(process.env["BONOB_SONOS_DEVICE_DISCOVERY"] || "true") == "true",
|
||||
seedHost: process.env["BONOB_SONOS_SEED_HOST"],
|
||||
bnbEnvVar("SONOS_DEVICE_DISCOVERY", { default: "true" }) == "true",
|
||||
seedHost: bnbEnvVar("SONOS_SEED_HOST"),
|
||||
},
|
||||
autoRegister:
|
||||
(process.env["BONOB_SONOS_AUTO_REGISTER"] || "false") == "true",
|
||||
sid: Number(process.env["BONOB_SONOS_SERVICE_ID"] || "246"),
|
||||
bnbEnvVar("SONOS_AUTO_REGISTER", { default: "false" }) == "true",
|
||||
sid: Number(bnbEnvVar("SONOS_SERVICE_ID", { default: "246" })),
|
||||
},
|
||||
navidrome: {
|
||||
url: process.env["BONOB_NAVIDROME_URL"] || `http://${hostname()}:4533`,
|
||||
customClientsFor:
|
||||
process.env["BONOB_NAVIDROME_CUSTOM_CLIENTS"] || undefined,
|
||||
subsonic: {
|
||||
url: bnbEnvVar("SUBSONIC_URL", { legacy: ["BONOB_NAVIDROME_URL"], default: `http://${hostname()}:4533` })!,
|
||||
customClientsFor: bnbEnvVar("SUBSONIC_CUSTOM_CLIENTS", { legacy: ["BONOB_NAVIDROME_CUSTOM_CLIENTS"] }),
|
||||
},
|
||||
scrobbleTracks: (process.env["BONOB_SCROBBLE_TRACKS"] || "true") == "true",
|
||||
scrobbleTracks: bnbEnvVar("SCROBBLE_TRACKS", { default: "true" }) == "true",
|
||||
reportNowPlaying:
|
||||
(process.env["BONOB_REPORT_NOW_PLAYING"] || "true") == "true",
|
||||
bnbEnvVar("REPORT_NOW_PLAYING", { default: "true" }) == "true",
|
||||
};
|
||||
}
|
||||
|
||||
10
src/i8n.ts
10
src/i8n.ts
@@ -41,7 +41,7 @@ export type KEY =
|
||||
|
||||
const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
"en-US": {
|
||||
AppLinkMessage: "Linking sonos with $BONOB_SONOS_SERVICE_NAME",
|
||||
AppLinkMessage: "Linking sonos with $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artists",
|
||||
albums: "Albums",
|
||||
tracks: "Tracks",
|
||||
@@ -62,7 +62,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
devices: "Devices",
|
||||
services: "Services",
|
||||
login: "Login",
|
||||
logInToBonob: "Log in to $BONOB_SONOS_SERVICE_NAME",
|
||||
logInToBonob: "Log in to $BNB_SONOS_SERVICE_NAME",
|
||||
username: "Username",
|
||||
password: "Password",
|
||||
successfullyRegistered: "Successfully registered",
|
||||
@@ -75,7 +75,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
noSonosDevices: "No sonos devices",
|
||||
},
|
||||
"nl-NL": {
|
||||
AppLinkMessage: "Sonos koppelen aan $BONOB_SONOS_SERVICE_NAME",
|
||||
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artiesten",
|
||||
albums: "Albums",
|
||||
tracks: "Nummers",
|
||||
@@ -96,7 +96,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
devices: "Apparaten",
|
||||
services: "Services",
|
||||
login: "Inloggen",
|
||||
logInToBonob: "Login op $BONOB_SONOS_SERVICE_NAME",
|
||||
logInToBonob: "Login op $BNB_SONOS_SERVICE_NAME",
|
||||
username: "Gebruikersnaam",
|
||||
password: "Wachtwoord",
|
||||
successfullyRegistered: "Registratie geslaagd",
|
||||
@@ -151,7 +151,7 @@ export default (serviceName: string): I8N =>
|
||||
translations["en-US"];
|
||||
return (key: KEY) => {
|
||||
const value = langToUse[key]?.replace(
|
||||
"$BONOB_SONOS_SERVICE_NAME",
|
||||
"$BNB_SONOS_SERVICE_NAME",
|
||||
serviceName
|
||||
);
|
||||
if (value) return value;
|
||||
|
||||
@@ -52,6 +52,7 @@ export type AlbumSummary = {
|
||||
name: string;
|
||||
year: string | undefined;
|
||||
genre: Genre | undefined;
|
||||
coverArt: string | undefined;
|
||||
|
||||
artistName: string;
|
||||
artistId: string;
|
||||
@@ -71,6 +72,7 @@ export type Track = {
|
||||
duration: number;
|
||||
number: number | undefined;
|
||||
genre: Genre | undefined;
|
||||
coverArt: string | undefined;
|
||||
album: AlbumSummary;
|
||||
artist: ArtistSummary;
|
||||
};
|
||||
@@ -118,6 +120,7 @@ export const albumToAlbumSummary = (it: Album): AlbumSummary => ({
|
||||
genre: it.genre,
|
||||
artistName: it.artistName,
|
||||
artistId: it.artistId,
|
||||
coverArt: it.coverArt
|
||||
});
|
||||
|
||||
export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({
|
||||
@@ -174,7 +177,7 @@ export interface MusicLibrary {
|
||||
trackId: string;
|
||||
range: string | undefined;
|
||||
}): Promise<TrackStream>;
|
||||
coverArt(id: string, type: "album" | "artist", size?: number): Promise<CoverArt | undefined>;
|
||||
coverArt(id: string, size?: number): Promise<CoverArt | undefined>;
|
||||
nowPlaying(id: string): Promise<boolean>
|
||||
scrobble(id: string): Promise<boolean>
|
||||
searchArtists(query: string): Promise<ArtistSummary[]>;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LOGIN_ROUTE,
|
||||
CREATE_REGISTRATION_ROUTE,
|
||||
REMOVE_REGISTRATION_ROUTE,
|
||||
sonosifyMimeType,
|
||||
} from "./smapi";
|
||||
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||
import { MusicService, isSuccess } from "./music_service";
|
||||
@@ -317,6 +318,13 @@ function server(
|
||||
}, headers=(${JSON.stringify(stream.headers)})`
|
||||
);
|
||||
|
||||
const sonosisfyContentType = (contentType: string) =>
|
||||
contentType
|
||||
.split(";")
|
||||
.map((it) => it.trim())
|
||||
.map((it) => sonosifyMimeType(it))
|
||||
.join("; ");
|
||||
|
||||
const respondWith = ({
|
||||
status,
|
||||
filter,
|
||||
@@ -326,7 +334,7 @@ function server(
|
||||
}: {
|
||||
status: number;
|
||||
filter: Transform;
|
||||
headers: Record<string, string | undefined>;
|
||||
headers: Record<string, string>;
|
||||
sendStream: boolean;
|
||||
nowPlaying: boolean;
|
||||
}) => {
|
||||
@@ -340,9 +348,11 @@ function server(
|
||||
: Promise.resolve(true)
|
||||
).then((_) => {
|
||||
res.status(status);
|
||||
Object.entries(stream.headers)
|
||||
Object.entries(headers)
|
||||
.filter(([_, v]) => v !== undefined)
|
||||
.forEach(([header, value]) => res.setHeader(header, value));
|
||||
.forEach(([header, value]) => {
|
||||
res.setHeader(header, value!);
|
||||
});
|
||||
if (sendStream) stream.stream.pipe(filter).pipe(res);
|
||||
else res.send();
|
||||
});
|
||||
@@ -353,7 +363,9 @@ function server(
|
||||
status: 200,
|
||||
filter: new PassThrough(),
|
||||
headers: {
|
||||
"content-type": stream.headers["content-type"],
|
||||
"content-type": sonosisfyContentType(
|
||||
stream.headers["content-type"]
|
||||
),
|
||||
"content-length": stream.headers["content-length"],
|
||||
"accept-ranges": stream.headers["accept-ranges"],
|
||||
},
|
||||
@@ -365,7 +377,9 @@ function server(
|
||||
status: 206,
|
||||
filter: new PassThrough(),
|
||||
headers: {
|
||||
"content-type": stream.headers["content-type"],
|
||||
"content-type": sonosisfyContentType(
|
||||
stream.headers["content-type"]
|
||||
),
|
||||
"content-length": stream.headers["content-length"],
|
||||
"content-range": stream.headers["content-range"],
|
||||
"accept-ranges": stream.headers["accept-ranges"],
|
||||
@@ -457,25 +471,22 @@ function server(
|
||||
"centre",
|
||||
];
|
||||
|
||||
app.get("/art/:type/:ids/size/:size", (req, res) => {
|
||||
app.get("/art/:ids/size/:size", (req, res) => {
|
||||
const authToken = accessTokens.authTokenFor(
|
||||
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
|
||||
);
|
||||
const type = req.params["type"]!;
|
||||
const ids = req.params["ids"]!.split("&");
|
||||
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 if (!(size > 0)) {
|
||||
return res.status(400).send();
|
||||
}
|
||||
|
||||
return musicService
|
||||
.login(authToken)
|
||||
.then((it) => Promise.all(ids.map((id) => it.coverArt(id, type, size))))
|
||||
.then((it) => Promise.all(ids.map((id) => it.coverArt(id, size))))
|
||||
.then((coverArts) => coverArts.filter((it) => it))
|
||||
.then(shuffle)
|
||||
.then((coverArts) => {
|
||||
@@ -513,12 +524,9 @@ function server(
|
||||
}
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
logger.error(
|
||||
`Failed fetching image ${type}/${ids.join("&")}/size/${size}`,
|
||||
{
|
||||
cause: e,
|
||||
}
|
||||
);
|
||||
logger.error(`Failed fetching image ${ids.join("&")}/size/${size}`, {
|
||||
cause: e,
|
||||
});
|
||||
return res.status(500).send();
|
||||
});
|
||||
});
|
||||
|
||||
59
src/smapi.ts
59
src/smapi.ts
@@ -215,10 +215,7 @@ const genre = (bonobUrl: URLBuilder, genre: Genre) => ({
|
||||
itemType: "container",
|
||||
id: `genre:${genre.id}`,
|
||||
title: genre.name,
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
iconForGenre(genre.name)
|
||||
).href(),
|
||||
albumArtURI: iconArtURI(bonobUrl, iconForGenre(genre.name)).href(),
|
||||
});
|
||||
|
||||
const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
|
||||
@@ -238,31 +235,37 @@ export const playlistAlbumArtURL = (
|
||||
bonobUrl: URLBuilder,
|
||||
playlist: Playlist
|
||||
) => {
|
||||
const ids = uniq(playlist.entries.map((it) => it.album?.id).filter((it) => it));
|
||||
const ids = uniq(
|
||||
playlist.entries.map((it) => it.coverArt).filter((it) => it)
|
||||
);
|
||||
if (ids.length == 0) {
|
||||
return iconArtURI(bonobUrl, "error");
|
||||
} else {
|
||||
return bonobUrl.append({
|
||||
pathname: `/art/album/${ids.slice(0, 9).join("&")}/size/180`
|
||||
pathname: `/art/${ids.slice(0, 9).join("&")}/size/180`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) =>
|
||||
bonobUrl.append({ pathname: `/art/album/${album.id}/size/180` });
|
||||
|
||||
export const iconArtURI = (
|
||||
export const defaultAlbumArtURI = (
|
||||
bonobUrl: URLBuilder,
|
||||
icon: ICON
|
||||
{ coverArt }: { coverArt: string | undefined }
|
||||
) =>
|
||||
coverArt
|
||||
? bonobUrl.append({ pathname: `/art/${coverArt}/size/180` })
|
||||
: iconArtURI(bonobUrl, "vinyl");
|
||||
|
||||
export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON) =>
|
||||
bonobUrl.append({
|
||||
pathname: `/icon/${icon}/size/legacy`
|
||||
pathname: `/icon/${icon}/size/legacy`,
|
||||
});
|
||||
|
||||
export const defaultArtistArtURI = (
|
||||
bonobUrl: URLBuilder,
|
||||
artist: ArtistSummary
|
||||
) => bonobUrl.append({ pathname: `/art/artist/${artist.id}/size/180` });
|
||||
) => bonobUrl.append({ pathname: `/art/artist:${artist.id}/size/180` });
|
||||
|
||||
export const sonosifyMimeType = (mimeType: string) => mimeType == "audio/x-flac" ? "audio/flac" : mimeType;
|
||||
|
||||
export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
|
||||
itemType: "album",
|
||||
@@ -281,17 +284,17 @@ export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
|
||||
export const track = (bonobUrl: URLBuilder, track: Track) => ({
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
mimeType: track.mimeType,
|
||||
mimeType: sonosifyMimeType(track.mimeType),
|
||||
title: track.name,
|
||||
|
||||
trackMetadata: {
|
||||
album: track.album.name,
|
||||
albumId: track.album.id,
|
||||
albumId: `album:${track.album.id}`,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: track.artist.id,
|
||||
albumArtURI: defaultAlbumArtURI(bonobUrl, track.album).href(),
|
||||
albumArtistId: `artist:${track.artist.id}`,
|
||||
albumArtURI: defaultAlbumArtURI(bonobUrl, track).href(),
|
||||
artist: track.artist.name,
|
||||
artistId: track.artist.id,
|
||||
artistId: `artist:${track.artist.id}`,
|
||||
duration: track.duration,
|
||||
genre: track.album.genre?.name,
|
||||
genreId: track.album.genre?.id,
|
||||
@@ -368,7 +371,7 @@ function bindSmapiSoapServiceToExpress(
|
||||
const urlWithToken = (accessToken: string) =>
|
||||
bonobUrl.append({
|
||||
searchParams: {
|
||||
"bat": accessToken,
|
||||
bat: accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -506,23 +509,7 @@ function bindSmapiSoapServiceToExpress(
|
||||
return musicLibrary.track(typeId).then((it) => ({
|
||||
getExtendedMetadataResult: {
|
||||
mediaMetadata: {
|
||||
id: `track:${it.id}`,
|
||||
itemType: "track",
|
||||
title: it.name,
|
||||
mimeType: it.mimeType,
|
||||
trackMetadata: {
|
||||
artistId: `artist:${it.artist.id}`,
|
||||
artist: it.artist.name,
|
||||
albumId: `album:${it.album.id}`,
|
||||
album: it.album.name,
|
||||
genre: it.genre?.name,
|
||||
genreId: it.genre?.id,
|
||||
duration: it.duration,
|
||||
albumArtURI: defaultAlbumArtURI(
|
||||
urlWithToken(accessToken),
|
||||
it.album
|
||||
).href(),
|
||||
},
|
||||
...track(urlWithToken(accessToken), it)
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -148,7 +148,7 @@ export type song = {
|
||||
_artist: string;
|
||||
_track: string | undefined;
|
||||
_genre: string;
|
||||
_coverArt: string;
|
||||
_coverArt: string | undefined;
|
||||
_created: "2004-11-08T23:36:11";
|
||||
_duration: string | undefined;
|
||||
_bitRate: "128";
|
||||
@@ -179,6 +179,7 @@ export type entry = {
|
||||
_track: string;
|
||||
_year: string;
|
||||
_genre: string;
|
||||
_coverArt: string;
|
||||
_contentType: string;
|
||||
_duration: string;
|
||||
_albumId: string;
|
||||
@@ -223,6 +224,12 @@ export function isError(
|
||||
return (subsonicResponse as SubsonicError).error !== undefined;
|
||||
}
|
||||
|
||||
export const splitCoverArtId = (coverArt: string): [string, string] => {
|
||||
const parts = coverArt.split(":").filter(it => it.length > 0);
|
||||
if(parts.length < 2) throw `'${coverArt}' is an invalid coverArt id'`
|
||||
return [parts[0]!, parts.slice(1).join(":")];
|
||||
};
|
||||
|
||||
export type IdName = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -239,6 +246,8 @@ export type getAlbumListParams = {
|
||||
|
||||
export const MAX_ALBUM_LIST = 500;
|
||||
|
||||
const maybeAsCoverArt = (coverArt: string | undefined) => coverArt ? `coverArt:${coverArt}` : undefined
|
||||
|
||||
const asTrack = (album: Album, song: song) => ({
|
||||
id: song._id,
|
||||
name: song._title,
|
||||
@@ -246,6 +255,7 @@ const asTrack = (album: Album, song: song) => ({
|
||||
duration: parseInt(song._duration || "0"),
|
||||
number: parseInt(song._track || "0"),
|
||||
genre: maybeAsGenre(song._genre),
|
||||
coverArt: maybeAsCoverArt(song._coverArt),
|
||||
album,
|
||||
artist: {
|
||||
id: song._artistId,
|
||||
@@ -260,6 +270,7 @@ const asAlbum = (album: album) => ({
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
});
|
||||
|
||||
export const asGenre = (genreName: string) => ({
|
||||
@@ -298,7 +309,7 @@ export const asURLSearchParams = (q: any) => {
|
||||
return urlSearchParams;
|
||||
};
|
||||
|
||||
export class Navidrome implements MusicService {
|
||||
export class Subsonic implements MusicService {
|
||||
url: string;
|
||||
encryption: Encryption;
|
||||
streamClientApplication: StreamClientApplication;
|
||||
@@ -335,7 +346,7 @@ export class Navidrome implements MusicService {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status != 200 && response.status != 206) {
|
||||
throw `Navidrome failed with a ${response.status || "no!"} status`;
|
||||
throw `Subsonic failed with a ${response.status || "no!"} status`;
|
||||
} else return response;
|
||||
});
|
||||
|
||||
@@ -368,7 +379,7 @@ export class Navidrome implements MusicService {
|
||||
)
|
||||
.then((json) => json["subsonic-response"])
|
||||
.then((json) => {
|
||||
if (isError(json)) throw `Navidrome error:${json.error._message}`;
|
||||
if (isError(json)) throw `Subsonic error:${json.error._message}`;
|
||||
else return json as unknown as T;
|
||||
});
|
||||
|
||||
@@ -427,6 +438,7 @@ export class Navidrome implements MusicService {
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
}));
|
||||
|
||||
getArtist = (
|
||||
@@ -440,14 +452,7 @@ export class Navidrome implements MusicService {
|
||||
.then((it) => ({
|
||||
id: it._id,
|
||||
name: it._name,
|
||||
albums: (it.album || []).map((album) => ({
|
||||
id: album._id,
|
||||
name: album._name,
|
||||
year: album._year,
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: it._id,
|
||||
artistName: it._name,
|
||||
})),
|
||||
albums: this.toAlbumSummary(it.album || []),
|
||||
}));
|
||||
|
||||
getArtistWithInfo = (credentials: Credentials, id: string) =>
|
||||
@@ -487,6 +492,7 @@ export class Navidrome implements MusicService {
|
||||
genre: maybeAsGenre(album._genre),
|
||||
artistId: album._artistId,
|
||||
artistName: album._artist,
|
||||
coverArt: maybeAsCoverArt(album._coverArt)
|
||||
}));
|
||||
|
||||
search3 = (credentials: Credentials, q: any) =>
|
||||
@@ -602,14 +608,16 @@ export class Navidrome implements MusicService {
|
||||
stream: res.data,
|
||||
}))
|
||||
),
|
||||
coverArt: async (id: string, type: "album" | "artist", size?: number) => {
|
||||
if (type == "album") {
|
||||
coverArt: async (coverArt: string, size?: number) => {
|
||||
const [type, id] = splitCoverArtId(coverArt);
|
||||
if (type == "coverArt") {
|
||||
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) => {
|
||||
const albumsWithCoverArt = artist.albums.filter(it => it.coverArt);
|
||||
if (artist.image.large) {
|
||||
return axios
|
||||
.get(artist.image.large!, {
|
||||
@@ -633,9 +641,9 @@ export class Navidrome implements MusicService {
|
||||
};
|
||||
}
|
||||
});
|
||||
} else if (artist.albums.length > 0) {
|
||||
} else if (albumsWithCoverArt.length > 0) {
|
||||
return navidrome
|
||||
.getCoverArt(credentials, artist.albums[0]!.id, size)
|
||||
.getCoverArt(credentials, splitCoverArtId(albumsWithCoverArt[0]!.coverArt!)[1], size)
|
||||
.then((res) => ({
|
||||
contentType: res.headers["content-type"],
|
||||
data: Buffer.from(res.data, "binary"),
|
||||
@@ -708,6 +716,7 @@ export class Navidrome implements MusicService {
|
||||
duration: parseInt(entry._duration || "0"),
|
||||
number: trackNumber++,
|
||||
genre: maybeAsGenre(entry._genre),
|
||||
coverArt: maybeAsCoverArt(entry._coverArt),
|
||||
album: {
|
||||
id: entry._albumId,
|
||||
name: entry._album,
|
||||
@@ -715,6 +724,7 @@ export class Navidrome implements MusicService {
|
||||
genre: maybeAsGenre(entry._genre),
|
||||
artistName: entry._artist,
|
||||
artistId: entry._artistId,
|
||||
coverArt: maybeAsCoverArt(entry._coverArt)
|
||||
},
|
||||
artist: {
|
||||
id: entry._artistId,
|
||||
Reference in New Issue
Block a user