diff --git a/src/music_service.ts b/src/music_service.ts index 16cbc72..991468b 100644 --- a/src/music_service.ts +++ b/src/music_service.ts @@ -25,17 +25,17 @@ export type AuthFailure = { export type ArtistSummary = { id: string; name: string; - image: Images -} + image: Images; +}; export type Images = { - small: string | undefined, - medium: string | undefined, - large: string | undefined, -} + small: string | undefined; + medium: string | undefined; + large: string | undefined; +}; export type Artist = ArtistSummary & { - albums: Album[] + albums: Album[]; }; export type AlbumSummary = { @@ -43,11 +43,10 @@ export type AlbumSummary = { name: string; year: string | undefined; genre: string | undefined; -} - -export type Album = AlbumSummary & { }; +export type Album = AlbumSummary & {}; + export type Paging = { _index: number; _count: number; @@ -70,30 +69,33 @@ export const asResult = ([results, total]: [T[], number]) => ({ total, }); -export type ArtistQuery = Paging +export type ArtistQuery = Paging; export type AlbumQuery = Paging & { - artistId?: string - genre?: string -} + artistId?: string; + genre?: string; +}; -export const artistToArtistSummary = ( - it: Artist -): ArtistSummary => ({ +export const artistToArtistSummary = (it: Artist): ArtistSummary => ({ id: it.id, name: it.name, image: it.image, }); -export const albumToAlbumSummary = ( - it: Album -): AlbumSummary => ({ +export const albumToAlbumSummary = (it: Album): AlbumSummary => ({ id: it.id, name: it.name, year: it.year, genre: it.genre, }); +export const range = (size: number) => [...Array(size).keys()]; + +export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] => + artists.flatMap((artist) => + artist.albums.map((album) => [artist, album] as [Artist, Album]) + ); + export interface MusicService { generateToken(credentials: Credentials): Promise; login(authToken: string): Promise; @@ -104,4 +106,5 @@ export interface MusicLibrary { artist(id: string): Promise; albums(q: AlbumQuery): Promise>; album(id: string): Promise; + genres(): Promise; } diff --git a/src/navidrome.ts b/src/navidrome.ts index f3d1e35..d461b82 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -1,4 +1,6 @@ import { option as O } from "fp-ts"; +import * as A from "fp-ts/Array"; +import { ordString } from "fp-ts/lib/Ord"; import { pipe } from "fp-ts/lib/function"; import { Md5 } from "ts-md5/dist/md5"; import { @@ -21,7 +23,6 @@ import axios from "axios"; import { Encryption } from "./encryption"; import randomString from "./random_string"; - export const t = (password: string, s: string) => Md5.hashStr(`${password}${s}`); @@ -75,6 +76,18 @@ export type GetAlbumListResponse = SubsonicResponse & { }; }; +export type genre = { + _songCount: string; + _albumCount: string; + __text: string; +}; + +export type GenGenresResponse = SubsonicResponse & { + genres: { + genre: genre[]; + }; +}; + export type SubsonicError = SubsonicResponse & { error: { _code: string; @@ -115,13 +128,13 @@ export type IdName = { }; export type getAlbumListParams = { - type: string, + type: string; size?: number; offet?: number; - fromYear?: string, - toYear?: string, - genre?: string -} + fromYear?: string; + toYear?: string; + genre?: string; +}; const MAX_ALBUM_LIST = 500; @@ -258,10 +271,15 @@ export class Navidrome implements MusicService { albums: (q: AlbumQuery): Promise> => { const p = pipe( O.fromNullable(q.genre), - O.map(genre => ({ type: "byGenre", genre })), - O.getOrElse(() => ({ type: "alphabeticalByArtist" })), - ) - + O.map((genre) => ({ + type: "byGenre", + genre, + })), + O.getOrElse(() => ({ + type: "alphabeticalByArtist", + })) + ); + return navidrome .get(credentials, "/rest/getAlbumList", { ...p, @@ -286,6 +304,14 @@ export class Navidrome implements MusicService { album: (_: string): Promise => { return Promise.reject("not implemented"); }, + genres: () => + navidrome + .get(credentials, "/rest/getGenres") + .then((it) => pipe( + it.genres.genre, + A.map(it => it.__text), + A.sort(ordString) + )), }; return Promise.resolve(musicLibrary); diff --git a/tests/in_memory_music_service.test.ts b/tests/in_memory_music_service.test.ts index cd3eb20..ac8d7f0 100644 --- a/tests/in_memory_music_service.test.ts +++ b/tests/in_memory_music_service.test.ts @@ -346,7 +346,11 @@ describe("InMemoryMusicService", () => { describe("fetching all on one page", () => { it.only("should return all the albums of that genre for all the artists", async () => { expect( - await musicLibrary.albums({ _index: 0, _count: 100, genre: "Pop" }) + await musicLibrary.albums({ + _index: 0, + _count: 100, + genre: "Pop", + }) ).toEqual({ results: [ albumToAlbumSummary(artist1_album1), @@ -379,7 +383,7 @@ describe("InMemoryMusicService", () => { }); }); }); - + describe("can fetch the last page", () => { it("should return only the albums for the last page", async () => { expect( @@ -394,7 +398,6 @@ describe("InMemoryMusicService", () => { }); }); }); - }); it("should return empty list if there are no albums for the genre", async () => { @@ -411,5 +414,27 @@ describe("InMemoryMusicService", () => { }); }); }); + + describe("genres", () => { + const artist1 = anArtist({ albums: [anAlbum({ genre: "Pop" }), anAlbum({ genre: "Rock" }), anAlbum({ genre: "Pop" })] }); + const artist2 = anArtist({ albums: [anAlbum({ genre: "Hip-Hop" }), anAlbum({ genre: "Rap" }), anAlbum({ genre: "Pop" })] }); + + beforeEach(() => { + service.hasArtists(artist1, artist2); + }); + + describe("fetching all in one page", () => { + it("should provide an array of artists", async () => { + expect( + await musicLibrary.genres() + ).toEqual([ + "Hip-Hop", + "Pop", + "Rap", + "Rock" + ]); + }); + }); + }); }); }); diff --git a/tests/in_memory_music_service.ts b/tests/in_memory_music_service.ts index 6caa641..29ab35f 100644 --- a/tests/in_memory_music_service.ts +++ b/tests/in_memory_music_service.ts @@ -1,5 +1,8 @@ import { option as O } from "fp-ts"; +import * as A from "fp-ts/Array"; +import { eqString } from "fp-ts/lib/Eq"; import { pipe } from "fp-ts/lib/function"; +import { ordString } from "fp-ts/lib/Ord"; import { MusicService, @@ -53,6 +56,7 @@ export class InMemoryMusicService implements MusicService { const credentials = JSON.parse(token) as Credentials; if (this.users[credentials.username] != credentials.password) return Promise.reject("Invalid auth token"); + return Promise.resolve({ artists: (q: ArtistQuery) => Promise.resolve(this.artists.map(artistToArtistSummary)) @@ -71,15 +75,9 @@ export class InMemoryMusicService implements MusicService { .flatMap((artist) => artist.albums.map((album) => [artist, album])) .filter( pipe( - pipe( - O.fromNullable(q.artistId), - O.map(albumByArtist) - ), + pipe(O.fromNullable(q.artistId), O.map(albumByArtist)), O.alt(() => - pipe( - O.fromNullable(q.genre), - O.map(albumWithGenre) - ) + pipe(O.fromNullable(q.genre), O.map(albumWithGenre)) ), O.getOrElse(() => all) ) @@ -96,6 +94,18 @@ export class InMemoryMusicService implements MusicService { O.map((it) => Promise.resolve(it)), O.getOrElse(() => Promise.reject(`No album with id '${id}'`)) ), + genres: () => + Promise.resolve( + pipe( + this.artists, + A.map((it) => it.albums), + A.flatten, + A.map((it) => O.fromNullable(it.genre)), + A.compact, + A.uniq(eqString), + A.sort(ordString) + ) + ), }); } diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index a017c02..996c31e 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -13,6 +13,8 @@ import { AuthSuccess, Images, albumToAlbumSummary, + range, + asArtistAlbumPairs, } from "../src/music_service"; import { anAlbum, anArtist } from "./builders"; @@ -99,6 +101,17 @@ const artistXml = ( `; +const genresXml = ( + genres: string[] +) => ` + + ${genres.map( + (it) => + `${it}` + )} + + `; + const PING_OK = ``; describe("Navidrome", () => { @@ -163,6 +176,31 @@ describe("Navidrome", () => { }); }); + describe("getting genres", () => { + const genres = ["HipHop", "Rap", "TripHop", "Pop", "Rock"]; + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => Promise.resolve(ok(genresXml(genres)))); + }); + + it.only("should return them alphabetically sorted", async () => { + const result = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.genres()); + + expect(result).toEqual(genres.sort()); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getGenres`, { + params: { + ...authParams, + }, + }); + }); + }); + describe("getting an artist", () => { const album1: Album = anAlbum(); @@ -386,30 +424,30 @@ describe("Navidrome", () => { }); }); - const range = (size: number) => [...Array(size).keys()]; - - const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] => - artists.flatMap((artist) => - artist.albums.map((album) => [artist, album] as [Artist, Album]) - ); - describe("getting albums", () => { describe("filtering", () => { const album1 = anAlbum({ genre: "Pop" }); const album2 = anAlbum({ genre: "Rock" }); const album3 = anAlbum({ genre: "Pop" }); - - const artist = anArtist({ albums: [album1, album2, album3]}); + + const artist = anArtist({ albums: [album1, album2, album3] }); describe("by genre", () => { beforeEach(() => { mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => - Promise.resolve(ok(albumListXml([[artist, album1], [artist, album3]]))) + Promise.resolve( + ok( + albumListXml([ + [artist, album1], + [artist, album3], + ]) + ) + ) ); }); - + it("should pass the filter to navidrome", async () => { const q = { _index: 0, _count: 500, genre: "Pop" }; const result = await navidrome @@ -417,12 +455,12 @@ describe("Navidrome", () => { .then((it) => it as AuthSuccess) .then((it) => navidrome.login(it.authToken)) .then((it) => it.albums(q)); - + expect(result).toEqual({ results: [album1, album3].map(albumToAlbumSummary), total: 2, }); - + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, { params: { type: "byGenre", @@ -434,7 +472,6 @@ describe("Navidrome", () => { }); }); }); - }); describe("when there are less than 500 albums", () => { @@ -505,7 +542,7 @@ describe("Navidrome", () => { }, }); }); - }); + }); }); describe("when there are more than 500 albums", () => {