From d508eaebcffdb30ac8085b3b1b45fe14eb352aeb Mon Sep 17 00:00:00 2001 From: Simon J Date: Tue, 21 Sep 2021 10:53:02 +1000 Subject: [PATCH] Change ND genre ids to b64 encoded strings of genre, so as to differentiate between genre name and id (#54) --- src/access_tokens.ts | 7 +++--- src/b64.ts | 2 ++ src/navidrome.ts | 32 +++++++++++---------------- tests/b64.test.ts | 17 ++++++++++++++ tests/builders.ts | 23 ++++++++++--------- tests/in_memory_music_service.test.ts | 2 +- tests/in_memory_music_service.ts | 10 ++++----- tests/navidrome.test.ts | 28 ++++++++++++----------- 8 files changed, 68 insertions(+), 53 deletions(-) create mode 100644 src/b64.ts create mode 100644 tests/b64.test.ts diff --git a/src/access_tokens.ts b/src/access_tokens.ts index 9be8d1b..6750d60 100644 --- a/src/access_tokens.ts +++ b/src/access_tokens.ts @@ -5,6 +5,7 @@ import crypto from "crypto"; import { Encryption } from "./encryption"; import logger from "./logger"; import { Clock, SystemClock } from "./clock"; +import { b64Encode, b64Decode } from "./b64"; type AccessToken = { value: string; @@ -60,14 +61,12 @@ export class EncryptedAccessTokens implements AccessTokens { } mint = (authToken: string): string => - Buffer.from(JSON.stringify(this.encryption.encrypt(authToken))).toString( - "base64" - ); + b64Encode(JSON.stringify(this.encryption.encrypt(authToken))); authTokenFor(value: string): string | undefined { try { return this.encryption.decrypt( - JSON.parse(Buffer.from(value, "base64").toString("ascii")) + JSON.parse(b64Decode(value)) ); } catch { logger.warn("Failed to decrypt access token..."); diff --git a/src/b64.ts b/src/b64.ts new file mode 100644 index 0000000..bbc4658 --- /dev/null +++ b/src/b64.ts @@ -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"); \ No newline at end of file diff --git a/src/navidrome.ts b/src/navidrome.ts index 987cb7b..9af913b 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -21,11 +21,12 @@ import { } from "./music_service"; import X2JS from "x2js"; import sharp from "sharp"; -import _, { pick } from "underscore"; +import _ from "underscore"; import axios, { AxiosRequestConfig } from "axios"; import { Encryption } from "./encryption"; import randomString from "./random_string"; +import { b64Encode, b64Decode } from "./b64"; export const BROWSER_HEADERS = { accept: @@ -262,7 +263,7 @@ const asAlbum = (album: album) => ({ }); export const asGenre = (genreName: string) => ({ - id: genreName, + id: b64Encode(genreName), name: genreName, }); @@ -374,20 +375,16 @@ export class Navidrome implements MusicService { generateToken = async (credentials: Credentials) => this.getJSON(credentials, "/rest/ping.view") .then(() => ({ - authToken: Buffer.from( + authToken: b64Encode( JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials))) - ).toString("base64"), + ), userId: credentials.username, nickname: credentials.username, })) .catch((e) => ({ message: `${e}` })); parseToken = (token: string): Credentials => - JSON.parse( - this.encryption.decrypt( - JSON.parse(Buffer.from(token, "base64").toString("ascii")) - ) - ); + JSON.parse(this.encryption.decrypt(JSON.parse(b64Decode(token)))); getArtists = ( credentials: Credentials @@ -519,8 +516,8 @@ export class Navidrome implements MusicService { })), artist: async (id: string): Promise => navidrome.getArtistWithInfo(credentials, id), - albums: async (q: AlbumQuery): Promise> => { - return Promise.all([ + albums: async (q: AlbumQuery): Promise> => + Promise.all([ navidrome .getArtists(credentials) .then((it) => @@ -528,7 +525,8 @@ export class Navidrome implements MusicService { ), navidrome .getJSON(credentials, "/rest/getAlbumList2", { - ...pick(q, "type", "genre"), + type: q.type, + ...(q.genre ? { genre: b64Decode(q.genre) } : {}), size: 500, offset: q._index, }) @@ -536,12 +534,8 @@ export class Navidrome implements MusicService { .then(navidrome.toAlbumSummary), ]).then(([total, albums]) => ({ results: albums.slice(0, q._count), - total: - albums.length == 500 - ? total - : q._index + albums.length, - })); - }, + total: albums.length == 500 ? total : q._index + albums.length, + })), album: (id: string): Promise => navidrome.getAlbum(credentials, id), genres: () => @@ -553,7 +547,7 @@ export class Navidrome implements MusicService { A.filter((it) => Number.parseInt(it._albumCount) > 0), A.map((it) => it.__text), A.sort(ordString), - A.map((it) => ({ id: it, name: it })) + A.map((it) => ({ id: b64Encode(it), name: it })) ) ), tracks: (albumId: string) => diff --git a/tests/b64.test.ts b/tests/b64.test.ts new file mode 100644 index 0000000..a6937ae --- /dev/null +++ b/tests/b64.test.ts @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/tests/builders.ts b/tests/builders.ts index 7bd327a..3382951 100644 --- a/tests/builders.ts +++ b/tests/builders.ts @@ -5,6 +5,7 @@ import { Credentials } from "../src/smapi"; import { Service, Device } from "../src/sonos"; import { Album, Artist, Track, albumToAlbumSummary, artistToArtistSummary, PlaylistSummary, Playlist } from "../src/music_service"; import randomString from "../src/random_string"; +import { b64Encode } from "../src/b64"; const randomInt = (max: number) => Math.floor(Math.random() * Math.floor(max)); const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`; @@ -111,16 +112,18 @@ export function anArtist(fields: Partial = {}): Artist { return artist; } -export const HIP_HOP = { id: "genre_hip_hop", name: "Hip-Hop" }; -export const METAL = { id: "genre_metal", name: "Metal" }; -export const NEW_WAVE = { id: "genre_new_wave", name: "New Wave" }; -export const POP = { id: "genre_pop", name: "Pop" }; -export const POP_ROCK = { id: "genre_pop_rock", name: "Pop Rock" }; -export const REGGAE = { id: "genre_reggae", name: "Reggae" }; -export const ROCK = { id: "genre_rock", name: "Rock" }; -export const SKA = { id: "genre_ska", name: "Ska" }; -export const PUNK = { id: "genre_punk", name: "Punk" }; -export const TRIP_HOP = { id: "genre_trip_hop", name: "Trip Hop" }; +export const aGenre = (name: string) => ({ id: b64Encode(name), name }) + +export const HIP_HOP = aGenre("Hip-Hop"); +export const METAL = aGenre("Metal"); +export const NEW_WAVE = aGenre("New Wave"); +export const POP = aGenre("Pop"); +export const POP_ROCK = aGenre("Pop Rock"); +export const REGGAE = aGenre("Reggae"); +export const ROCK = aGenre("Rock"); +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 randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)]; diff --git a/tests/in_memory_music_service.test.ts b/tests/in_memory_music_service.test.ts index ad90c08..f56fba5 100644 --- a/tests/in_memory_music_service.test.ts +++ b/tests/in_memory_music_service.test.ts @@ -467,9 +467,9 @@ describe("InMemoryMusicService", () => { it("should provide an array of artists", async () => { expect(await musicLibrary.genres()).toEqual([ HIP_HOP, + SKA, POP, ROCK, - SKA, ]); }); }); diff --git a/tests/in_memory_music_service.ts b/tests/in_memory_music_service.ts index 3bab52d..9a7b044 100644 --- a/tests/in_memory_music_service.ts +++ b/tests/in_memory_music_service.ts @@ -5,6 +5,8 @@ import { pipe } from "fp-ts/lib/function"; import { ordString, fromCompare } from "fp-ts/lib/Ord"; import { shuffle } from "underscore"; +import { b64Encode, b64Decode } from "../src/b64"; + import { MusicService, Credentials, @@ -37,9 +39,7 @@ export class InMemoryMusicService implements MusicService { this.users[username] == password ) { return Promise.resolve({ - authToken: Buffer.from(JSON.stringify({ username, password })).toString( - "base64" - ), + authToken: b64Encode(JSON.stringify({ username, password })), userId: username, nickname: username, }); @@ -49,9 +49,7 @@ export class InMemoryMusicService implements MusicService { } login(token: string): Promise { - const credentials = JSON.parse( - Buffer.from(token, "base64").toString("ascii") - ) as Credentials; + const credentials = JSON.parse(b64Decode(token)) as Credentials; if (this.users[credentials.username] != credentials.password) return Promise.reject("Invalid auth token"); diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index e667b79..c7562d6 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -38,12 +38,14 @@ import { SimilarArtist, } from "../src/music_service"; import { + aGenre, anAlbum, anArtist, aPlaylist, aPlaylistSummary, aTrack, } from "./builders"; +import { b64Encode } from "../src/b64"; describe("t", () => { 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) => it.genres()); - expect(result).toEqual([{ id: "genre1", name: "genre1" }]); + expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]); expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { params: asURLSearchParams(authParams), @@ -579,10 +581,10 @@ describe("Navidrome", () => { .then((it) => it.genres()); expect(result).toEqual([ - { id: "g1", name: "g1" }, - { id: "g2", name: "g2" }, - { id: "g3", name: "g3" }, - { id: "g4", name: "g4" }, + { id: b64Encode("g1"), name: "g1" }, + { id: b64Encode("g2"), name: "g2" }, + { id: b64Encode("g3"), name: "g3" }, + { id: b64Encode("g4"), name: "g4" }, ]); 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 = { _index: 0, _count: 100, - genre: "Pop", + genre: b64Encode("Pop"), type: "byGenre", }; const result = await navidrome @@ -2300,7 +2302,7 @@ describe("Navidrome", () => { describe("streaming a track", () => { const trackId = uuid(); - const genre = { id: "foo", name: "foo" }; + const genre = aGenre("foo"); const album = anAlbum({ genre }); const artist = anArtist({ @@ -3535,7 +3537,7 @@ describe("Navidrome", () => { it("should return true", async () => { const album = anAlbum({ name: "foo woo", - genre: { id: "pop", name: "pop" }, + genre: { id: b64Encode("pop"), name: "pop" }, }); const artist = anArtist({ name: "#1", albums: [album] }); @@ -3570,13 +3572,13 @@ describe("Navidrome", () => { it("should return true", async () => { const album1 = anAlbum({ name: "album1", - genre: { id: "pop", name: "pop" }, + genre: { id: b64Encode("pop"), name: "pop" }, }); const artist1 = anArtist({ name: "artist1", albums: [album1] }); const album2 = anAlbum({ name: "album2", - genre: { id: "pop", name: "pop" }, + genre: { id: b64Encode("pop"), name: "pop" }, }); const artist2 = anArtist({ name: "artist2", albums: [album2] }); @@ -3904,11 +3906,11 @@ describe("Navidrome", () => { const id = uuid(); const name = "Great Playlist"; const track1 = aTrack({ - genre: { id: "pop", name: "pop" }, + genre: { id: b64Encode("pop"), name: "pop" }, number: 66, }); const track2 = aTrack({ - genre: { id: "rock", name: "rock" }, + genre: { id: b64Encode("rock"), name: "rock" }, number: 77, });