mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to list tracks on an album
This commit is contained in:
@@ -34,6 +34,12 @@ export type Images = {
|
|||||||
large: string | undefined;
|
large: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NO_IMAGES: Images = {
|
||||||
|
small: undefined,
|
||||||
|
medium: undefined,
|
||||||
|
large: undefined
|
||||||
|
}
|
||||||
|
|
||||||
export type Artist = ArtistSummary & {
|
export type Artist = ArtistSummary & {
|
||||||
albums: AlbumSummary[];
|
albums: AlbumSummary[];
|
||||||
};
|
};
|
||||||
@@ -46,7 +52,6 @@ export type AlbumSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Album = AlbumSummary & {
|
export type Album = AlbumSummary & {
|
||||||
tracks: Track[]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Track = {
|
export type Track = {
|
||||||
@@ -54,6 +59,10 @@ export type Track = {
|
|||||||
name: string;
|
name: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
duration: string;
|
duration: string;
|
||||||
|
number: string | undefined;
|
||||||
|
genre: string | undefined;
|
||||||
|
album: AlbumSummary;
|
||||||
|
artist: ArtistSummary
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Paging = {
|
export type Paging = {
|
||||||
@@ -114,5 +123,6 @@ export interface MusicLibrary {
|
|||||||
artist(id: string): Promise<Artist>;
|
artist(id: string): Promise<Artist>;
|
||||||
albums(q: AlbumQuery): Promise<Result<AlbumSummary>>;
|
albums(q: AlbumQuery): Promise<Result<AlbumSummary>>;
|
||||||
album(id: string): Promise<Album>;
|
album(id: string): Promise<Album>;
|
||||||
|
tracks(albumId: string): Promise<Track[]>;
|
||||||
genres(): Promise<string[]>;
|
genres(): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/navidrome.ts
101
src/navidrome.ts
@@ -16,6 +16,7 @@ import {
|
|||||||
MusicLibrary,
|
MusicLibrary,
|
||||||
Images,
|
Images,
|
||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
|
NO_IMAGES,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
import X2JS from "x2js";
|
import X2JS from "x2js";
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ export type artistSummary = {
|
|||||||
_name: string;
|
_name: string;
|
||||||
_albumCount: string;
|
_albumCount: string;
|
||||||
_artistImageUrl: string | undefined;
|
_artistImageUrl: string | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GetArtistsResponse = SubsonicResponse & {
|
export type GetArtistsResponse = SubsonicResponse & {
|
||||||
artists: {
|
artists: {
|
||||||
@@ -118,31 +119,33 @@ export type GetArtistResponse = SubsonicResponse & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type song = {
|
export type song = {
|
||||||
"_id": string,
|
_id: string;
|
||||||
"_parent": string,
|
_parent: string;
|
||||||
"_title": string,
|
_title: string;
|
||||||
"_album": string,
|
_album: string;
|
||||||
"_artist": string,
|
_artist: string;
|
||||||
"_coverArt": string,
|
_track: string;
|
||||||
"_created": "2004-11-08T23:36:11",
|
_genre: string;
|
||||||
"_duration": string,
|
_coverArt: string;
|
||||||
"_bitRate": "128",
|
_created: "2004-11-08T23:36:11";
|
||||||
"_suffix": "mp3",
|
_duration: string;
|
||||||
"_contentType": string,
|
_bitRate: "128";
|
||||||
"_albumId": string,
|
_suffix: "mp3";
|
||||||
"_artistId": string,
|
_contentType: string;
|
||||||
"_type": "music"
|
_albumId: string;
|
||||||
}
|
_artistId: string;
|
||||||
|
_type: "music";
|
||||||
|
};
|
||||||
|
|
||||||
export type GetAlbumResponse = {
|
export type GetAlbumResponse = {
|
||||||
album: {
|
album: {
|
||||||
_id: string,
|
_id: string;
|
||||||
_name: string,
|
_name: string;
|
||||||
_genre: string,
|
_genre: string;
|
||||||
_year: string,
|
_year: string;
|
||||||
song: song[]
|
song: song[];
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export function isError(
|
export function isError(
|
||||||
subsonicResponse: SubsonicResponse
|
subsonicResponse: SubsonicResponse
|
||||||
@@ -329,29 +332,57 @@ export class Navidrome implements MusicService {
|
|||||||
total: Math.min(MAX_ALBUM_LIST, total),
|
total: Math.min(MAX_ALBUM_LIST, total),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
album: (id: string): Promise<Album> => navidrome
|
album: (id: string): Promise<Album> =>
|
||||||
|
navidrome
|
||||||
.get<GetAlbumResponse>(credentials, "/rest/getAlbum", { id })
|
.get<GetAlbumResponse>(credentials, "/rest/getAlbum", { id })
|
||||||
.then(it => it.album)
|
.then((it) => it.album)
|
||||||
.then(album => ({
|
.then((album) => ({
|
||||||
id: album._id,
|
id: album._id,
|
||||||
name: album._name,
|
name: album._name,
|
||||||
year: album._year,
|
year: album._year,
|
||||||
genre: album._genre,
|
genre: album._genre,
|
||||||
tracks: album.song.map(track => ({
|
// tracks: album.song.map(track => ({
|
||||||
id: track._id,
|
// id: track._id,
|
||||||
name: track._title,
|
// name: track._title,
|
||||||
mimeType: track._contentType,
|
// mimeType: track._contentType,
|
||||||
duration: track._duration,
|
// duration: track._duration,
|
||||||
}))
|
// }))
|
||||||
})),
|
})),
|
||||||
genres: () =>
|
genres: () =>
|
||||||
navidrome
|
navidrome
|
||||||
.get<GenGenresResponse>(credentials, "/rest/getGenres")
|
.get<GenGenresResponse>(credentials, "/rest/getGenres")
|
||||||
.then((it) => pipe(
|
.then((it) =>
|
||||||
|
pipe(
|
||||||
it.genres.genre,
|
it.genres.genre,
|
||||||
A.map(it => it.__text),
|
A.map((it) => it.__text),
|
||||||
A.sort(ordString)
|
A.sort(ordString)
|
||||||
)),
|
)
|
||||||
|
),
|
||||||
|
tracks: (albumId: string) =>
|
||||||
|
navidrome
|
||||||
|
.get<GetAlbumResponse>(credentials, "/rest/getAlbum", { id: albumId })
|
||||||
|
.then((it) => it.album)
|
||||||
|
.then((album) =>
|
||||||
|
album.song.map((song) => ({
|
||||||
|
id: song._id,
|
||||||
|
name: song._title,
|
||||||
|
mimeType: song._contentType,
|
||||||
|
duration: song._duration,
|
||||||
|
number: song._track,
|
||||||
|
genre: song._genre,
|
||||||
|
album: {
|
||||||
|
id: album._id,
|
||||||
|
name: album._name,
|
||||||
|
year: album._year,
|
||||||
|
genre: album._genre,
|
||||||
|
},
|
||||||
|
artist: {
|
||||||
|
id: song._artistId,
|
||||||
|
name: song._artist,
|
||||||
|
image: NO_IMAGES,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(musicLibrary);
|
return Promise.resolve(musicLibrary);
|
||||||
|
|||||||
37
src/smapi.ts
37
src/smapi.ts
@@ -11,6 +11,7 @@ import {
|
|||||||
MusicLibrary,
|
MusicLibrary,
|
||||||
MusicService,
|
MusicService,
|
||||||
slice2,
|
slice2,
|
||||||
|
Track,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
|
|
||||||
export const LOGIN_ROUTE = "/login";
|
export const LOGIN_ROUTE = "/login";
|
||||||
@@ -168,7 +169,7 @@ const genre = (genre: string) => ({
|
|||||||
itemType: "container",
|
itemType: "container",
|
||||||
id: `genre:${genre}`,
|
id: `genre:${genre}`,
|
||||||
title: genre,
|
title: genre,
|
||||||
})
|
});
|
||||||
|
|
||||||
const album = (album: AlbumSummary) => ({
|
const album = (album: AlbumSummary) => ({
|
||||||
itemType: "album",
|
itemType: "album",
|
||||||
@@ -182,6 +183,27 @@ const album = (album: AlbumSummary) => ({
|
|||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const track = (track: Track) => ({
|
||||||
|
itemType: "track",
|
||||||
|
id: `track:${track.id}`,
|
||||||
|
mimeType: track.mimeType,
|
||||||
|
title: track.name,
|
||||||
|
|
||||||
|
trackMetadata: {
|
||||||
|
album: track.album.name,
|
||||||
|
albumId: track.album.id,
|
||||||
|
albumArtist: track.artist.name,
|
||||||
|
albumArtistId: track.artist.id,
|
||||||
|
// albumArtURI
|
||||||
|
artist: track.artist.name,
|
||||||
|
artistId: track.artist.id,
|
||||||
|
duration: track.duration,
|
||||||
|
genre: track.album.genre,
|
||||||
|
// genreId
|
||||||
|
trackNumber: track.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
type SoapyHeaders = {
|
type SoapyHeaders = {
|
||||||
credentials?: Credentials;
|
credentials?: Credentials;
|
||||||
};
|
};
|
||||||
@@ -289,7 +311,18 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(([page, total]) =>
|
.then(([page, total]) =>
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaCollection: page.map(album),
|
mediaCollection: page.map(album),
|
||||||
index: 0,
|
index: paging._index,
|
||||||
|
total,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
case "album":
|
||||||
|
return await musicLibrary
|
||||||
|
.tracks(typeId!)
|
||||||
|
.then(slice2(paging))
|
||||||
|
.then(([page, total]) =>
|
||||||
|
getMetadataResult({
|
||||||
|
mediaCollection: page.map(track),
|
||||||
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ export function anArtist(fields: Partial<Artist> = {}): Artist {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SAMPLE_GENRES = ["Metal", "Pop", "Rock", "Hip-Hop"]
|
||||||
|
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)]
|
||||||
|
|
||||||
export function aTrack(fields: Partial<Track> = {}): Track {
|
export function aTrack(fields: Partial<Track> = {}): Track {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
return {
|
return {
|
||||||
@@ -90,19 +93,21 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
|||||||
name: `Track ${id}`,
|
name: `Track ${id}`,
|
||||||
mimeType: `audio/mp3-${id}`,
|
mimeType: `audio/mp3-${id}`,
|
||||||
duration: `${randomInt(500)}`,
|
duration: `${randomInt(500)}`,
|
||||||
|
number: `${randomInt(100)}`,
|
||||||
|
genre: randomGenre(),
|
||||||
|
artist: anArtist(),
|
||||||
|
album: anAlbum(),
|
||||||
...fields
|
...fields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anAlbum(fields: Partial<Album> = {}): Album {
|
export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||||
const genres = ["Metal", "Pop", "Rock", "Hip-Hop"];
|
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `Album ${id}`,
|
name: `Album ${id}`,
|
||||||
genre: genres[randomInt(genres.length)],
|
genre: randomGenre(),
|
||||||
year: `19${randomInt(99)}`,
|
year: `19${randomInt(99)}`,
|
||||||
tracks: [aTrack(), aTrack(), aTrack()],
|
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
albumToAlbumSummary,
|
albumToAlbumSummary,
|
||||||
} from "../src/music_service";
|
} from "../src/music_service";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { anArtist, anAlbum } from "./builders";
|
import { anArtist, anAlbum, aTrack } from "./builders";
|
||||||
|
|
||||||
describe("InMemoryMusicService", () => {
|
describe("InMemoryMusicService", () => {
|
||||||
const service = new InMemoryMusicService();
|
const service = new InMemoryMusicService();
|
||||||
@@ -176,6 +176,34 @@ describe("InMemoryMusicService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("tracks", () => {
|
||||||
|
const artist1Album1 = anAlbum();
|
||||||
|
const artist1Album2 = anAlbum();
|
||||||
|
const artist1 = anArtist({ albums: [artist1Album1, artist1Album2] });
|
||||||
|
|
||||||
|
const track1 = aTrack({ album: artist1Album1, artist: artist1 });
|
||||||
|
const track2 = aTrack({ album: artist1Album1, artist: artist1 });
|
||||||
|
const track3 = aTrack({ album: artist1Album2, artist: artist1 });
|
||||||
|
const track4 = aTrack({ album: artist1Album2, artist: artist1 });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.hasArtists(artist1);
|
||||||
|
service.hasTracks(track1, track2, track3, track4);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching tracks for an album", () => {
|
||||||
|
it("should return only tracks on that album", async () => {
|
||||||
|
expect(await musicLibrary.tracks(artist1Album1.id)).toEqual([track1, track2])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching tracks for an album that doesnt exist", () => {
|
||||||
|
it("should return empty array", async () => {
|
||||||
|
expect(await musicLibrary.tracks("non existant album id")).toEqual([])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("albums", () => {
|
describe("albums", () => {
|
||||||
const artist1_album1 = anAlbum({ genre: "Pop" });
|
const artist1_album1 = anAlbum({ genre: "Pop" });
|
||||||
const artist1_album2 = anAlbum({ genre: "Rock" });
|
const artist1_album2 = anAlbum({ genre: "Rock" });
|
||||||
@@ -332,8 +360,20 @@ describe("InMemoryMusicService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("genres", () => {
|
describe("genres", () => {
|
||||||
const artist1 = anArtist({ albums: [anAlbum({ genre: "Pop" }), anAlbum({ genre: "Rock" }), anAlbum({ genre: "Pop" })] });
|
const artist1 = anArtist({
|
||||||
const artist2 = anArtist({ albums: [anAlbum({ genre: "Hip-Hop" }), anAlbum({ genre: "Rap" }), anAlbum({ genre: "Pop" })] });
|
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(() => {
|
beforeEach(() => {
|
||||||
service.hasArtists(artist1, artist2);
|
service.hasArtists(artist1, artist2);
|
||||||
@@ -341,13 +381,11 @@ describe("InMemoryMusicService", () => {
|
|||||||
|
|
||||||
describe("fetching all in one page", () => {
|
describe("fetching all in one page", () => {
|
||||||
it("should provide an array of artists", async () => {
|
it("should provide an array of artists", async () => {
|
||||||
expect(
|
expect(await musicLibrary.genres()).toEqual([
|
||||||
await musicLibrary.genres()
|
|
||||||
).toEqual([
|
|
||||||
"Hip-Hop",
|
"Hip-Hop",
|
||||||
"Pop",
|
"Pop",
|
||||||
"Rap",
|
"Rap",
|
||||||
"Rock"
|
"Rock",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
artistToArtistSummary,
|
artistToArtistSummary,
|
||||||
albumToAlbumSummary,
|
albumToAlbumSummary,
|
||||||
Album,
|
Album,
|
||||||
|
Track,
|
||||||
} from "../src/music_service";
|
} from "../src/music_service";
|
||||||
|
|
||||||
type P<T> = (t: T) => boolean;
|
type P<T> = (t: T) => boolean;
|
||||||
@@ -29,6 +30,7 @@ const albumWithGenre = (genre: string): P<[Artist, Album]> => ([_, album]) =>
|
|||||||
export class InMemoryMusicService implements MusicService {
|
export class InMemoryMusicService implements MusicService {
|
||||||
users: Record<string, string> = {};
|
users: Record<string, string> = {};
|
||||||
artists: Artist[] = [];
|
artists: Artist[] = [];
|
||||||
|
tracks: Track[] = [];
|
||||||
|
|
||||||
generateToken({
|
generateToken({
|
||||||
username,
|
username,
|
||||||
@@ -101,6 +103,7 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
A.sort(ordString)
|
A.sort(ordString)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
tracks: (albumId: string) => Promise.resolve(this.tracks.filter(it => it.album.id === albumId))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +122,15 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasTracks(...newTracks: Track[]) {
|
||||||
|
this.tracks = [...this.tracks, ...newTracks];
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.artists = [];
|
this.artists = [];
|
||||||
|
this.tracks = [];
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import {
|
|||||||
range,
|
range,
|
||||||
asArtistAlbumPairs,
|
asArtistAlbumPairs,
|
||||||
Track,
|
Track,
|
||||||
AlbumSummary
|
AlbumSummary,
|
||||||
|
artistToArtistSummary,
|
||||||
|
NO_IMAGES
|
||||||
} from "../src/music_service";
|
} from "../src/music_service";
|
||||||
import { anAlbum, anArtist, aTrack } from "./builders";
|
import { anAlbum, anArtist, aTrack } from "./builders";
|
||||||
|
|
||||||
@@ -81,14 +83,16 @@ const albumXml = (artist: Artist, album: AlbumSummary, tracks: Track[] = []) =>
|
|||||||
created="2021-01-07T08:19:55.834207205Z"
|
created="2021-01-07T08:19:55.834207205Z"
|
||||||
artistId="${artist.id}"
|
artistId="${artist.id}"
|
||||||
songCount="19"
|
songCount="19"
|
||||||
isVideo="false">${tracks.map(track => songXml(artist, album, track))}</album>`;
|
isVideo="false">${tracks.map(track => songXml(track))}</album>`;
|
||||||
|
|
||||||
const songXml = (artist: Artist, album: AlbumSummary, track: Track) => `<song
|
const songXml = (track: Track) => `<song
|
||||||
id="${track.id}"
|
id="${track.id}"
|
||||||
parent="${album.id}"
|
parent="${track.album.id}"
|
||||||
title="${track.name}"
|
title="${track.name}"
|
||||||
album="${album.name}"
|
album="${track.album.name}"
|
||||||
artist="${artist.name}"
|
artist="${track.artist.name}"
|
||||||
|
track="${track.number}"
|
||||||
|
genre="${track.genre}"
|
||||||
isDir="false"
|
isDir="false"
|
||||||
coverArt="71381"
|
coverArt="71381"
|
||||||
created="2004-11-08T23:36:11"
|
created="2004-11-08T23:36:11"
|
||||||
@@ -99,8 +103,8 @@ const songXml = (artist: Artist, album: AlbumSummary, track: Track) => `<song
|
|||||||
contentType="${track.mimeType}"
|
contentType="${track.mimeType}"
|
||||||
isVideo="false"
|
isVideo="false"
|
||||||
path="ACDC/High voltage/ACDC - The Jack.mp3"
|
path="ACDC/High voltage/ACDC - The Jack.mp3"
|
||||||
albumId="${album.id}"
|
albumId="${track.album.id}"
|
||||||
artistId="${artist.name}"
|
artistId="${track.artist.id}"
|
||||||
type="music"/>`;
|
type="music"/>`;
|
||||||
|
|
||||||
const albumListXml = (
|
const albumListXml = (
|
||||||
@@ -133,8 +137,8 @@ const genresXml = (
|
|||||||
</genres>
|
</genres>
|
||||||
</subsonic-response>`;
|
</subsonic-response>`;
|
||||||
|
|
||||||
const getAlbumXml = (artist: Artist, album: Album) => `<subsonic-response status="ok" version="1.8.0">
|
const getAlbumXml = (artist: Artist, album: Album, tracks: Track[]) => `<subsonic-response status="ok" version="1.8.0">
|
||||||
${albumXml(artist, album, album.tracks)}
|
${albumXml(artist, album, tracks)}
|
||||||
</subsonic-response>`
|
</subsonic-response>`
|
||||||
|
|
||||||
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
||||||
@@ -624,22 +628,24 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
describe("getting an album", () => {
|
describe("getting an album", () => {
|
||||||
describe("when it exists", () => {
|
describe("when it exists", () => {
|
||||||
const album = anAlbum({ tracks: [
|
const album = anAlbum();
|
||||||
aTrack(),
|
|
||||||
aTrack(),
|
|
||||||
aTrack(),
|
|
||||||
aTrack(),
|
|
||||||
] });
|
|
||||||
|
|
||||||
const artist = anArtist({ albums: [album] })
|
const artist = anArtist({ albums: [album] })
|
||||||
|
|
||||||
|
const tracks = [
|
||||||
|
aTrack({ artist, album }),
|
||||||
|
aTrack({ artist, album }),
|
||||||
|
aTrack({ artist, album }),
|
||||||
|
aTrack({ artist, album }),
|
||||||
|
]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
ok(
|
ok(
|
||||||
getAlbumXml(artist, album)
|
getAlbumXml(artist, album, tracks)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -663,4 +669,55 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getting tracks", () => {
|
||||||
|
describe("for an album", () => {
|
||||||
|
describe("when it exists", () => {
|
||||||
|
const album = anAlbum({ id: "album1", name: "Burnin" });
|
||||||
|
const albumSummary = albumToAlbumSummary(album);
|
||||||
|
|
||||||
|
const artist = anArtist({ id: "artist1", name: "Bob Marley", albums: [album] })
|
||||||
|
const artistSummary = {
|
||||||
|
...artistToArtistSummary(artist),
|
||||||
|
image: NO_IMAGES
|
||||||
|
};
|
||||||
|
|
||||||
|
const tracks = [
|
||||||
|
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||||
|
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||||
|
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||||
|
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
ok(
|
||||||
|
getAlbumXml(artist, album, tracks)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the album", async () => {
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.tracks(album.id));
|
||||||
|
|
||||||
|
expect(result).toEqual(tracks);
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||||
|
params: {
|
||||||
|
id: album.id,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
someCredentials,
|
someCredentials,
|
||||||
anArtist,
|
anArtist,
|
||||||
anAlbum,
|
anAlbum,
|
||||||
|
aTrack,
|
||||||
} from "./builders";
|
} from "./builders";
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import supersoap from "./supersoap";
|
import supersoap from "./supersoap";
|
||||||
@@ -470,7 +471,7 @@ describe("api", () => {
|
|||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
})),
|
})),
|
||||||
index: 0,
|
index: 2,
|
||||||
total: artistWithManyAlbums.albums.length,
|
total: artistWithManyAlbums.albums.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -631,6 +632,107 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("asking for tracks", () => {
|
||||||
|
describe("for an album", () => {
|
||||||
|
const album = anAlbum();
|
||||||
|
const artist = anArtist({
|
||||||
|
albums: [album],
|
||||||
|
});
|
||||||
|
|
||||||
|
const track1 = aTrack({ artist, album, number: "1" });
|
||||||
|
const track2 = aTrack({ artist, album, number: "2" });
|
||||||
|
const track3 = aTrack({ artist, album, number: "3" });
|
||||||
|
const track4 = aTrack({ artist, album, number: "4" });
|
||||||
|
const track5 = aTrack({ artist, album, number: "5" });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasArtists(artist);
|
||||||
|
musicService.hasTracks(track1, track2, track3, track4, track5);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for all albums", () => {
|
||||||
|
it("should return them all", async () => {
|
||||||
|
const result = await ws.getMetadataAsync({
|
||||||
|
id: `album:${album.id}`,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
track1,
|
||||||
|
track2,
|
||||||
|
track3,
|
||||||
|
track4,
|
||||||
|
track5,
|
||||||
|
].map((track) => ({
|
||||||
|
itemType: "track",
|
||||||
|
id: `track:${track.id}`,
|
||||||
|
mimeType: track.mimeType,
|
||||||
|
title: track.name,
|
||||||
|
|
||||||
|
trackMetadata: {
|
||||||
|
album: track.album.name,
|
||||||
|
albumId: track.album.id,
|
||||||
|
albumArtist: track.artist.name,
|
||||||
|
albumArtistId: track.artist.id,
|
||||||
|
// albumArtURI
|
||||||
|
artist: track.artist.name,
|
||||||
|
artistId: track.artist.id,
|
||||||
|
duration: track.duration,
|
||||||
|
genre: track.album.genre,
|
||||||
|
// genreId
|
||||||
|
trackNumber: track.number,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
index: 0,
|
||||||
|
total: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for a single page of tracks", () => {
|
||||||
|
it("should return only that page", async () => {
|
||||||
|
const result = await ws.getMetadataAsync({
|
||||||
|
id: `album:${album.id}`,
|
||||||
|
index: 2,
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
track3,
|
||||||
|
track4,
|
||||||
|
].map((track) => ({
|
||||||
|
itemType: "track",
|
||||||
|
id: `track:${track.id}`,
|
||||||
|
mimeType: track.mimeType,
|
||||||
|
title: track.name,
|
||||||
|
|
||||||
|
trackMetadata: {
|
||||||
|
album: track.album.name,
|
||||||
|
albumId: track.album.id,
|
||||||
|
albumArtist: track.artist.name,
|
||||||
|
albumArtistId: track.artist.id,
|
||||||
|
// albumArtURI
|
||||||
|
artist: track.artist.name,
|
||||||
|
artistId: track.artist.id,
|
||||||
|
duration: track.duration,
|
||||||
|
genre: track.album.genre,
|
||||||
|
// genreId
|
||||||
|
trackNumber: track.number,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
index: 2,
|
||||||
|
total: 5,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user