mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to query artists from navidrome with paging
This commit is contained in:
@@ -18,7 +18,7 @@ const app = server(
|
||||
sonos(process.env["BONOB_SONOS_SEED_HOST"]),
|
||||
bonob,
|
||||
WEB_ADDRESS,
|
||||
new Navidrome(process.env["BONOB_NAVIDROME_URL"] || "http://localhost:4533", encryption())
|
||||
new Navidrome(process.env["BONOB_NAVIDROME_URL"] || "http://localhost:4533", encryption(process.env["BONOB_SECRET"] || "bonob"))
|
||||
);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
||||
import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
|
||||
|
||||
const ALGORITHM = "aes-256-cbc"
|
||||
const IV = randomBytes(16);
|
||||
const KEY = randomBytes(32);
|
||||
|
||||
export type Hash = {
|
||||
iv: string,
|
||||
@@ -14,17 +13,18 @@ export type Encryption = {
|
||||
decrypt: (hash: Hash) => string
|
||||
}
|
||||
|
||||
const encryption = (): Encryption => {
|
||||
const encryption = (secret: string): Encryption => {
|
||||
const key = createHash('sha256').update(String(secret)).digest('base64').substr(0, 32);
|
||||
return {
|
||||
encrypt: (value: string) => {
|
||||
const cipher = createCipheriv(ALGORITHM, KEY, IV);
|
||||
const cipher = createCipheriv(ALGORITHM, key, IV);
|
||||
return {
|
||||
iv: IV.toString("hex"),
|
||||
encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex")
|
||||
};
|
||||
},
|
||||
decrypt: (hash: Hash) => {
|
||||
const decipher = createDecipheriv(ALGORITHM, KEY, Buffer.from(hash.iv, 'hex'));
|
||||
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(hash.iv, 'hex'));
|
||||
return Buffer.concat([decipher.update(Buffer.from(hash.encryptedData, 'hex')), decipher.final()]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export type Credentials = { username: string; password: string };
|
||||
|
||||
export function isSuccess(
|
||||
@@ -38,8 +37,19 @@ export type Album = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Paging = {
|
||||
_index?: number;
|
||||
_count?: number;
|
||||
};
|
||||
|
||||
export interface MusicLibrary {
|
||||
artists(): Artist[];
|
||||
artists({ _index, _count }: Paging): Promise<[Artist[], number]>;
|
||||
artist(id: string): Artist;
|
||||
albums({ artistId, _index, _count }: { artistId?: string, _index?: number, _count?: number }): Album[];
|
||||
albums({
|
||||
artistId,
|
||||
_index,
|
||||
_count,
|
||||
}: {
|
||||
artistId?: string;
|
||||
} & Paging): Promise<[Album[], number]>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Md5 } from "ts-md5/dist/md5";
|
||||
import { Credentials, MusicService } from "./music_service";
|
||||
import { Credentials, MusicService, Paging, Album, Artist } from "./music_service";
|
||||
import X2JS from "x2js";
|
||||
|
||||
import axios from "axios";
|
||||
@@ -25,6 +25,15 @@ export type SubsonicResponse = {
|
||||
_status: string;
|
||||
};
|
||||
|
||||
export type GetArtistsResponse = SubsonicResponse & {
|
||||
artists: {
|
||||
index: {
|
||||
artist: { _id: string; _name: string; _albumCount: string }[];
|
||||
_name: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SubsonicError = SubsonicResponse & {
|
||||
error: {
|
||||
_code: string;
|
||||
@@ -47,11 +56,11 @@ export class Navidrome implements MusicService {
|
||||
this.encryption = encryption;
|
||||
}
|
||||
|
||||
get = async (
|
||||
get = async <T>(
|
||||
{ username, password }: Credentials,
|
||||
path: string,
|
||||
q: {} = {}
|
||||
): Promise<SubsonicResponse> =>
|
||||
): Promise<T> =>
|
||||
axios
|
||||
.get(`${this.url}${path}`, {
|
||||
params: {
|
||||
@@ -66,11 +75,11 @@ export class Navidrome implements MusicService {
|
||||
.then((json) => json["subsonic-response"])
|
||||
.then((json) => {
|
||||
if (isError(json)) throw json.error._message;
|
||||
else return json;
|
||||
else return (json as unknown) as T;
|
||||
});
|
||||
|
||||
generateToken = async (credentials: Credentials) =>
|
||||
this.get(credentials, "/rest/ping.view").then((_) => ({
|
||||
this.get(credentials, "/rest/ping.view").then(() => ({
|
||||
authToken: Buffer.from(
|
||||
JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials)))
|
||||
).toString("base64"),
|
||||
@@ -85,17 +94,33 @@ export class Navidrome implements MusicService {
|
||||
)
|
||||
);
|
||||
|
||||
async login(_: string) {
|
||||
// const credentials: Credentials = this.parseToken(token);
|
||||
async login(token: string) {
|
||||
const navidrome = this;
|
||||
const credentials: Credentials = this.parseToken(token);
|
||||
return Promise.resolve({
|
||||
artists: () => [],
|
||||
artists: ({ _index, _count }: Paging): Promise<[Artist[], number]> =>
|
||||
navidrome
|
||||
.get<GetArtistsResponse>(credentials, "/rest/getArtists")
|
||||
.then((it) => it.artists.index.flatMap((it) => it.artist))
|
||||
.then((artists) =>
|
||||
artists.map((it) => ({ id: it._id, name: it._name }))
|
||||
)
|
||||
.then((artists) => {
|
||||
const i0 = _index || 0;
|
||||
const i1 = _count ? i0 + _count : undefined;
|
||||
return [artists.slice(i0, i1), artists.length];
|
||||
}),
|
||||
artist: (id: string) => ({
|
||||
id,
|
||||
name: id,
|
||||
}),
|
||||
albums: ({ artistId }: { artistId?: string }) => {
|
||||
albums: ({
|
||||
artistId,
|
||||
}: {
|
||||
artistId?: string;
|
||||
} & Paging): Promise<[Album[], number]> => {
|
||||
console.log(artistId);
|
||||
return [];
|
||||
return Promise.resolve([[], 0]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
85
src/smapi.ts
85
src/smapi.ts
@@ -6,7 +6,7 @@ import path from "path";
|
||||
import logger from "./logger";
|
||||
|
||||
import { LinkCodes } from "./link_codes";
|
||||
import { MusicLibrary, MusicService } from "./music_service";
|
||||
import { MusicLibrary, MusicService } from "./music_service";
|
||||
|
||||
export const LOGIN_ROUTE = "/login";
|
||||
export const SOAP_PATH = "/ws/sonos";
|
||||
@@ -64,14 +64,18 @@ export type GetMetadataResponse = {
|
||||
|
||||
export function getMetadataResult({
|
||||
mediaCollection,
|
||||
index,
|
||||
total,
|
||||
}: {
|
||||
mediaCollection: any[] | undefined;
|
||||
index: number;
|
||||
total: number;
|
||||
}): GetMetadataResponse {
|
||||
return {
|
||||
getMetadataResult: {
|
||||
count: mediaCollection?.length || 0,
|
||||
index: 0,
|
||||
total: mediaCollection?.length || 0,
|
||||
index,
|
||||
total,
|
||||
mediaCollection: mediaCollection || [],
|
||||
},
|
||||
};
|
||||
@@ -181,8 +185,8 @@ function bindSmapiSoapServiceToExpress(
|
||||
id,
|
||||
index,
|
||||
count,
|
||||
// recursive,
|
||||
}: { id: string; index: number; count: number; recursive: boolean },
|
||||
}: // recursive,
|
||||
{ id: string; index: number; count: number; recursive: boolean },
|
||||
_,
|
||||
headers?: SoapyHeaders
|
||||
) => {
|
||||
@@ -194,21 +198,22 @@ function bindSmapiSoapServiceToExpress(
|
||||
},
|
||||
};
|
||||
}
|
||||
const login = await musicService.login(
|
||||
headers.credentials.loginToken.token
|
||||
).catch(_ => {
|
||||
throw {
|
||||
Fault: {
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
},
|
||||
};
|
||||
});
|
||||
const login = await musicService
|
||||
.login(headers.credentials.loginToken.token)
|
||||
.catch((_) => {
|
||||
throw {
|
||||
Fault: {
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const musicLibrary = login as MusicLibrary;
|
||||
|
||||
// const [type, typeId] = id.split(":");
|
||||
const type = id;
|
||||
const [type, typeId] = id.split(":");
|
||||
const paging = { _index: index, _count: count };
|
||||
logger.debug(`Fetching type=${type}, typeId=${typeId}`);
|
||||
switch (type) {
|
||||
case "root":
|
||||
return getMetadataResult({
|
||||
@@ -216,27 +221,39 @@ function bindSmapiSoapServiceToExpress(
|
||||
container({ id: "artists", title: "Artists" }),
|
||||
container({ id: "albums", title: "Albums" }),
|
||||
],
|
||||
index: 0,
|
||||
total: 2,
|
||||
});
|
||||
case "artists":
|
||||
return getMetadataResult({
|
||||
mediaCollection: musicLibrary.artists().map((it) =>
|
||||
container({
|
||||
id: `artist:${it.id}`,
|
||||
title: it.name,
|
||||
return await musicLibrary
|
||||
.artists(paging)
|
||||
.then(([artists, total]) =>
|
||||
getMetadataResult({
|
||||
mediaCollection: artists.map((it) =>
|
||||
container({
|
||||
id: `artist:${it.id}`,
|
||||
title: it.name,
|
||||
})
|
||||
),
|
||||
index: paging._index,
|
||||
total,
|
||||
})
|
||||
),
|
||||
});
|
||||
);
|
||||
case "albums":
|
||||
return getMetadataResult({
|
||||
mediaCollection: musicLibrary
|
||||
.albums({ _index: index, _count: count })
|
||||
.map((it) =>
|
||||
container({
|
||||
id: `album:${it.id}`,
|
||||
title: it.name,
|
||||
})
|
||||
),
|
||||
});
|
||||
return await musicLibrary
|
||||
.albums(paging)
|
||||
.then(([albums, total]) =>
|
||||
getMetadataResult({
|
||||
mediaCollection: albums.map((it) =>
|
||||
container({
|
||||
id: `album:${it.id}`,
|
||||
title: it.name,
|
||||
})
|
||||
),
|
||||
index: paging._index,
|
||||
total,
|
||||
})
|
||||
);
|
||||
default:
|
||||
throw `Unsupported id:${id}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user