mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Compare commits
14 Commits
c122b9ac90
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6897397c28 | ||
|
|
3a14b62de4 | ||
|
|
9e5df22701 | ||
|
|
e29d5c5d24 | ||
|
|
b97590dd36 | ||
|
|
b0dc11abcb | ||
|
|
5009732da2 | ||
|
|
ddde55d02b | ||
|
|
0602e1f077 | ||
|
|
7eeedff040 | ||
|
|
0451c3a931 | ||
|
|
cc0dc3704d | ||
|
|
dabb7d0f12 | ||
|
|
a38ca831df |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-bullseye
|
FROM node:23-bullseye
|
||||||
|
|
||||||
LABEL maintainer=simojenki
|
LABEL maintainer=simojenki
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-bullseye-slim AS build
|
FROM node:23-bullseye-slim AS build
|
||||||
|
|
||||||
WORKDIR /bonob
|
WORKDIR /bonob
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ RUN apt-get update && \
|
|||||||
NODE_ENV=production npm install --omit=dev
|
NODE_ENV=production npm install --omit=dev
|
||||||
|
|
||||||
|
|
||||||
FROM node:22-bullseye-slim
|
FROM node:23-bullseye-slim
|
||||||
|
|
||||||
LABEL maintainer="simojenki" \
|
LABEL maintainer="simojenki" \
|
||||||
org.opencontainers.image.source="https://github.com/simojenki/bonob" \
|
org.opencontainers.image.source="https://github.com/simojenki/bonob" \
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import logger from "./logger";
|
|||||||
import {
|
import {
|
||||||
axiosImageFetcher,
|
axiosImageFetcher,
|
||||||
cachingImageFetcher,
|
cachingImageFetcher,
|
||||||
SubsonicMusicService,
|
|
||||||
TranscodingCustomPlayers,
|
TranscodingCustomPlayers,
|
||||||
NO_CUSTOM_PLAYERS,
|
NO_CUSTOM_PLAYERS,
|
||||||
Subsonic
|
Subsonic
|
||||||
} from "./subsonic";
|
} from "./subsonic";
|
||||||
|
import { SubsonicMusicService} from "./subsonic_music_library";
|
||||||
import { InMemoryAPITokens, sha256 } from "./api_tokens";
|
import { InMemoryAPITokens, sha256 } from "./api_tokens";
|
||||||
import { InMemoryLinkCodes } from "./link_codes";
|
import { InMemoryLinkCodes } from "./link_codes";
|
||||||
import readConfig from "./config";
|
import readConfig from "./config";
|
||||||
import sonos, { bonobService } from "./sonos";
|
import sonos, { bonobService } from "./sonos";
|
||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_library";
|
||||||
import { SystemClock } from "./clock";
|
import { SystemClock } from "./clock";
|
||||||
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export type ArtistSummary = {
|
|||||||
|
|
||||||
export type SimilarArtist = ArtistSummary & { inLibrary: boolean };
|
export type SimilarArtist = ArtistSummary & { inLibrary: boolean };
|
||||||
|
|
||||||
export type Artist = ArtistSummary & {
|
// todo: maybe is should be artist.summary rather than an artist also being a summary?
|
||||||
|
export type Artist = Pick<ArtistSummary, "id" | "name" | "image"> & {
|
||||||
albums: AlbumSummary[];
|
albums: AlbumSummary[];
|
||||||
similarArtists: SimilarArtist[]
|
similarArtists: SimilarArtist[]
|
||||||
};
|
};
|
||||||
@@ -34,12 +35,11 @@ export type AlbumSummary = {
|
|||||||
year: string | undefined;
|
year: string | undefined;
|
||||||
genre: Genre | undefined;
|
genre: Genre | undefined;
|
||||||
coverArt: BUrn | undefined;
|
coverArt: BUrn | undefined;
|
||||||
|
|
||||||
artistName: string | undefined;
|
artistName: string | undefined;
|
||||||
artistId: string | undefined;
|
artistId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Album = AlbumSummary & {};
|
export type Album = Pick<AlbumSummary, "id" | "name" | "year" | "genre" | "coverArt" | "artistName" | "artistId"> & { tracks: Track[] };
|
||||||
|
|
||||||
export type Genre = {
|
export type Genre = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -60,7 +60,7 @@ export type Encoding = {
|
|||||||
mimeType: string
|
mimeType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Track = {
|
export type TrackSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
encoding: Encoding,
|
encoding: Encoding,
|
||||||
@@ -68,9 +68,12 @@ export type Track = {
|
|||||||
number: number | undefined;
|
number: number | undefined;
|
||||||
genre: Genre | undefined;
|
genre: Genre | undefined;
|
||||||
coverArt: BUrn | undefined;
|
coverArt: BUrn | undefined;
|
||||||
album: AlbumSummary;
|
|
||||||
artist: ArtistSummary;
|
artist: ArtistSummary;
|
||||||
rating: Rating;
|
rating: Rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Track = TrackSummary & {
|
||||||
|
album: AlbumSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RadioStation = {
|
export type RadioStation = {
|
||||||
@@ -129,6 +132,18 @@ export const albumToAlbumSummary = (it: Album): AlbumSummary => ({
|
|||||||
coverArt: it.coverArt
|
coverArt: it.coverArt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const trackToTrackSummary = (it: Track): TrackSummary => ({
|
||||||
|
id: it.id,
|
||||||
|
name: it.name,
|
||||||
|
encoding: it.encoding,
|
||||||
|
duration: it.duration,
|
||||||
|
number: it.number,
|
||||||
|
genre: it.genre,
|
||||||
|
coverArt: it.coverArt,
|
||||||
|
artist: it.artist,
|
||||||
|
rating: it.rating
|
||||||
|
});
|
||||||
|
|
||||||
export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({
|
export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({
|
||||||
id: it.id,
|
id: it.id,
|
||||||
name: it.name,
|
name: it.name,
|
||||||
@@ -176,7 +191,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[]>;
|
|
||||||
track(trackId: string): Promise<Track>;
|
track(trackId: string): Promise<Track>;
|
||||||
genres(): Promise<Genre[]>;
|
genres(): Promise<Genre[]>;
|
||||||
years(): Promise<Year[]>;
|
years(): Promise<Year[]>;
|
||||||
@@ -200,8 +214,8 @@ export interface MusicLibrary {
|
|||||||
deletePlaylist(id: string): Promise<boolean>
|
deletePlaylist(id: string): Promise<boolean>
|
||||||
addToPlaylist(playlistId: string, trackId: string): Promise<boolean>
|
addToPlaylist(playlistId: string, trackId: string): Promise<boolean>
|
||||||
removeFromPlaylist(playlistId: string, indicies: number[]): Promise<boolean>
|
removeFromPlaylist(playlistId: string, indicies: number[]): Promise<boolean>
|
||||||
similarSongs(id: string): Promise<Track[]>;
|
similarSongs(id: string): Promise<TrackSummary[]>;
|
||||||
topSongs(artistId: string): Promise<Track[]>;
|
topSongs(artistId: string): Promise<TrackSummary[]>;
|
||||||
radioStation(id: string): Promise<RadioStation>
|
radioStation(id: string): Promise<RadioStation>
|
||||||
radioStations(): Promise<RadioStation[]>
|
radioStations(): Promise<RadioStation[]>
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
ratingAsInt,
|
ratingAsInt,
|
||||||
} from "./smapi";
|
} from "./smapi";
|
||||||
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||||
import { MusicService, AuthFailure, AuthSuccess } from "./music_service";
|
import { MusicService, AuthFailure, AuthSuccess } from "./music_library";
|
||||||
import bindSmapiSoapServiceToExpress from "./smapi";
|
import bindSmapiSoapServiceToExpress from "./smapi";
|
||||||
import { APITokens, InMemoryAPITokens } from "./api_tokens";
|
import { APITokens, InMemoryAPITokens } from "./api_tokens";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import logger from "./logger";
|
|||||||
|
|
||||||
import { LinkCodes } from "./link_codes";
|
import { LinkCodes } from "./link_codes";
|
||||||
import {
|
import {
|
||||||
Album,
|
|
||||||
AlbumQuery,
|
AlbumQuery,
|
||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
ArtistSummary,
|
ArtistSummary,
|
||||||
@@ -22,7 +21,7 @@ import {
|
|||||||
Rating,
|
Rating,
|
||||||
slice2,
|
slice2,
|
||||||
Track,
|
Track,
|
||||||
} from "./music_service";
|
} from "./music_library";
|
||||||
import { APITokens } from "./api_tokens";
|
import { APITokens } from "./api_tokens";
|
||||||
import { Clock } from "./clock";
|
import { Clock } from "./clock";
|
||||||
import { URLBuilder } from "./url_builder";
|
import { URLBuilder } from "./url_builder";
|
||||||
@@ -612,7 +611,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "artist":
|
case "artist":
|
||||||
return musicLibrary.artist(typeId).then((artist) => {
|
return musicLibrary.artist(typeId).then((artist) => {
|
||||||
const [page, total] = slice2<Album>(paging)(
|
const [page, total] = slice2<AlbumSummary>(paging)(
|
||||||
artist.albums
|
artist.albums
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -982,7 +981,8 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
});
|
});
|
||||||
case "album":
|
case "album":
|
||||||
return musicLibrary
|
return musicLibrary
|
||||||
.tracks(typeId!)
|
.album(typeId!)
|
||||||
|
.then(it => it.tracks)
|
||||||
.then(slice2(paging))
|
.then(slice2(paging))
|
||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
|
|||||||
838
src/subsonic.ts
838
src/subsonic.ts
File diff suppressed because it is too large
Load Diff
320
src/subsonic_music_library.ts
Normal file
320
src/subsonic_music_library.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { taskEither as TE } from "fp-ts";
|
||||||
|
import { pipe } from "fp-ts/lib/function";
|
||||||
|
import {
|
||||||
|
Credentials,
|
||||||
|
MusicService,
|
||||||
|
ArtistSummary,
|
||||||
|
Result,
|
||||||
|
slice2,
|
||||||
|
AlbumQuery,
|
||||||
|
ArtistQuery,
|
||||||
|
MusicLibrary,
|
||||||
|
Album,
|
||||||
|
AlbumSummary,
|
||||||
|
Rating,
|
||||||
|
Artist,
|
||||||
|
AuthFailure,
|
||||||
|
AuthSuccess,
|
||||||
|
} from "./music_library";
|
||||||
|
import {
|
||||||
|
Subsonic,
|
||||||
|
CustomPlayers,
|
||||||
|
NO_CUSTOM_PLAYERS,
|
||||||
|
asToken,
|
||||||
|
parseToken,
|
||||||
|
artistImageURN,
|
||||||
|
asYear,
|
||||||
|
isValidImage
|
||||||
|
} from "./subsonic";
|
||||||
|
import _ from "underscore";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import logger from "./logger";
|
||||||
|
import { assertSystem, BUrn } from "./burn";
|
||||||
|
|
||||||
|
export class SubsonicMusicService implements MusicService {
|
||||||
|
subsonic: Subsonic;
|
||||||
|
customPlayers: CustomPlayers;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
subsonic: Subsonic,
|
||||||
|
customPlayers: CustomPlayers = NO_CUSTOM_PLAYERS
|
||||||
|
) {
|
||||||
|
this.subsonic = subsonic;
|
||||||
|
this.customPlayers = customPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken = (
|
||||||
|
credentials: Credentials
|
||||||
|
): TE.TaskEither<AuthFailure, AuthSuccess> =>
|
||||||
|
pipe(
|
||||||
|
this.subsonic.ping(credentials),
|
||||||
|
TE.flatMap(({ type }) => TE.tryCatch(
|
||||||
|
() => this.libraryFor({ ...credentials, type }).then(library => ({ type, library })),
|
||||||
|
() => new AuthFailure("Failed to get library")
|
||||||
|
)),
|
||||||
|
TE.flatMap(({ library, type }) => pipe(
|
||||||
|
library.bearerToken(credentials),
|
||||||
|
TE.map(bearer => ({ bearer, type }))
|
||||||
|
)),
|
||||||
|
TE.map(({ bearer, type}) => ({
|
||||||
|
serviceToken: asToken({ ...credentials, bearer, type }),
|
||||||
|
userId: credentials.username,
|
||||||
|
nickname: credentials.username,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
refreshToken = (serviceToken: string) =>
|
||||||
|
this.generateToken(parseToken(serviceToken));
|
||||||
|
|
||||||
|
login = async (token: string) => this.libraryFor(parseToken(token));
|
||||||
|
|
||||||
|
private libraryFor = (
|
||||||
|
credentials: Credentials & { type: string }
|
||||||
|
): Promise<SubsonicMusicLibrary> => {
|
||||||
|
const genericSubsonic = new SubsonicMusicLibrary(
|
||||||
|
this.subsonic,
|
||||||
|
credentials,
|
||||||
|
this.customPlayers
|
||||||
|
);
|
||||||
|
// return Promise.resolve(genericSubsonic);
|
||||||
|
|
||||||
|
if (credentials.type == "navidrome") {
|
||||||
|
// todo: there does not seem to be a test for this??
|
||||||
|
const nd: SubsonicMusicLibrary = {
|
||||||
|
...genericSubsonic,
|
||||||
|
flavour: () => "navidrome",
|
||||||
|
bearerToken: (credentials: Credentials) =>
|
||||||
|
pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
axios.post(
|
||||||
|
this.subsonic.url.append({ pathname: "/auth/login" }).href(),
|
||||||
|
_.pick(credentials, "username", "password")
|
||||||
|
),
|
||||||
|
() => new AuthFailure("Failed to get bearerToken")
|
||||||
|
),
|
||||||
|
TE.map((it) => it.data.token as string | undefined)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return Promise.resolve(nd);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(genericSubsonic);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubsonicMusicLibrary implements MusicLibrary {
|
||||||
|
subsonic: Subsonic;
|
||||||
|
credentials: Credentials;
|
||||||
|
customPlayers: CustomPlayers;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
subsonic: Subsonic,
|
||||||
|
credentials: Credentials,
|
||||||
|
customPlayers: CustomPlayers
|
||||||
|
) {
|
||||||
|
this.subsonic = subsonic;
|
||||||
|
this.credentials = credentials;
|
||||||
|
this.customPlayers = customPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
flavour = () => "subsonic";
|
||||||
|
|
||||||
|
bearerToken = (_: Credentials) =>
|
||||||
|
TE.right<AuthFailure, string | undefined>(undefined);
|
||||||
|
|
||||||
|
// todo: q needs to support greater than the max page size supported by subsonic
|
||||||
|
// maybe subsonic should error?
|
||||||
|
artists = (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
||||||
|
this.subsonic
|
||||||
|
.getArtists(this.credentials)
|
||||||
|
.then(slice2(q))
|
||||||
|
.then(([page, total]) => ({
|
||||||
|
total,
|
||||||
|
results: page,
|
||||||
|
}));
|
||||||
|
|
||||||
|
artist = async (id: string): Promise<Artist> =>
|
||||||
|
Promise.all([
|
||||||
|
this.subsonic.getArtist(this.credentials, id),
|
||||||
|
this.subsonic.getArtistInfo(this.credentials, id),
|
||||||
|
]).then(([artist, artistInfo]) => ({
|
||||||
|
id: artist.id,
|
||||||
|
name: artist.name,
|
||||||
|
image: artistImageURN({
|
||||||
|
artistId: artist.id,
|
||||||
|
artistImageURL: [
|
||||||
|
artist.artistImageUrl,
|
||||||
|
// todo: subsonic.artistInfo should just return a valid image or undefined, then the music lib just chooses first undefined
|
||||||
|
// out of artist.image and artistInfo.image
|
||||||
|
artistInfo.images.l,
|
||||||
|
artistInfo.images.m,
|
||||||
|
artistInfo.images.s,
|
||||||
|
// todo: do we still need this isValidImage?
|
||||||
|
].find(isValidImage),
|
||||||
|
}),
|
||||||
|
albums: artist.albums,
|
||||||
|
similarArtists: artistInfo.similarArtist,
|
||||||
|
}));
|
||||||
|
|
||||||
|
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
||||||
|
this.subsonic.getAlbumList2(this.credentials, q);
|
||||||
|
|
||||||
|
album = (id: string): Promise<Album> =>
|
||||||
|
this.subsonic.getAlbum(this.credentials, id);
|
||||||
|
|
||||||
|
genres = () =>
|
||||||
|
this.subsonic.getGenres(this.credentials);
|
||||||
|
|
||||||
|
track = (trackId: string) =>
|
||||||
|
this.subsonic.getTrack(this.credentials, trackId);
|
||||||
|
|
||||||
|
rate = (trackId: string, rating: Rating) =>
|
||||||
|
// todo: this is a bit odd
|
||||||
|
Promise.resolve(true)
|
||||||
|
.then(() => {
|
||||||
|
if (rating.stars >= 0 && rating.stars <= 5) {
|
||||||
|
return this.subsonic.getTrack(this.credentials, trackId);
|
||||||
|
} else {
|
||||||
|
throw `Invalid rating.stars value of ${rating.stars}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((track) => {
|
||||||
|
const thingsToUpdate = [];
|
||||||
|
if (track.rating.love != rating.love) {
|
||||||
|
thingsToUpdate.push(
|
||||||
|
(rating.love ? this.subsonic.star : this.subsonic.unstar)(this.credentials,{ id: trackId })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (track.rating.stars != rating.stars) {
|
||||||
|
thingsToUpdate.push(
|
||||||
|
this.subsonic.setRating(this.credentials, trackId, rating.stars)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(thingsToUpdate);
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
stream = async ({
|
||||||
|
trackId,
|
||||||
|
range,
|
||||||
|
}: {
|
||||||
|
trackId: string;
|
||||||
|
range: string | undefined;
|
||||||
|
}) =>
|
||||||
|
this.subsonic
|
||||||
|
.getTrack(this.credentials, trackId)
|
||||||
|
.then((track) =>
|
||||||
|
this.subsonic.stream(this.credentials, trackId, track.encoding.player, range)
|
||||||
|
);
|
||||||
|
|
||||||
|
coverArt = async (coverArtURN: BUrn, size?: number) =>
|
||||||
|
Promise.resolve(coverArtURN)
|
||||||
|
.then((it) => assertSystem(it, "subsonic"))
|
||||||
|
.then((it) =>
|
||||||
|
this.subsonic.getCoverArt(
|
||||||
|
this.credentials,
|
||||||
|
it.resource.split(":")[1]!,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then((res) => ({
|
||||||
|
contentType: res.headers["content-type"],
|
||||||
|
data: Buffer.from(res.data, "binary"),
|
||||||
|
}))
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(`Failed getting coverArt for urn:'${coverArtURN}': ${e}`);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
// todo: unit test the difference between scrobble and nowPlaying
|
||||||
|
scrobble = async (id: string) =>
|
||||||
|
this.subsonic.scrobble(this.credentials, id, true);
|
||||||
|
|
||||||
|
nowPlaying = async (id: string) =>
|
||||||
|
this.subsonic.scrobble(this.credentials, id, false);
|
||||||
|
|
||||||
|
searchArtists = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, artistCount: 20 })
|
||||||
|
.then(({ artists }) =>
|
||||||
|
artists.map((artist) => ({
|
||||||
|
id: artist.id,
|
||||||
|
name: artist.name,
|
||||||
|
image: artistImageURN({
|
||||||
|
artistId: artist.id,
|
||||||
|
artistImageURL: artist.artistImageUrl,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
searchAlbums = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, albumCount: 20 })
|
||||||
|
.then(({ albums }) => this.subsonic.toAlbumSummary(albums));
|
||||||
|
|
||||||
|
searchTracks = async (query: string) =>
|
||||||
|
this.subsonic
|
||||||
|
.search3(this.credentials, { query, songCount: 20 })
|
||||||
|
.then(({ songs }) =>
|
||||||
|
Promise.all(
|
||||||
|
songs.map((it) => this.subsonic.getTrack(this.credentials, it.id))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
playlists = async () =>
|
||||||
|
this.subsonic.playlists(this.credentials);
|
||||||
|
|
||||||
|
playlist = async (id: string) =>
|
||||||
|
this.subsonic.playlist(this.credentials, id);
|
||||||
|
|
||||||
|
createPlaylist = async (name: string) =>
|
||||||
|
this.subsonic.createPlayList(this.credentials, name);
|
||||||
|
|
||||||
|
deletePlaylist = async (id: string) =>
|
||||||
|
this.subsonic.deletePlayList(this.credentials, id);
|
||||||
|
|
||||||
|
addToPlaylist = async (playlistId: string, trackId: string) =>
|
||||||
|
this.subsonic.updatePlaylist(this.credentials, playlistId, { songIdToAdd: trackId });
|
||||||
|
|
||||||
|
removeFromPlaylist = async (playlistId: string, indicies: number[]) =>
|
||||||
|
this.subsonic.updatePlaylist(this.credentials, playlistId, { songIndexToRemove: indicies });
|
||||||
|
|
||||||
|
similarSongs = async (id: string) =>
|
||||||
|
this.subsonic.getSimilarSongs2(this.credentials, id)
|
||||||
|
|
||||||
|
topSongs = async (artistId: string) =>
|
||||||
|
this.subsonic.getArtist(this.credentials, artistId)
|
||||||
|
.then(({ name }) =>
|
||||||
|
this.subsonic.getTopSongs(this.credentials, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
radioStations = async () =>
|
||||||
|
this.subsonic.getInternetRadioStations(this.credentials);
|
||||||
|
|
||||||
|
radioStation = async (id: string) =>
|
||||||
|
this.radioStations().then((it) => it.find((station) => station.id === id)!);
|
||||||
|
|
||||||
|
years = async () => {
|
||||||
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 100000, // FIXME: better than this, probably doesnt work anyway as max _count is 500 or something
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
|
const years = this.subsonic
|
||||||
|
.getAlbumList2(this.credentials, q)
|
||||||
|
.then(({ results }) =>
|
||||||
|
results
|
||||||
|
.map((album) => album.year || "?")
|
||||||
|
.filter((item, i, ar) => ar.indexOf(item) === i)
|
||||||
|
.sort()
|
||||||
|
.map((year) => ({
|
||||||
|
...asYear(year),
|
||||||
|
}))
|
||||||
|
.reverse()
|
||||||
|
);
|
||||||
|
return years;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,14 +8,14 @@ import {
|
|||||||
Album,
|
Album,
|
||||||
Artist,
|
Artist,
|
||||||
Track,
|
Track,
|
||||||
albumToAlbumSummary,
|
|
||||||
artistToArtistSummary,
|
|
||||||
PlaylistSummary,
|
PlaylistSummary,
|
||||||
Playlist,
|
Playlist,
|
||||||
SimilarArtist,
|
SimilarArtist,
|
||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
RadioStation
|
RadioStation,
|
||||||
} from "../src/music_service";
|
ArtistSummary,
|
||||||
|
TrackSummary
|
||||||
|
} from "../src/music_library";
|
||||||
|
|
||||||
import { b64Encode } from "../src/b64";
|
import { b64Encode } from "../src/b64";
|
||||||
import { artistImageURN } from "../src/subsonic";
|
import { artistImageURN } from "../src/subsonic";
|
||||||
@@ -116,13 +116,26 @@ export function aSimilarArtist(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anArtist(fields: Partial<Artist> = {}): Artist {
|
export function anArtistSummary(fields: Partial<ArtistSummary> = {}): ArtistSummary {
|
||||||
const id = fields.id || uuid();
|
const id = fields.id || uuid();
|
||||||
const artist = {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `Artist ${id}`,
|
name: `Artist ${id}`,
|
||||||
albums: [anAlbum(), anAlbum(), anAlbum()],
|
|
||||||
image: { system: "subsonic", resource: `art:${id}` },
|
image: { system: "subsonic", resource: `art:${id}` },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function anArtist(fields: Partial<Artist> = {}): Artist {
|
||||||
|
const id = fields.id || uuid();
|
||||||
|
const name = `Artist ${randomstring.generate()}`
|
||||||
|
const albums = fields.albums || [
|
||||||
|
anAlbumSummary({ artistId: id, artistName: name }),
|
||||||
|
anAlbumSummary({ artistId: id, artistName: name }),
|
||||||
|
anAlbumSummary({ artistId: id, artistName: name })
|
||||||
|
];
|
||||||
|
const artist = {
|
||||||
|
...anArtistSummary({ id, name }),
|
||||||
|
albums,
|
||||||
similarArtists: [
|
similarArtists: [
|
||||||
aSimilarArtist({ id: uuid(), name: "Similar artist1", inLibrary: true }),
|
aSimilarArtist({ id: uuid(), name: "Similar artist1", inLibrary: true }),
|
||||||
aSimilarArtist({ id: uuid(), name: "Similar artist2", inLibrary: true }),
|
aSimilarArtist({ id: uuid(), name: "Similar artist2", inLibrary: true }),
|
||||||
@@ -166,9 +179,9 @@ export const SAMPLE_GENRES = [
|
|||||||
];
|
];
|
||||||
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];
|
export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)];
|
||||||
|
|
||||||
export function aTrack(fields: Partial<Track> = {}): Track {
|
export function aTrackSummary(fields: Partial<TrackSummary> = {}): TrackSummary {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
const artist = anArtist();
|
const artist = fields.artist || anArtistSummary();
|
||||||
const genre = fields.genre || randomGenre();
|
const genre = fields.genre || randomGenre();
|
||||||
const rating = { love: false, stars: Math.floor(Math.random() * 5) };
|
const rating = { love: false, stars: Math.floor(Math.random() * 5) };
|
||||||
return {
|
return {
|
||||||
@@ -181,28 +194,53 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
|||||||
duration: randomInt(500),
|
duration: randomInt(500),
|
||||||
number: randomInt(100),
|
number: randomInt(100),
|
||||||
genre,
|
genre,
|
||||||
artist: artistToArtistSummary(artist),
|
artist,
|
||||||
album: albumToAlbumSummary(
|
|
||||||
anAlbum({ artistId: artist.id, artistName: artist.name, genre })
|
|
||||||
),
|
|
||||||
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
coverArt: { system: "subsonic", resource: `art:${uuid()}`},
|
||||||
rating,
|
rating,
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export function anAlbum(fields: Partial<Album> = {}): Album {
|
export function aTrack(fields: Partial<Track> = {}): Track {
|
||||||
|
const summary = aTrackSummary(fields);
|
||||||
|
const album = fields.album || anAlbumSummary({ artistId: summary.artist.id, artistName: summary.artist.name, genre: summary.genre })
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
album,
|
||||||
|
...fields
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function anAlbumSummary(fields: Partial<AlbumSummary> = {}): AlbumSummary {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `Album ${id}`,
|
name: `Album ${id}`,
|
||||||
genre: randomGenre(),
|
|
||||||
year: `19${randomInt(99)}`,
|
year: `19${randomInt(99)}`,
|
||||||
|
genre: randomGenre(),
|
||||||
|
coverArt: { system: "subsonic", resource: `art:${uuid()}` },
|
||||||
artistId: `Artist ${uuid()}`,
|
artistId: `Artist ${uuid()}`,
|
||||||
artistName: `Artist ${randomstring.generate()}`,
|
artistName: `Artist ${randomstring.generate()}`,
|
||||||
coverArt: { system: "subsonic", resource: `art:${uuid()}` },
|
...fields
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||||
|
const albumSummary = anAlbumSummary()
|
||||||
|
const album = {
|
||||||
|
...albumSummary,
|
||||||
|
tracks: [],
|
||||||
...fields,
|
...fields,
|
||||||
};
|
};
|
||||||
|
const artistSummary = anArtistSummary({ id: album.artistId, name: album.artistName })
|
||||||
|
const tracks = fields.tracks || [
|
||||||
|
aTrack({ album: albumSummary, artist: artistSummary }),
|
||||||
|
aTrack({ album: albumSummary, artist: artistSummary })
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
...album,
|
||||||
|
tracks
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function aRadioStation(fields: Partial<RadioStation> = {}): RadioStation {
|
export function aRadioStation(fields: Partial<RadioStation> = {}): RadioStation {
|
||||||
@@ -216,20 +254,6 @@ export function aRadioStation(fields: Partial<RadioStation> = {}): RadioStation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anAlbumSummary(fields: Partial<AlbumSummary> = {}): AlbumSummary {
|
|
||||||
const id = uuid();
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: `Album ${id}`,
|
|
||||||
year: `19${randomInt(99)}`,
|
|
||||||
genre: randomGenre(),
|
|
||||||
coverArt: { system: "subsonic", resource: `art:${uuid()}` },
|
|
||||||
artistId: `Artist ${uuid()}`,
|
|
||||||
artistName: `Artist ${randomstring.generate()}`,
|
|
||||||
...fields
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BLONDIE_ID = uuid();
|
export const BLONDIE_ID = uuid();
|
||||||
export const BLONDIE_NAME = "Blondie";
|
export const BLONDIE_NAME = "Blondie";
|
||||||
export const BLONDIE: Artist = {
|
export const BLONDIE: Artist = {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { InMemoryMusicService } from "./in_memory_music_service";
|
|||||||
import {
|
import {
|
||||||
MusicLibrary,
|
MusicLibrary,
|
||||||
artistToArtistSummary,
|
artistToArtistSummary,
|
||||||
albumToAlbumSummary,
|
} from "../src/music_library";
|
||||||
} from "../src/music_service";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import {
|
import {
|
||||||
anArtist,
|
anArtist,
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
METAL,
|
METAL,
|
||||||
HIP_HOP,
|
HIP_HOP,
|
||||||
SKA,
|
SKA,
|
||||||
|
anAlbumSummary,
|
||||||
} from "./builders";
|
} from "./builders";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
|
||||||
@@ -167,23 +167,6 @@ describe("InMemoryMusicService", () => {
|
|||||||
service.hasTracks(track1, track2, track3, track4);
|
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, rating: { love: false, stars: 0 } },
|
|
||||||
{ ...track2, rating: { love: false, stars: 0 } },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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("fetching a single track", () => {
|
describe("fetching a single track", () => {
|
||||||
describe("when it exists", () => {
|
describe("when it exists", () => {
|
||||||
it("should return the track", async () => {
|
it("should return the track", async () => {
|
||||||
@@ -194,16 +177,16 @@ describe("InMemoryMusicService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("albums", () => {
|
describe("albums", () => {
|
||||||
const artist1_album1 = anAlbum({ genre: POP });
|
const artist1_album1 = anAlbumSummary({ genre: POP });
|
||||||
const artist1_album2 = anAlbum({ genre: ROCK });
|
const artist1_album2 = anAlbumSummary({ genre: ROCK });
|
||||||
const artist1_album3 = anAlbum({ genre: METAL });
|
const artist1_album3 = anAlbumSummary({ genre: METAL });
|
||||||
const artist1_album4 = anAlbum({ genre: POP });
|
const artist1_album4 = anAlbumSummary({ genre: POP });
|
||||||
const artist1_album5 = anAlbum({ genre: POP });
|
const artist1_album5 = anAlbumSummary({ genre: POP });
|
||||||
|
|
||||||
const artist2_album1 = anAlbum({ genre: METAL });
|
const artist2_album1 = anAlbumSummary({ genre: METAL });
|
||||||
|
|
||||||
const artist3_album1 = anAlbum({ genre: HIP_HOP });
|
const artist3_album1 = anAlbumSummary({ genre: HIP_HOP });
|
||||||
const artist3_album2 = anAlbum({ genre: POP });
|
const artist3_album2 = anAlbumSummary({ genre: POP });
|
||||||
|
|
||||||
const artist1 = anArtist({
|
const artist1 = anArtist({
|
||||||
name: "artist1",
|
name: "artist1",
|
||||||
@@ -212,8 +195,8 @@ describe("InMemoryMusicService", () => {
|
|||||||
artist1_album2,
|
artist1_album2,
|
||||||
artist1_album3,
|
artist1_album3,
|
||||||
artist1_album4,
|
artist1_album4,
|
||||||
artist1_album5,
|
artist1_album5
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] });
|
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] });
|
||||||
const artist3 = anArtist({
|
const artist3 = anArtist({
|
||||||
@@ -275,16 +258,16 @@ describe("InMemoryMusicService", () => {
|
|||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [
|
results: [
|
||||||
albumToAlbumSummary(artist1_album1),
|
artist1_album1,
|
||||||
albumToAlbumSummary(artist1_album2),
|
artist1_album2,
|
||||||
albumToAlbumSummary(artist1_album3),
|
artist1_album3,
|
||||||
albumToAlbumSummary(artist1_album4),
|
artist1_album4,
|
||||||
albumToAlbumSummary(artist1_album5),
|
artist1_album5,
|
||||||
|
|
||||||
albumToAlbumSummary(artist2_album1),
|
artist2_album1,
|
||||||
|
|
||||||
albumToAlbumSummary(artist3_album1),
|
artist3_album1,
|
||||||
albumToAlbumSummary(artist3_album2),
|
artist3_album2,
|
||||||
],
|
],
|
||||||
total: totalAlbumCount,
|
total: totalAlbumCount,
|
||||||
});
|
});
|
||||||
@@ -300,7 +283,7 @@ describe("InMemoryMusicService", () => {
|
|||||||
type: "alphabeticalByName",
|
type: "alphabeticalByName",
|
||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: _.sortBy(allAlbums, "name").map(albumToAlbumSummary),
|
results: _.sortBy(allAlbums, "name"),
|
||||||
total: totalAlbumCount,
|
total: totalAlbumCount,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -317,9 +300,9 @@ describe("InMemoryMusicService", () => {
|
|||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [
|
results: [
|
||||||
albumToAlbumSummary(artist1_album5),
|
artist1_album5,
|
||||||
albumToAlbumSummary(artist2_album1),
|
artist2_album1,
|
||||||
albumToAlbumSummary(artist3_album1),
|
artist3_album1,
|
||||||
],
|
],
|
||||||
total: totalAlbumCount,
|
total: totalAlbumCount,
|
||||||
});
|
});
|
||||||
@@ -336,8 +319,8 @@ describe("InMemoryMusicService", () => {
|
|||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [
|
results: [
|
||||||
albumToAlbumSummary(artist3_album1),
|
artist3_album1,
|
||||||
albumToAlbumSummary(artist3_album2),
|
artist3_album2,
|
||||||
],
|
],
|
||||||
total: totalAlbumCount,
|
total: totalAlbumCount,
|
||||||
});
|
});
|
||||||
@@ -357,10 +340,10 @@ describe("InMemoryMusicService", () => {
|
|||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [
|
results: [
|
||||||
albumToAlbumSummary(artist1_album1),
|
artist1_album1,
|
||||||
albumToAlbumSummary(artist1_album4),
|
artist1_album4,
|
||||||
albumToAlbumSummary(artist1_album5),
|
artist1_album5,
|
||||||
albumToAlbumSummary(artist3_album2),
|
artist3_album2,
|
||||||
],
|
],
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
@@ -379,8 +362,8 @@ describe("InMemoryMusicService", () => {
|
|||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [
|
results: [
|
||||||
albumToAlbumSummary(artist1_album4),
|
artist1_album4,
|
||||||
albumToAlbumSummary(artist1_album5),
|
artist1_album5,
|
||||||
],
|
],
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
@@ -397,7 +380,7 @@ describe("InMemoryMusicService", () => {
|
|||||||
_count: 100,
|
_count: 100,
|
||||||
})
|
})
|
||||||
).toEqual({
|
).toEqual({
|
||||||
results: [albumToAlbumSummary(artist3_album2)],
|
results: [artist3_album2],
|
||||||
total: 4,
|
total: 4,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -424,7 +407,10 @@ describe("InMemoryMusicService", () => {
|
|||||||
describe("when it exists", () => {
|
describe("when it exists", () => {
|
||||||
it("should provide an album", async () => {
|
it("should provide an album", async () => {
|
||||||
expect(await musicLibrary.album(artist1_album5.id)).toEqual(
|
expect(await musicLibrary.album(artist1_album5.id)).toEqual(
|
||||||
artist1_album5
|
{
|
||||||
|
...artist1_album5,
|
||||||
|
tracks: []
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ import {
|
|||||||
slice2,
|
slice2,
|
||||||
asResult,
|
asResult,
|
||||||
artistToArtistSummary,
|
artistToArtistSummary,
|
||||||
albumToAlbumSummary,
|
|
||||||
Track,
|
Track,
|
||||||
Genre,
|
Genre,
|
||||||
Rating,
|
Rating,
|
||||||
} from "../src/music_service";
|
} from "../src/music_library";
|
||||||
import { BUrn } from "../src/burn";
|
import { BUrn } from "../src/burn";
|
||||||
|
|
||||||
export class InMemoryMusicService implements MusicService {
|
export class InMemoryMusicService implements MusicService {
|
||||||
@@ -97,14 +96,13 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((matches) => matches.map((it) => it.album))
|
.then((matches) => matches.map((it) => it.album))
|
||||||
.then((it) => it.map(albumToAlbumSummary))
|
|
||||||
.then(slice2(q))
|
.then(slice2(q))
|
||||||
.then(asResult),
|
.then(asResult),
|
||||||
album: (id: string) =>
|
album: (id: string) =>
|
||||||
pipe(
|
pipe(
|
||||||
this.artists.flatMap((it) => it.albums).find((it) => it.id === id),
|
this.artists.flatMap((it) => it.albums).find((it) => it.id === id),
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.map((it) => Promise.resolve(it)),
|
O.map((it) => Promise.resolve({ ...it, tracks: [] })),
|
||||||
O.getOrElse(() => Promise.reject(`No album with id '${id}'`))
|
O.getOrElse(() => Promise.reject(`No album with id '${id}'`))
|
||||||
),
|
),
|
||||||
genres: () =>
|
genres: () =>
|
||||||
@@ -119,12 +117,6 @@ export class InMemoryMusicService implements MusicService {
|
|||||||
A.sort(fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id)))
|
A.sort(fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id)))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
tracks: (albumId: string) =>
|
|
||||||
Promise.resolve(
|
|
||||||
this.tracks
|
|
||||||
.filter((it) => it.album.id === albumId)
|
|
||||||
.map((it) => ({ ...it, rating: { love: false, stars: 0 } }))
|
|
||||||
),
|
|
||||||
rate: (_: string, _2: Rating) => Promise.resolve(false),
|
rate: (_: string, _2: Rating) => Promise.resolve(false),
|
||||||
track: (trackId: string) =>
|
track: (trackId: string) =>
|
||||||
pipe(
|
pipe(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { anArtist } from "./builders";
|
import { anArtist } from "./builders";
|
||||||
import { artistToArtistSummary } from "../src/music_service";
|
import { artistToArtistSummary } from "../src/music_library";
|
||||||
|
|
||||||
describe("artistToArtistSummary", () => {
|
describe("artistToArtistSummary", () => {
|
||||||
it("should map fields correctly", () => {
|
it("should map fields correctly", () => {
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "./builders";
|
} from "./builders";
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import { InMemoryLinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes } from "../src/link_codes";
|
||||||
import { Credentials } from "../src/music_service";
|
import { Credentials } from "../src/music_library";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { Service, bonobService, Sonos } from "../src/sonos";
|
import { Service, bonobService, Sonos } from "../src/sonos";
|
||||||
import supersoap from "./supersoap";
|
import supersoap from "./supersoap";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import request from "supertest";
|
|||||||
import Image from "image-js";
|
import Image from "image-js";
|
||||||
import { either as E, taskEither as TE } from "fp-ts";
|
import { either as E, taskEither as TE } from "fp-ts";
|
||||||
|
|
||||||
import { AuthFailure, MusicService } from "../src/music_service";
|
import { AuthFailure, MusicService } from "../src/music_library";
|
||||||
import makeServer, {
|
import makeServer, {
|
||||||
BONOB_ACCESS_TOKEN_HEADER,
|
BONOB_ACCESS_TOKEN_HEADER,
|
||||||
RangeBytesFromFilter,
|
RangeBytesFromFilter,
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
PUNK,
|
PUNK,
|
||||||
aPlaylist,
|
aPlaylist,
|
||||||
aRadioStation,
|
aRadioStation,
|
||||||
|
anArtistSummary,
|
||||||
|
anAlbumSummary,
|
||||||
} 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";
|
||||||
@@ -49,7 +51,7 @@ import {
|
|||||||
artistToArtistSummary,
|
artistToArtistSummary,
|
||||||
MusicService,
|
MusicService,
|
||||||
playlistToPlaylistSummary,
|
playlistToPlaylistSummary,
|
||||||
} from "../src/music_service";
|
} from "../src/music_library";
|
||||||
import { APITokens } from "../src/api_tokens";
|
import { APITokens } from "../src/api_tokens";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import url, { URLBuilder } from "../src/url_builder";
|
import url, { URLBuilder } from "../src/url_builder";
|
||||||
@@ -2356,10 +2358,8 @@ describe("wsdl api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("asking for an album", () => {
|
describe("asking for an album", () => {
|
||||||
const album = anAlbum();
|
const album = anAlbumSummary();
|
||||||
const artist = anArtist({
|
const artist = anArtistSummary();
|
||||||
albums: [album],
|
|
||||||
});
|
|
||||||
|
|
||||||
const track1 = aTrack({ artist, album, number: 1 });
|
const track1 = aTrack({ artist, album, number: 1 });
|
||||||
const track2 = aTrack({ artist, album, number: 2 });
|
const track2 = aTrack({ artist, album, number: 2 });
|
||||||
@@ -2370,7 +2370,12 @@ describe("wsdl api", () => {
|
|||||||
const tracks = [track1, track2, track3, track4, track5];
|
const tracks = [track1, track2, track3, track4, track5];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
musicLibrary.tracks.mockResolvedValue(tracks);
|
musicLibrary.album.mockResolvedValue(anAlbum({
|
||||||
|
...album,
|
||||||
|
artistName: artist.name,
|
||||||
|
artistId: artist.id,
|
||||||
|
tracks
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("asking for all for an album", () => {
|
describe("asking for all for an album", () => {
|
||||||
@@ -2394,7 +2399,7 @@ describe("wsdl api", () => {
|
|||||||
total: tracks.length,
|
total: tracks.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(musicLibrary.tracks).toHaveBeenCalledWith(album.id);
|
expect(musicLibrary.album).toHaveBeenCalledWith(album.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2421,7 +2426,7 @@ describe("wsdl api", () => {
|
|||||||
total: tracks.length,
|
total: tracks.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(musicLibrary.tracks).toHaveBeenCalledWith(album.id);
|
expect(musicLibrary.album).toHaveBeenCalledWith(album.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3950
tests/subsonic_music_library.test.ts
Normal file
3950
tests/subsonic_music_library.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user