Change ND genre ids to b64 encoded strings of genre, so as to differentiate between genre name and id (#54)

This commit is contained in:
Simon J
2021-09-21 10:53:02 +10:00
committed by GitHub
parent be4fcdff24
commit d508eaebcf
8 changed files with 68 additions and 53 deletions

View File

@@ -5,6 +5,7 @@ import crypto from "crypto";
import { Encryption } from "./encryption"; import { Encryption } from "./encryption";
import logger from "./logger"; import logger from "./logger";
import { Clock, SystemClock } from "./clock"; import { Clock, SystemClock } from "./clock";
import { b64Encode, b64Decode } from "./b64";
type AccessToken = { type AccessToken = {
value: string; value: string;
@@ -60,14 +61,12 @@ export class EncryptedAccessTokens implements AccessTokens {
} }
mint = (authToken: string): string => mint = (authToken: string): string =>
Buffer.from(JSON.stringify(this.encryption.encrypt(authToken))).toString( b64Encode(JSON.stringify(this.encryption.encrypt(authToken)));
"base64"
);
authTokenFor(value: string): string | undefined { authTokenFor(value: string): string | undefined {
try { try {
return this.encryption.decrypt( return this.encryption.decrypt(
JSON.parse(Buffer.from(value, "base64").toString("ascii")) JSON.parse(b64Decode(value))
); );
} catch { } catch {
logger.warn("Failed to decrypt access token..."); logger.warn("Failed to decrypt access token...");

2
src/b64.ts Normal file
View File

@@ -0,0 +1,2 @@
export const b64Encode = (value: string) => Buffer.from(value).toString("base64");
export const b64Decode = (value: string) => Buffer.from(value, "base64").toString("ascii");

View File

@@ -21,11 +21,12 @@ import {
} from "./music_service"; } from "./music_service";
import X2JS from "x2js"; import X2JS from "x2js";
import sharp from "sharp"; import sharp from "sharp";
import _, { pick } from "underscore"; import _ from "underscore";
import axios, { AxiosRequestConfig } from "axios"; import axios, { AxiosRequestConfig } from "axios";
import { Encryption } from "./encryption"; import { Encryption } from "./encryption";
import randomString from "./random_string"; import randomString from "./random_string";
import { b64Encode, b64Decode } from "./b64";
export const BROWSER_HEADERS = { export const BROWSER_HEADERS = {
accept: accept:
@@ -262,7 +263,7 @@ const asAlbum = (album: album) => ({
}); });
export const asGenre = (genreName: string) => ({ export const asGenre = (genreName: string) => ({
id: genreName, id: b64Encode(genreName),
name: genreName, name: genreName,
}); });
@@ -374,20 +375,16 @@ export class Navidrome implements MusicService {
generateToken = async (credentials: Credentials) => generateToken = async (credentials: Credentials) =>
this.getJSON(credentials, "/rest/ping.view") this.getJSON(credentials, "/rest/ping.view")
.then(() => ({ .then(() => ({
authToken: Buffer.from( authToken: b64Encode(
JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials))) JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials)))
).toString("base64"), ),
userId: credentials.username, userId: credentials.username,
nickname: credentials.username, nickname: credentials.username,
})) }))
.catch((e) => ({ message: `${e}` })); .catch((e) => ({ message: `${e}` }));
parseToken = (token: string): Credentials => parseToken = (token: string): Credentials =>
JSON.parse( JSON.parse(this.encryption.decrypt(JSON.parse(b64Decode(token))));
this.encryption.decrypt(
JSON.parse(Buffer.from(token, "base64").toString("ascii"))
)
);
getArtists = ( getArtists = (
credentials: Credentials credentials: Credentials
@@ -519,8 +516,8 @@ export class Navidrome implements MusicService {
})), })),
artist: async (id: string): Promise<Artist> => artist: async (id: string): Promise<Artist> =>
navidrome.getArtistWithInfo(credentials, id), navidrome.getArtistWithInfo(credentials, id),
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> => { albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
return Promise.all([ Promise.all([
navidrome navidrome
.getArtists(credentials) .getArtists(credentials)
.then((it) => .then((it) =>
@@ -528,7 +525,8 @@ export class Navidrome implements MusicService {
), ),
navidrome navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", { .getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
...pick(q, "type", "genre"), type: q.type,
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
size: 500, size: 500,
offset: q._index, offset: q._index,
}) })
@@ -536,12 +534,8 @@ export class Navidrome implements MusicService {
.then(navidrome.toAlbumSummary), .then(navidrome.toAlbumSummary),
]).then(([total, albums]) => ({ ]).then(([total, albums]) => ({
results: albums.slice(0, q._count), results: albums.slice(0, q._count),
total: total: albums.length == 500 ? total : q._index + albums.length,
albums.length == 500 })),
? total
: q._index + albums.length,
}));
},
album: (id: string): Promise<Album> => album: (id: string): Promise<Album> =>
navidrome.getAlbum(credentials, id), navidrome.getAlbum(credentials, id),
genres: () => genres: () =>
@@ -553,7 +547,7 @@ export class Navidrome implements MusicService {
A.filter((it) => Number.parseInt(it._albumCount) > 0), A.filter((it) => Number.parseInt(it._albumCount) > 0),
A.map((it) => it.__text), A.map((it) => it.__text),
A.sort(ordString), A.sort(ordString),
A.map((it) => ({ id: it, name: it })) A.map((it) => ({ id: b64Encode(it), name: it }))
) )
), ),
tracks: (albumId: string) => tracks: (albumId: string) =>

