mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Move getGenres onto subsonic
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { option as O } from "fp-ts";
|
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 { pipe } from "fp-ts/lib/function";
|
||||||
import { Md5 } from "ts-md5";
|
import { Md5 } from "ts-md5";
|
||||||
import {
|
import {
|
||||||
@@ -195,13 +197,15 @@ export type GetTopSongsResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GetInternetRadioStationsResponse = {
|
export type GetInternetRadioStationsResponse = {
|
||||||
internetRadioStations: { internetRadioStation: {
|
internetRadioStations: {
|
||||||
id: string,
|
internetRadioStation: {
|
||||||
name: string,
|
id: string;
|
||||||
streamUrl: string,
|
name: string;
|
||||||
homePageUrl?: string }[]
|
streamUrl: string;
|
||||||
}
|
homePageUrl?: string;
|
||||||
}
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type GetSongResponse = {
|
export type GetSongResponse = {
|
||||||
song: song;
|
song: song;
|
||||||
@@ -274,14 +278,20 @@ export const artistImageURN = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asTrack = (album: Album, song: song, customPlayers: CustomPlayers): Track => ({
|
export const asTrack = (
|
||||||
|
album: Album,
|
||||||
|
song: song,
|
||||||
|
customPlayers: CustomPlayers
|
||||||
|
): Track => ({
|
||||||
id: song.id,
|
id: song.id,
|
||||||
name: song.title,
|
name: song.title,
|
||||||
encoding: pipe(
|
encoding: pipe(
|
||||||
customPlayers.encodingFor({ mimeType: song.contentType }),
|
customPlayers.encodingFor({ mimeType: song.contentType }),
|
||||||
O.getOrElse(() => ({
|
O.getOrElse(() => ({
|
||||||
player: DEFAULT_CLIENT_APPLICATION,
|
player: DEFAULT_CLIENT_APPLICATION,
|
||||||
mimeType: song.transcodedContentType ? song.transcodedContentType : song.contentType
|
mimeType: song.transcodedContentType
|
||||||
|
? song.transcodedContentType
|
||||||
|
: song.contentType,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
duration: song.duration || 0,
|
duration: song.duration || 0,
|
||||||
@@ -327,7 +337,9 @@ export const asGenre = (genreName: string) => ({
|
|||||||
name: genreName,
|
name: genreName,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const maybeAsGenre = (genreName: string | undefined): Genre | undefined =>
|
export const maybeAsGenre = (
|
||||||
|
genreName: string | undefined
|
||||||
|
): Genre | undefined =>
|
||||||
pipe(
|
pipe(
|
||||||
genreName,
|
genreName,
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
@@ -340,7 +352,7 @@ export const asYear = (year: string) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export interface CustomPlayers {
|
export interface CustomPlayers {
|
||||||
encodingFor({ mimeType }: { mimeType: string }): O.Option<Encoding>
|
encodingFor({ mimeType }: { mimeType: string }): O.Option<Encoding>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomClient = {
|
export type CustomClient = {
|
||||||
@@ -367,21 +379,22 @@ export class TranscodingCustomPlayers implements CustomPlayers {
|
|||||||
return new TranscodingCustomPlayers(new Map(parts));
|
return new TranscodingCustomPlayers(new Map(parts));
|
||||||
}
|
}
|
||||||
|
|
||||||
encodingFor = ({ mimeType }: { mimeType: string }): O.Option<Encoding> => pipe(
|
encodingFor = ({ mimeType }: { mimeType: string }): O.Option<Encoding> =>
|
||||||
|
pipe(
|
||||||
this.transcodings.get(mimeType),
|
this.transcodings.get(mimeType),
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.map(transcodedMimeType => ({
|
O.map((transcodedMimeType) => ({
|
||||||
player:`${DEFAULT_CLIENT_APPLICATION}+${mimeType}`,
|
player: `${DEFAULT_CLIENT_APPLICATION}+${mimeType}`,
|
||||||
mimeType: transcodedMimeType
|
mimeType: transcodedMimeType,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NO_CUSTOM_PLAYERS: CustomPlayers = {
|
export const NO_CUSTOM_PLAYERS: CustomPlayers = {
|
||||||
encodingFor(_) {
|
encodingFor(_) {
|
||||||
return O.none
|
return O.none;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DEFAULT_CLIENT_APPLICATION = "bonob";
|
export const DEFAULT_CLIENT_APPLICATION = "bonob";
|
||||||
export const USER_AGENT = "bonob";
|
export const USER_AGENT = "bonob";
|
||||||
@@ -674,8 +687,8 @@ export class Subsonic {
|
|||||||
this.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
|
this.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
|
||||||
type: AlbumQueryTypeToSubsonicType[q.type],
|
type: AlbumQueryTypeToSubsonicType[q.type],
|
||||||
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
|
...(q.genre ? { genre: b64Decode(q.genre) } : {}),
|
||||||
...(q.fromYear ? { fromYear: q.fromYear} : {}),
|
...(q.fromYear ? { fromYear: q.fromYear } : {}),
|
||||||
...(q.toYear ? { toYear: q.toYear} : {}),
|
...(q.toYear ? { toYear: q.toYear } : {}),
|
||||||
size: 500,
|
size: 500,
|
||||||
offset: q._index,
|
offset: q._index,
|
||||||
})
|
})
|
||||||
@@ -686,11 +699,22 @@ export class Subsonic {
|
|||||||
total: albums.length == 500 ? total : q._index + albums.length,
|
total: albums.length == 500 ? total : q._index + albums.length,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
getGenres = (credentials: Credentials) =>
|
||||||
|
this.getJSON<GetGenresResponse>(credentials, "/rest/getGenres").then((it) =>
|
||||||
|
pipe(
|
||||||
|
it.genres.genre || [],
|
||||||
|
A.filter((it) => it.albumCount > 0),
|
||||||
|
A.map((it) => it.value),
|
||||||
|
A.sort(ordString),
|
||||||
|
A.map(maybeAsGenre),
|
||||||
|
A.filter((it) => it != undefined)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// getStarred2 = (credentials: Credentials): Promise<{ albums: Album[] }> =>
|
// getStarred2 = (credentials: Credentials): Promise<{ albums: Album[] }> =>
|
||||||
// this.getJSON<GetStarredResponse>(credentials, "/rest/getStarred2")
|
// this.getJSON<GetStarredResponse>(credentials, "/rest/getStarred2")
|
||||||
// .then((it) => it.starred2)
|
// .then((it) => it.starred2)
|
||||||
// .then((it) => ({
|
// .then((it) => ({
|
||||||
// albums: it.album.map(asAlbum),
|
// albums: it.album.map(asAlbum),
|
||||||
// }));
|
// }));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
import { option as O, taskEither as TE } from "fp-ts";
|
import { option as O, taskEither as TE } 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 { pipe } from "fp-ts/lib/function";
|
||||||
import {
|
import {
|
||||||
Credentials,
|
Credentials,
|
||||||
@@ -23,7 +19,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Subsonic,
|
Subsonic,
|
||||||
CustomPlayers,
|
CustomPlayers,
|
||||||
GetGenresResponse,
|
|
||||||
GetAlbumResponse,
|
GetAlbumResponse,
|
||||||
asTrack,
|
asTrack,
|
||||||
asAlbum,
|
asAlbum,
|
||||||
@@ -46,11 +41,9 @@ import {
|
|||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { b64Encode } from "./b64";
|
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { assertSystem, BUrn } from "./burn";
|
import { assertSystem, BUrn } from "./burn";
|
||||||
|
|
||||||
|
|
||||||
export class SubsonicMusicService implements MusicService {
|
export class SubsonicMusicService implements MusicService {
|
||||||
subsonic: Subsonic;
|
subsonic: Subsonic;
|
||||||
customPlayers: CustomPlayers;
|
customPlayers: CustomPlayers;
|
||||||
@@ -63,7 +56,9 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
this.customPlayers = customPlayers;
|
this.customPlayers = customPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateToken = (credentials: Credentials): TE.TaskEither<AuthFailure, AuthSuccess> => {
|
generateToken = (
|
||||||
|
credentials: Credentials
|
||||||
|
): TE.TaskEither<AuthFailure, AuthSuccess> => {
|
||||||
const x: TE.TaskEither<AuthFailure, PingResponse> = TE.tryCatch(
|
const x: TE.TaskEither<AuthFailure, PingResponse> = TE.tryCatch(
|
||||||
() =>
|
() =>
|
||||||
this.subsonic.getJSON<PingResponse>(
|
this.subsonic.getJSON<PingResponse>(
|
||||||
@@ -71,7 +66,7 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
"/rest/ping.view"
|
"/rest/ping.view"
|
||||||
),
|
),
|
||||||
(e) => new AuthFailure(e as string)
|
(e) => new AuthFailure(e as string)
|
||||||
)
|
);
|
||||||
return pipe(
|
return pipe(
|
||||||
x,
|
x,
|
||||||
TE.flatMap(({ type }) =>
|
TE.flatMap(({ type }) =>
|
||||||
@@ -94,8 +89,8 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
userId: credentials.username,
|
userId: credentials.username,
|
||||||
nickname: credentials.username,
|
nickname: credentials.username,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
refreshToken = (serviceToken: string) =>
|
refreshToken = (serviceToken: string) =>
|
||||||
this.generateToken(parseToken(serviceToken));
|
this.generateToken(parseToken(serviceToken));
|
||||||
@@ -105,7 +100,11 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
private libraryFor = (
|
private libraryFor = (
|
||||||
credentials: Credentials & { type: string }
|
credentials: Credentials & { type: string }
|
||||||
): Promise<SubsonicMusicLibrary> => {
|
): Promise<SubsonicMusicLibrary> => {
|
||||||
const genericSubsonic = new SubsonicMusicLibrary(this.subsonic, credentials, this.customPlayers);
|
const genericSubsonic = new SubsonicMusicLibrary(
|
||||||
|
this.subsonic,
|
||||||
|
credentials,
|
||||||
|
this.customPlayers
|
||||||
|
);
|
||||||
// return Promise.resolve(genericSubsonic);
|
// return Promise.resolve(genericSubsonic);
|
||||||
|
|
||||||
if (credentials.type == "navidrome") {
|
if (credentials.type == "navidrome") {
|
||||||
@@ -125,7 +124,7 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
),
|
),
|
||||||
TE.map((it) => it.data.token as string | undefined)
|
TE.map((it) => it.data.token as string | undefined)
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
return Promise.resolve(nd);
|
return Promise.resolve(nd);
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(genericSubsonic);
|
return Promise.resolve(genericSubsonic);
|
||||||
@@ -133,25 +132,25 @@ export class SubsonicMusicService implements MusicService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class SubsonicMusicLibrary implements MusicLibrary {
|
export class SubsonicMusicLibrary implements MusicLibrary {
|
||||||
subsonic: Subsonic;
|
subsonic: Subsonic;
|
||||||
credentials: Credentials
|
credentials: Credentials;
|
||||||
customPlayers: CustomPlayers
|
customPlayers: CustomPlayers;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
subsonic: Subsonic,
|
subsonic: Subsonic,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
customPlayers: CustomPlayers
|
customPlayers: CustomPlayers
|
||||||
) {
|
) {
|
||||||
this.subsonic = subsonic
|
this.subsonic = subsonic;
|
||||||
this.credentials = credentials
|
this.credentials = credentials;
|
||||||
this.customPlayers = customPlayers
|
this.customPlayers = customPlayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
flavour = () => "subsonic"
|
flavour = () => "subsonic";
|
||||||
|
|
||||||
bearerToken = (_: Credentials) => TE.right<AuthFailure, string | undefined>(undefined)
|
bearerToken = (_: Credentials) =>
|
||||||
|
TE.right<AuthFailure, string | undefined>(undefined);
|
||||||
|
|
||||||
artists = (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
artists = (q: ArtistQuery): Promise<Result<ArtistSummary>> =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -164,28 +163,18 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
name: it.name,
|
name: it.name,
|
||||||
image: it.image,
|
image: it.image,
|
||||||
})),
|
})),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
artist = async (id: string): Promise<Artist> =>
|
artist = async (id: string): Promise<Artist> =>
|
||||||
this.subsonic.getArtistWithInfo(this.credentials, id)
|
this.subsonic.getArtistWithInfo(this.credentials, id);
|
||||||
|
|
||||||
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
|
||||||
this.subsonic.getAlbumList2(this.credentials, q)
|
this.subsonic.getAlbumList2(this.credentials, q);
|
||||||
|
|
||||||
album = (id: string): Promise<Album> => this.subsonic.getAlbum(this.credentials, id)
|
album = (id: string): Promise<Album> =>
|
||||||
|
this.subsonic.getAlbum(this.credentials, id);
|
||||||
|
|
||||||
genres = () =>
|
genres = () => this.subsonic.getGenres(this.credentials);
|
||||||
this.subsonic
|
|
||||||
.getJSON<GetGenresResponse>(this.credentials, "/rest/getGenres")
|
|
||||||
.then((it) =>
|
|
||||||
pipe(
|
|
||||||
it.genres.genre || [],
|
|
||||||
A.filter((it) => it.albumCount > 0),
|
|
||||||
A.map((it) => it.value),
|
|
||||||
A.sort(ordString),
|
|
||||||
A.map((it) => ({ id: b64Encode(it), name: it }))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
tracks = (albumId: string) =>
|
tracks = (albumId: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -194,10 +183,13 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
})
|
})
|
||||||
.then((it) => it.album)
|
.then((it) => it.album)
|
||||||
.then((album) =>
|
.then((album) =>
|
||||||
(album.song || []).map((song) => asTrack(asAlbum(album), song, this.customPlayers))
|
(album.song || []).map((song) =>
|
||||||
|
asTrack(asAlbum(album), song, this.customPlayers)
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
|
||||||
track = (trackId: string) => this.subsonic.getTrack(this.credentials, trackId)
|
track = (trackId: string) =>
|
||||||
|
this.subsonic.getTrack(this.credentials, trackId);
|
||||||
|
|
||||||
rate = (trackId: string, rating: Rating) =>
|
rate = (trackId: string, rating: Rating) =>
|
||||||
Promise.resolve(true)
|
Promise.resolve(true)
|
||||||
@@ -232,7 +224,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
return Promise.all(thingsToUpdate);
|
return Promise.all(thingsToUpdate);
|
||||||
})
|
})
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false);
|
||||||
|
|
||||||
stream = async ({
|
stream = async ({
|
||||||
trackId,
|
trackId,
|
||||||
@@ -275,22 +267,26 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
},
|
},
|
||||||
stream: stream.data,
|
stream: stream.data,
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
|
|
||||||
coverArt = async (coverArtURN: BUrn, size?: number) =>
|
coverArt = async (coverArtURN: BUrn, size?: number) =>
|
||||||
Promise.resolve(coverArtURN)
|
Promise.resolve(coverArtURN)
|
||||||
.then((it) => assertSystem(it, "subsonic"))
|
.then((it) => assertSystem(it, "subsonic"))
|
||||||
.then((it) => this.subsonic.getCoverArt(this.credentials, it.resource.split(":")[1]!, size))
|
.then((it) =>
|
||||||
|
this.subsonic.getCoverArt(
|
||||||
|
this.credentials,
|
||||||
|
it.resource.split(":")[1]!,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
)
|
||||||
.then((res) => ({
|
.then((res) => ({
|
||||||
contentType: res.headers["content-type"],
|
contentType: res.headers["content-type"],
|
||||||
data: Buffer.from(res.data, "binary"),
|
data: Buffer.from(res.data, "binary"),
|
||||||
}))
|
}))
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error(
|
logger.error(`Failed getting coverArt for urn:'${coverArtURN}': ${e}`);
|
||||||
`Failed getting coverArt for urn:'${coverArtURN}': ${e}`
|
|
||||||
);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
})
|
});
|
||||||
|
|
||||||
scrobble = async (id: string) =>
|
scrobble = async (id: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -299,7 +295,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
submission: true,
|
submission: true,
|
||||||
})
|
})
|
||||||
.then((_) => true)
|
.then((_) => true)
|
||||||
.catch(() => false)
|
.catch(() => false);
|
||||||
|
|
||||||
nowPlaying = async (id: string) =>
|
nowPlaying = async (id: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -308,7 +304,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
submission: false,
|
submission: false,
|
||||||
})
|
})
|
||||||
.then((_) => true)
|
.then((_) => true)
|
||||||
.catch(() => false)
|
.catch(() => false);
|
||||||
|
|
||||||
searchArtists = async (query: string) =>
|
searchArtists = async (query: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -322,12 +318,12 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
artistImageURL: artist.artistImageUrl,
|
artistImageURL: artist.artistImageUrl,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
)
|
);
|
||||||
|
|
||||||
searchAlbums = async (query: string) =>
|
searchAlbums = async (query: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
.search3(this.credentials, { query, albumCount: 20 })
|
.search3(this.credentials, { query, albumCount: 20 })
|
||||||
.then(({ albums }) => this.subsonic.toAlbumSummary(albums))
|
.then(({ albums }) => this.subsonic.toAlbumSummary(albums));
|
||||||
|
|
||||||
searchTracks = async (query: string) =>
|
searchTracks = async (query: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -336,12 +332,14 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
songs.map((it) => this.subsonic.getTrack(this.credentials, it.id))
|
songs.map((it) => this.subsonic.getTrack(this.credentials, it.id))
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
playlists = async () =>
|
playlists = async () =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
.getJSON<GetPlaylistsResponse>(this.credentials, "/rest/getPlaylists")
|
.getJSON<GetPlaylistsResponse>(this.credentials, "/rest/getPlaylists")
|
||||||
.then(({ playlists }) => (playlists.playlist || []).map(asPlayListSummary))
|
.then(({ playlists }) =>
|
||||||
|
(playlists.playlist || []).map(asPlayListSummary)
|
||||||
|
);
|
||||||
|
|
||||||
playlist = async (id: string) =>
|
playlist = async (id: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -371,7 +369,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
number: trackNumber++,
|
number: trackNumber++,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
createPlaylist = async (name: string) =>
|
createPlaylist = async (name: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -382,14 +380,14 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
id: playlist.id,
|
id: playlist.id,
|
||||||
name: playlist.name,
|
name: playlist.name,
|
||||||
coverArt: coverArtURN(playlist.coverArt),
|
coverArt: coverArtURN(playlist.coverArt),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
deletePlaylist = async (id: string) =>
|
deletePlaylist = async (id: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/deletePlaylist", {
|
.getJSON<GetPlaylistResponse>(this.credentials, "/rest/deletePlaylist", {
|
||||||
id,
|
id,
|
||||||
})
|
})
|
||||||
.then((_) => true)
|
.then((_) => true);
|
||||||
|
|
||||||
addToPlaylist = async (playlistId: string, trackId: string) =>
|
addToPlaylist = async (playlistId: string, trackId: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -397,7 +395,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
playlistId,
|
playlistId,
|
||||||
songIdToAdd: trackId,
|
songIdToAdd: trackId,
|
||||||
})
|
})
|
||||||
.then((_) => true)
|
.then((_) => true);
|
||||||
|
|
||||||
removeFromPlaylist = async (playlistId: string, indicies: number[]) =>
|
removeFromPlaylist = async (playlistId: string, indicies: number[]) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -405,7 +403,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
playlistId,
|
playlistId,
|
||||||
songIndexToRemove: indicies,
|
songIndexToRemove: indicies,
|
||||||
})
|
})
|
||||||
.then((_) => true)
|
.then((_) => true);
|
||||||
|
|
||||||
similarSongs = async (id: string) =>
|
similarSongs = async (id: string) =>
|
||||||
this.subsonic
|
this.subsonic
|
||||||
@@ -423,7 +421,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
.then((album) => asTrack(album, song, this.customPlayers))
|
.then((album) => asTrack(album, song, this.customPlayers))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
topSongs = async (artistId: string) =>
|
topSongs = async (artistId: string) =>
|
||||||
this.subsonic.getArtist(this.credentials, artistId).then(({ name }) =>
|
this.subsonic.getArtist(this.credentials, artistId).then(({ name }) =>
|
||||||
@@ -442,25 +440,26 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
|
|
||||||
radioStations = async () => this.subsonic
|
radioStations = async () =>
|
||||||
|
this.subsonic
|
||||||
.getJSON<GetInternetRadioStationsResponse>(
|
.getJSON<GetInternetRadioStationsResponse>(
|
||||||
this.credentials,
|
this.credentials,
|
||||||
"/rest/getInternetRadioStations"
|
"/rest/getInternetRadioStations"
|
||||||
)
|
)
|
||||||
.then((it) => it.internetRadioStations.internetRadioStation || [])
|
.then((it) => it.internetRadioStations.internetRadioStation || [])
|
||||||
.then((stations) => stations.map((it) => ({
|
.then((stations) =>
|
||||||
|
stations.map((it) => ({
|
||||||
id: it.id,
|
id: it.id,
|
||||||
name: it.name,
|
name: it.name,
|
||||||
url: it.streamUrl,
|
url: it.streamUrl,
|
||||||
homePage: it.homePageUrl
|
homePage: it.homePageUrl,
|
||||||
})))
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
radioStation = async (id: string) => this.radioStations()
|
radioStation = async (id: string) =>
|
||||||
.then(it =>
|
this.radioStations().then((it) => it.find((station) => station.id === id)!);
|
||||||
it.find(station => station.id === id)!
|
|
||||||
)
|
|
||||||
|
|
||||||
years = async () => {
|
years = async () => {
|
||||||
const q: AlbumQuery = {
|
const q: AlbumQuery = {
|
||||||
@@ -468,16 +467,18 @@ export class SubsonicMusicLibrary implements MusicLibrary {
|
|||||||
_count: 100000, // FIXME: better than this, probably doesnt work anyway as max _count is 500 or something
|
_count: 100000, // FIXME: better than this, probably doesnt work anyway as max _count is 500 or something
|
||||||
type: "alphabeticalByArtist",
|
type: "alphabeticalByArtist",
|
||||||
};
|
};
|
||||||
const years = this.subsonic.getAlbumList2(this.credentials, q)
|
const years = this.subsonic
|
||||||
|
.getAlbumList2(this.credentials, q)
|
||||||
.then(({ results }) =>
|
.then(({ results }) =>
|
||||||
results.map((album) => album.year || "?")
|
results
|
||||||
|
.map((album) => album.year || "?")
|
||||||
.filter((item, i, ar) => ar.indexOf(item) === i)
|
.filter((item, i, ar) => ar.indexOf(item) === i)
|
||||||
.sort()
|
.sort()
|
||||||
.map((year) => ({
|
.map((year) => ({
|
||||||
...asYear(year)
|
...asYear(year),
|
||||||
}))
|
}))
|
||||||
.reverse()
|
.reverse()
|
||||||
);
|
);
|
||||||
return years;
|
return years;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { v4 as uuid } from "uuid";
|
||||||
import { Md5 } from "ts-md5";
|
import { Md5 } from "ts-md5";
|
||||||
import tmp from "tmp";
|
import tmp from "tmp";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
@@ -5,6 +6,16 @@ import path from "path";
|
|||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { option as O } from "fp-ts";
|
import { option as O } from "fp-ts";
|
||||||
|
|
||||||
|
import sharp from "sharp";
|
||||||
|
jest.mock("sharp");
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
jest.mock("axios");
|
||||||
|
|
||||||
|
import randomstring from "randomstring";
|
||||||
|
jest.mock("randomstring");
|
||||||
|
|
||||||
|
import { URLBuilder } from "../src/url_builder";
|
||||||
import {
|
import {
|
||||||
isValidImage,
|
isValidImage,
|
||||||
t,
|
t,
|
||||||
@@ -17,20 +28,13 @@ import {
|
|||||||
TranscodingCustomPlayers,
|
TranscodingCustomPlayers,
|
||||||
CustomPlayers,
|
CustomPlayers,
|
||||||
NO_CUSTOM_PLAYERS,
|
NO_CUSTOM_PLAYERS,
|
||||||
|
Subsonic,
|
||||||
} from "../src/subsonic";
|
} from "../src/subsonic";
|
||||||
|
|
||||||
import sharp from "sharp";
|
import { b64Encode } from "../src/b64";
|
||||||
jest.mock("sharp");
|
|
||||||
|
|
||||||
import {
|
import { Album, Artist, Track } from "../src/music_library";
|
||||||
Album,
|
import { anAlbum, aTrack } from "./builders";
|
||||||
Artist,
|
|
||||||
Track,
|
|
||||||
} from "../src/music_library";
|
|
||||||
import {
|
|
||||||
anAlbum,
|
|
||||||
aTrack,
|
|
||||||
} from "./builders";
|
|
||||||
import { BUrn } from "../src/burn";
|
import { BUrn } from "../src/burn";
|
||||||
|
|
||||||
describe("t", () => {
|
describe("t", () => {
|
||||||
@@ -61,22 +65,29 @@ describe("isValidImage", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("StreamClient(s)", () => {
|
describe("StreamClient(s)", () => {
|
||||||
describe("CustomStreamClientApplications", () => {
|
describe("CustomStreamClientApplications", () => {
|
||||||
const customClients = TranscodingCustomPlayers.from("audio/flac,audio/mp3>audio/ogg")
|
const customClients = TranscodingCustomPlayers.from(
|
||||||
|
"audio/flac,audio/mp3>audio/ogg"
|
||||||
|
);
|
||||||
|
|
||||||
describe("clientFor", () => {
|
describe("clientFor", () => {
|
||||||
describe("when there is a match", () => {
|
describe("when there is a match", () => {
|
||||||
it("should return the match", () => {
|
it("should return the match", () => {
|
||||||
expect(customClients.encodingFor({ mimeType: "audio/flac" })).toEqual(O.of({player: "bonob+audio/flac", mimeType:"audio/flac"}))
|
expect(customClients.encodingFor({ mimeType: "audio/flac" })).toEqual(
|
||||||
expect(customClients.encodingFor({ mimeType: "audio/mp3" })).toEqual(O.of({player: "bonob+audio/mp3", mimeType:"audio/ogg"}))
|
O.of({ player: "bonob+audio/flac", mimeType: "audio/flac" })
|
||||||
|
);
|
||||||
|
expect(customClients.encodingFor({ mimeType: "audio/mp3" })).toEqual(
|
||||||
|
O.of({ player: "bonob+audio/mp3", mimeType: "audio/ogg" })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there is no match", () => {
|
describe("when there is no match", () => {
|
||||||
it("should return undefined", () => {
|
it("should return undefined", () => {
|
||||||
expect(customClients.encodingFor({ mimeType: "audio/bob" })).toEqual(O.none)
|
expect(customClients.encodingFor({ mimeType: "audio/bob" })).toEqual(
|
||||||
|
O.none
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -197,12 +208,13 @@ describe("cachingImageFetcher", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const maybeIdFromCoverArtUrn = (coverArt: BUrn | undefined) => pipe(
|
const maybeIdFromCoverArtUrn = (coverArt: BUrn | undefined) =>
|
||||||
|
pipe(
|
||||||
coverArt,
|
coverArt,
|
||||||
O.fromNullable,
|
O.fromNullable,
|
||||||
O.map(it => it.resource.split(":")[1]),
|
O.map((it) => it.resource.split(":")[1]),
|
||||||
O.getOrElseW(() => "")
|
O.getOrElseW(() => "")
|
||||||
)
|
);
|
||||||
|
|
||||||
const asSongJson = (track: Track) => ({
|
const asSongJson = (track: Track) => ({
|
||||||
id: track.id,
|
id: track.id,
|
||||||
@@ -241,8 +253,14 @@ describe("artistURN", () => {
|
|||||||
describe("a valid external URL", () => {
|
describe("a valid external URL", () => {
|
||||||
it("should return an external URN", () => {
|
it("should return an external URN", () => {
|
||||||
expect(
|
expect(
|
||||||
artistImageURN({ artistId: "someArtistId", artistImageURL: "http://example.com/image.jpg" })
|
artistImageURN({
|
||||||
).toEqual({ system: "external", resource: "http://example.com/image.jpg" });
|
artistId: "someArtistId",
|
||||||
|
artistImageURL: "http://example.com/image.jpg",
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
system: "external",
|
||||||
|
resource: "http://example.com/image.jpg",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,7 +270,7 @@ describe("artistURN", () => {
|
|||||||
expect(
|
expect(
|
||||||
artistImageURN({
|
artistImageURN({
|
||||||
artistId: "someArtistId",
|
artistId: "someArtistId",
|
||||||
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
||||||
})
|
})
|
||||||
).toEqual({ system: "subsonic", resource: "art:someArtistId" });
|
).toEqual({ system: "subsonic", resource: "art:someArtistId" });
|
||||||
});
|
});
|
||||||
@@ -263,7 +281,7 @@ describe("artistURN", () => {
|
|||||||
expect(
|
expect(
|
||||||
artistImageURN({
|
artistImageURN({
|
||||||
artistId: "-1",
|
artistId: "-1",
|
||||||
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
||||||
})
|
})
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -274,7 +292,7 @@ describe("artistURN", () => {
|
|||||||
expect(
|
expect(
|
||||||
artistImageURN({
|
artistImageURN({
|
||||||
artistId: undefined,
|
artistId: undefined,
|
||||||
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`
|
artistImageURL: `http://example.com/${DODGY_IMAGE_NAME}`,
|
||||||
})
|
})
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -284,19 +302,28 @@ describe("artistURN", () => {
|
|||||||
describe("undefined", () => {
|
describe("undefined", () => {
|
||||||
describe("and artistId is valid", () => {
|
describe("and artistId is valid", () => {
|
||||||
it("should return artist art by artist id URN", () => {
|
it("should return artist art by artist id URN", () => {
|
||||||
expect(artistImageURN({ artistId: "someArtistId", artistImageURL: undefined })).toEqual({system:"subsonic", resource:"art:someArtistId"});
|
expect(
|
||||||
|
artistImageURN({
|
||||||
|
artistId: "someArtistId",
|
||||||
|
artistImageURL: undefined,
|
||||||
|
})
|
||||||
|
).toEqual({ system: "subsonic", resource: "art:someArtistId" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and artistId is -1", () => {
|
describe("and artistId is -1", () => {
|
||||||
it("should return error icon", () => {
|
it("should return error icon", () => {
|
||||||
expect(artistImageURN({ artistId: "-1", artistImageURL: undefined })).toBeUndefined();
|
expect(
|
||||||
|
artistImageURN({ artistId: "-1", artistImageURL: undefined })
|
||||||
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and artistId is undefined", () => {
|
describe("and artistId is undefined", () => {
|
||||||
it("should return error icon", () => {
|
it("should return error icon", () => {
|
||||||
expect(artistImageURN({ artistId: undefined, artistImageURL: undefined })).toBeUndefined();
|
expect(
|
||||||
|
artistImageURN({ artistId: undefined, artistImageURL: undefined })
|
||||||
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -311,10 +338,20 @@ describe("asTrack", () => {
|
|||||||
|
|
||||||
describe("when the song has no artistId", () => {
|
describe("when the song has no artistId", () => {
|
||||||
const album = anAlbum();
|
const album = anAlbum();
|
||||||
const track = aTrack({ artist: { id: undefined, name: "Not in library so no id", image: undefined }});
|
const track = aTrack({
|
||||||
|
artist: {
|
||||||
|
id: undefined,
|
||||||
|
name: "Not in library so no id",
|
||||||
|
image: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
it("should provide no artistId", () => {
|
it("should provide no artistId", () => {
|
||||||
const result = asTrack(album, { ...asSongJson(track) }, NO_CUSTOM_PLAYERS);
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{ ...asSongJson(track) },
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
expect(result.artist.id).toBeUndefined();
|
expect(result.artist.id).toBeUndefined();
|
||||||
expect(result.artist.name).toEqual("Not in library so no id");
|
expect(result.artist.name).toEqual("Not in library so no id");
|
||||||
expect(result.artist.image).toBeUndefined();
|
expect(result.artist.image).toBeUndefined();
|
||||||
@@ -325,7 +362,11 @@ describe("asTrack", () => {
|
|||||||
const album = anAlbum();
|
const album = anAlbum();
|
||||||
|
|
||||||
it("should provide a ? to sonos", () => {
|
it("should provide a ? to sonos", () => {
|
||||||
const result = asTrack(album, { id: '1' } as any as song, NO_CUSTOM_PLAYERS);
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{ id: "1" } as any as song,
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
expect(result.artist.id).toBeUndefined();
|
expect(result.artist.id).toBeUndefined();
|
||||||
expect(result.artist.name).toEqual("?");
|
expect(result.artist.name).toEqual("?");
|
||||||
expect(result.artist.image).toBeUndefined();
|
expect(result.artist.image).toBeUndefined();
|
||||||
@@ -338,14 +379,22 @@ describe("asTrack", () => {
|
|||||||
|
|
||||||
describe("a value greater than 5", () => {
|
describe("a value greater than 5", () => {
|
||||||
it("should be returned as 0", () => {
|
it("should be returned as 0", () => {
|
||||||
const result = asTrack(album, { ...asSongJson(track), userRating: 6 }, NO_CUSTOM_PLAYERS);
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{ ...asSongJson(track), userRating: 6 },
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
expect(result.rating.stars).toEqual(0);
|
expect(result.rating.stars).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("a value less than 0", () => {
|
describe("a value less than 0", () => {
|
||||||
it("should be returned as 0", () => {
|
it("should be returned as 0", () => {
|
||||||
const result = asTrack(album, { ...asSongJson(track), userRating: -1 }, NO_CUSTOM_PLAYERS);
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{ ...asSongJson(track), userRating: -1 },
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
expect(result.rating.stars).toEqual(0);
|
expect(result.rating.stars).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -358,82 +407,281 @@ describe("asTrack", () => {
|
|||||||
describe("when there are no custom players", () => {
|
describe("when there are no custom players", () => {
|
||||||
describe("when subsonic reports no transcodedContentType", () => {
|
describe("when subsonic reports no transcodedContentType", () => {
|
||||||
it("should use the default client and default contentType", () => {
|
it("should use the default client and default contentType", () => {
|
||||||
const result = asTrack(album, {
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{
|
||||||
...asSongJson(track),
|
...asSongJson(track),
|
||||||
contentType: "nonTranscodedContentType",
|
contentType: "nonTranscodedContentType",
|
||||||
transcodedContentType: undefined
|
transcodedContentType: undefined,
|
||||||
}, NO_CUSTOM_PLAYERS);
|
},
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.encoding).toEqual({ player: "bonob", mimeType: "nonTranscodedContentType" })
|
expect(result.encoding).toEqual({
|
||||||
|
player: "bonob",
|
||||||
|
mimeType: "nonTranscodedContentType",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when subsonic reports a transcodedContentType", () => {
|
describe("when subsonic reports a transcodedContentType", () => {
|
||||||
it("should use the default client and transcodedContentType", () => {
|
it("should use the default client and transcodedContentType", () => {
|
||||||
const result = asTrack(album, {
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{
|
||||||
...asSongJson(track),
|
...asSongJson(track),
|
||||||
contentType: "nonTranscodedContentType",
|
contentType: "nonTranscodedContentType",
|
||||||
transcodedContentType: "transcodedContentType"
|
transcodedContentType: "transcodedContentType",
|
||||||
}, NO_CUSTOM_PLAYERS);
|
},
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.encoding).toEqual({ player: "bonob", mimeType: "transcodedContentType" })
|
expect(result.encoding).toEqual({
|
||||||
|
player: "bonob",
|
||||||
|
mimeType: "transcodedContentType",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there are custom players registered", () => {
|
describe("when there are custom players registered", () => {
|
||||||
const streamClient = {
|
const streamClient = {
|
||||||
encodingFor: jest.fn()
|
encodingFor: jest.fn(),
|
||||||
}
|
};
|
||||||
|
|
||||||
describe("however no player is found for the default mimeType", () => {
|
describe("however no player is found for the default mimeType", () => {
|
||||||
describe("and there is no transcodedContentType", () => {
|
describe("and there is no transcodedContentType", () => {
|
||||||
it("should use the default player with the default content type", () => {
|
it("should use the default player with the default content type", () => {
|
||||||
streamClient.encodingFor.mockReturnValue(O.none)
|
streamClient.encodingFor.mockReturnValue(O.none);
|
||||||
|
|
||||||
const result = asTrack(album, {
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{
|
||||||
...asSongJson(track),
|
...asSongJson(track),
|
||||||
contentType: "nonTranscodedContentType",
|
contentType: "nonTranscodedContentType",
|
||||||
transcodedContentType: undefined
|
transcodedContentType: undefined,
|
||||||
}, streamClient as unknown as CustomPlayers);
|
},
|
||||||
|
streamClient as unknown as CustomPlayers
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.encoding).toEqual({ player: "bonob", mimeType: "nonTranscodedContentType" });
|
expect(result.encoding).toEqual({
|
||||||
expect(streamClient.encodingFor).toHaveBeenCalledWith({ mimeType: "nonTranscodedContentType" });
|
player: "bonob",
|
||||||
|
mimeType: "nonTranscodedContentType",
|
||||||
|
});
|
||||||
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
||||||
|
mimeType: "nonTranscodedContentType",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and there is a transcodedContentType", () => {
|
describe("and there is a transcodedContentType", () => {
|
||||||
it("should use the default player with the transcodedContentType", () => {
|
it("should use the default player with the transcodedContentType", () => {
|
||||||
streamClient.encodingFor.mockReturnValue(O.none)
|
streamClient.encodingFor.mockReturnValue(O.none);
|
||||||
|
|
||||||
const result = asTrack(album, {
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{
|
||||||
...asSongJson(track),
|
...asSongJson(track),
|
||||||
contentType: "nonTranscodedContentType",
|
contentType: "nonTranscodedContentType",
|
||||||
transcodedContentType: "transcodedContentType1"
|
transcodedContentType: "transcodedContentType1",
|
||||||
}, streamClient as unknown as CustomPlayers);
|
},
|
||||||
|
streamClient as unknown as CustomPlayers
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.encoding).toEqual({ player: "bonob", mimeType: "transcodedContentType1" });
|
expect(result.encoding).toEqual({
|
||||||
expect(streamClient.encodingFor).toHaveBeenCalledWith({ mimeType: "nonTranscodedContentType" });
|
player: "bonob",
|
||||||
|
mimeType: "transcodedContentType1",
|
||||||
|
});
|
||||||
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
||||||
|
mimeType: "nonTranscodedContentType",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("there is a player with the matching content type", () => {
|
describe("there is a player with the matching content type", () => {
|
||||||
it("should use it", () => {
|
it("should use it", () => {
|
||||||
const customEncoding = { player: "custom-player", mimeType: "audio/some-mime-type" };
|
const customEncoding = {
|
||||||
|
player: "custom-player",
|
||||||
|
mimeType: "audio/some-mime-type",
|
||||||
|
};
|
||||||
streamClient.encodingFor.mockReturnValue(O.of(customEncoding));
|
streamClient.encodingFor.mockReturnValue(O.of(customEncoding));
|
||||||
|
|
||||||
const result = asTrack(album, {
|
const result = asTrack(
|
||||||
|
album,
|
||||||
|
{
|
||||||
...asSongJson(track),
|
...asSongJson(track),
|
||||||
contentType: "sourced-from/subsonic",
|
contentType: "sourced-from/subsonic",
|
||||||
transcodedContentType: "sourced-from/subsonic2"
|
transcodedContentType: "sourced-from/subsonic2",
|
||||||
}, streamClient as unknown as CustomPlayers);
|
},
|
||||||
|
streamClient as unknown as CustomPlayers
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.encoding).toEqual(customEncoding);
|
expect(result.encoding).toEqual(customEncoding);
|
||||||
expect(streamClient.encodingFor).toHaveBeenCalledWith({ mimeType: "sourced-from/subsonic" });
|
expect(streamClient.encodingFor).toHaveBeenCalledWith({
|
||||||
|
mimeType: "sourced-from/subsonic",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const subsonicOK = (body: any = {}) => ({
|
||||||
|
"subsonic-response": {
|
||||||
|
status: "ok",
|
||||||
|
version: "1.16.1",
|
||||||
|
type: "subsonic",
|
||||||
|
serverVersion: "0.45.1 (c55e6590)",
|
||||||
|
...body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const asGenreJson = (genre: { name: string; albumCount: number }) => ({
|
||||||
|
songCount: 1475,
|
||||||
|
albumCount: genre.albumCount,
|
||||||
|
value: genre.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGenresJson = (genres: { name: string; albumCount: number }[]) =>
|
||||||
|
subsonicOK({
|
||||||
|
genres: {
|
||||||
|
genre: genres.map(asGenreJson),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = (data: string | object) => ({
|
||||||
|
status: 200,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("subsonic", () => {
|
||||||
|
const url = new URLBuilder("http://127.0.0.22:4567/some-context-path");
|
||||||
|
const customPlayers = {
|
||||||
|
encodingFor: jest.fn(),
|
||||||
|
};
|
||||||
|
const username = `user1-${uuid()}`;
|
||||||
|
const password = `pass1-${uuid()}`;
|
||||||
|
const credentials = { username, password };
|
||||||
|
const subsonic = new Subsonic(url, customPlayers);
|
||||||
|
|
||||||
|
const mockRandomstring = jest.fn();
|
||||||
|
const mockGET = jest.fn();
|
||||||
|
const mockPOST = jest.fn();
|
||||||
|
|
||||||
|
const salt = "saltysalty";
|
||||||
|
|
||||||
|
const authParams = {
|
||||||
|
u: username,
|
||||||
|
v: "1.16.1",
|
||||||
|
c: "bonob",
|
||||||
|
t: t(password, salt),
|
||||||
|
s: salt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authParamsPlusJson = {
|
||||||
|
...authParams,
|
||||||
|
f: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "bonob",
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
randomstring.generate = mockRandomstring;
|
||||||
|
axios.get = mockGET;
|
||||||
|
axios.post = mockPOST;
|
||||||
|
|
||||||
|
mockRandomstring.mockReturnValue(salt);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getting genres", () => {
|
||||||
|
describe("when there are none", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(getGenresJson([])))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array", async () => {
|
||||||
|
const result = await subsonic.getGenres(credentials);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
|
url.append({ pathname: "/rest/getGenres" }).href(),
|
||||||
|
{
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is only 1 that has an albumCount > 0", () => {
|
||||||
|
const genres = [
|
||||||
|
{ name: "genre1", albumCount: 1 },
|
||||||
|
{ name: "genreWithNoAlbums", albumCount: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(getGenresJson(genres)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return them alphabetically sorted", async () => {
|
||||||
|
const result = await subsonic.getGenres(credentials);
|
||||||
|
|
||||||
|
expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]);
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(
|
||||||
|
url.append({ pathname: "/rest/getGenres" }).href(),
|
||||||
|
{
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are many that have an albumCount > 0", () => {
|
||||||
|
const genres = [
|
||||||
|
{ name: "g1", albumCount: 1 },
|
||||||
|
{ name: "g2", albumCount: 1 },
|
||||||
|
{ name: "g3", albumCount: 1 },
|
||||||
|
{ name: "g4", albumCount: 1 },
|
||||||
|
{ name: "someGenreWithNoAlbums", albumCount: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(getGenresJson(genres)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return them alphabetically sorted", async () => {
|
||||||
|
const result = await subsonic.getGenres(credentials);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ 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.append({ pathname: "/rest/getGenres" }).href(),
|
||||||
|
{
|
||||||
|
params: asURLSearchParams(authParamsPlusJson),
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user