17
tests/b64.test.ts Normal file
View File

@@ -0,0 +1,17 @@
import { b64Encode, b64Decode } from "../src/b64";
describe("b64", () => {
const value = "foobar100";
const encoded = Buffer.from(value).toString("base64");
describe("encode", () => {
it("should encode", () => {
expect(b64Encode(value)).toEqual(encoded);
});
});
describe("decode", () => {
it("should decode", () => {
expect(b64Decode(encoded)).toEqual(value);
});
});
});

View File

@@ -5,6 +5,7 @@ import { Credentials } from "../src/smapi";
import { Service, Device } from "../src/sonos"; import { Service, Device } from "../src/sonos";
import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary, PlaylistSummary, Playlist } from "../src/music_service"; import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary, PlaylistSummary, Playlist } from "../src/music_service";
import randomString from "../src/random_string"; import randomString from "../src/random_string";
import { b64Encode } from "../src/b64";
const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max)); const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max));
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`; const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`;
@@ -111,16 +112,18 @@ export function anArtist(fields: Partial<Artist> = {}): Artist {
return artist; return artist;
} }
export const HIP_HOP = { id: "genre_hip_hop", name: "Hip-Hop" }; export const aGenre = (name: string) => ({ id: b64Encode(name), name })
export const METAL = { id: "genre_metal", name: "Metal" };
export const NEW_WAVE = { id: "genre_new_wave", name: "New Wave" }; export const HIP_HOP = aGenre("Hip-Hop");
export const POP = { id: "genre_pop", name: "Pop" }; export const METAL = aGenre("Metal");
export const POP_ROCK = { id: "genre_pop_rock", name: "Pop Rock" }; export const NEW_WAVE = aGenre("New Wave");
export const REGGAE = { id: "genre_reggae", name: "Reggae" }; export const POP = aGenre("Pop");
export const ROCK = { id: "genre_rock", name: "Rock" }; export const POP_ROCK = aGenre("Pop Rock");
export const SKA = { id: "genre_ska", name: "Ska" }; export const REGGAE = aGenre("Reggae");
export const PUNK = { id: "genre_punk", name: "Punk" }; export const ROCK = aGenre("Rock");
export const TRIP_HOP = { id: "genre_trip_hop", name: "Trip Hop" }; export const SKA = aGenre("Ska");
export const PUNK = aGenre("Punk");
export const TRIP_HOP = aGenre("Trip Hop");
export const SAMPLE_GENRES = [HIP_HOP, METAL, NEW_WAVE, POP, POP_ROCK, REGGAE, ROCK, SKA]; export const SAMPLE_GENRES = [HIP_HOP, METAL, NEW_WAVE, POP, POP_ROCK, REGGAE, ROCK, SKA];
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)]; export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];

View File

@@ -467,9 +467,9 @@ describe("InMemoryMusicService", () => {
it("should provide an array of artists", async () => { it("should provide an array of artists", async () => {
expect(await musicLibrary.genres()).toEqual([ expect(await musicLibrary.genres()).toEqual([
HIP_HOP, HIP_HOP,
SKA,
POP, POP,
ROCK, ROCK,
SKA,
]); ]);
}); });
}); });

View File

@@ -5,6 +5,8 @@ import { pipe } from "fp-ts/lib/function";
import { ordString, fromCompare } from "fp-ts/lib/Ord"; import { ordString, fromCompare } from "fp-ts/lib/Ord";
import { shuffle } from "underscore"; import { shuffle } from "underscore";
import { b64Encode, b64Decode } from "../src/b64";
import { import {
MusicService, MusicService,
Credentials, Credentials,
@@ -37,9 +39,7 @@ export class InMemoryMusicService implements MusicService {
this.users[username] == password this.users[username] == password
) { ) {
return Promise.resolve({ return Promise.resolve({
authToken: Buffer.from(JSON.stringify({ username, password })).toString( authToken: b64Encode(JSON.stringify({ username, password })),
"base64"
),
userId: username, userId: username,
nickname: username, nickname: username,
}); });
@@ -49,9 +49,7 @@ export class InMemoryMusicService implements MusicService {
} }
login(token: string): Promise<MusicLibrary> { login(token: string): Promise<MusicLibrary> {
const credentials = JSON.parse( const credentials = JSON.parse(b64Decode(token)) as Credentials;
Buffer.from(token, "base64").toString("ascii")
) as Credentials;
if (this.users[credentials.username] != credentials.password) if (this.users[credentials.username] != credentials.password)
return Promise.reject("Invalid auth token"); return Promise.reject("Invalid auth token");

View File

@@ -38,12 +38,14 @@ import {
SimilarArtist, SimilarArtist,
} from "../src/music_service"; } from "../src/music_service";
import { import {
aGenre,
anAlbum, anAlbum,
anArtist, anArtist,
aPlaylist, aPlaylist,
aPlaylistSummary, aPlaylistSummary,
aTrack, aTrack,
} from "./builders"; } from "./builders";
import { b64Encode } from "../src/b64";
describe("t", () => { describe("t", () => {
it("should be an md5 of the password and the salt", () => { it("should be an md5 of the password and the salt", () => {
@@ -547,7 +549,7 @@ describe("Navidrome", () => {
.then((it) => navidrome.login(it.authToken)) .then((it) => navidrome.login(it.authToken))
.then((it) => it.genres()); .then((it) => it.genres());
expect(result).toEqual([{ id: "genre1", name: "genre1" }]); expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]);
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, {
params: asURLSearchParams(authParams), params: asURLSearchParams(authParams),
@@ -579,10 +581,10 @@ describe("Navidrome", () => {
.then((it) => it.genres()); .then((it) => it.genres());
expect(result).toEqual([ expect(result).toEqual([
{ id: "g1", name: "g1" }, { id: b64Encode("g1"), name: "g1" },
{ id: "g2", name: "g2" }, { id: b64Encode("g2"), name: "g2" },
{ id: "g3", name: "g3" }, { id: b64Encode("g3"), name: "g3" },
{ id: "g4", name: "g4" }, { id: b64Encode("g4"), name: "g4" },
]); ]);
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, {
@@ -1234,11 +1236,11 @@ describe("Navidrome", () => {
); );
}); });
it("should pass the filter to navidrome", async () => { it("should map the 64 encoded genre back into the subsonic genre", async () => {
const q: AlbumQuery = { const q: AlbumQuery = {
_index: 0, _index: 0,
_count: 100, _count: 100,
genre: "Pop", genre: b64Encode("Pop"),
type: "byGenre", type: "byGenre",
}; };
const result = await navidrome const result = await navidrome
@@ -2300,7 +2302,7 @@ describe("Navidrome", () => {
describe("streaming a track", () => { describe("streaming a track", () => {
const trackId = uuid(); const trackId = uuid();
const genre = { id: "foo", name: "foo" }; const genre = aGenre("foo");
const album = anAlbum({ genre }); const album = anAlbum({ genre });
const artist = anArtist({ const artist = anArtist({
@@ -3535,7 +3537,7 @@ describe("Navidrome", () => {
it("should return true", async () => { it("should return true", async () => {
const album = anAlbum({ const album = anAlbum({
name: "foo woo", name: "foo woo",
genre: { id: "pop", name: "pop" }, genre: { id: b64Encode("pop"), name: "pop" },
}); });
const artist = anArtist({ name: "#1", albums: [album] }); const artist = anArtist({ name: "#1", albums: [album] });
@@ -3570,13 +3572,13 @@ describe("Navidrome", () => {
it("should return true", async () => { it("should return true", async () => {
const album1 = anAlbum({ const album1 = anAlbum({
name: "album1", name: "album1",
genre: { id: "pop", name: "pop" }, genre: { id: b64Encode("pop"), name: "pop" },
}); });
const artist1 = anArtist({ name: "artist1", albums: [album1] }); const artist1 = anArtist({ name: "artist1", albums: [album1] });
const album2 = anAlbum({ const album2 = anAlbum({
name: "album2", name: "album2",
genre: { id: "pop", name: "pop" }, genre: { id: b64Encode("pop"), name: "pop" },
}); });
const artist2 = anArtist({ name: "artist2", albums: [album2] }); const artist2 = anArtist({ name: "artist2", albums: [album2] });
@@ -3904,11 +3906,11 @@ describe("Navidrome", () => {
const id = uuid(); const id = uuid();
const name = "Great Playlist"; const name = "Great Playlist";
const track1 = aTrack({ const track1 = aTrack({
genre: { id: "pop", name: "pop" }, genre: { id: b64Encode("pop"), name: "pop" },
number: 66, number: 66,
}); });
const track2 = aTrack({ const track2 = aTrack({
genre: { id: "rock", name: "rock" }, genre: { id: b64Encode("rock"), name: "rock" },
number: 77, number: 77,
}); });