mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to play radio stations from subsonic api (#199)
This commit is contained in:
@@ -20,8 +20,9 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
"esbenp.prettier-vscode",
|
||||
"redhat.vscode-xml"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type KEY =
|
||||
| "AppLinkMessage"
|
||||
| "artists"
|
||||
| "albums"
|
||||
| "internetRadio"
|
||||
| "playlists"
|
||||
| "genres"
|
||||
| "random"
|
||||
@@ -51,6 +52,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
AppLinkMessage: "Linking sonos with $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artists",
|
||||
albums: "Albums",
|
||||
internetRadio: "Internet Radio",
|
||||
tracks: "Tracks",
|
||||
playlists: "Playlists",
|
||||
genres: "Genres",
|
||||
@@ -92,6 +94,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
AppLinkMessage: "Forbinder Sonos med $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Kunstnere",
|
||||
albums: "Album",
|
||||
internetRadio: "Internet Radio",
|
||||
tracks: "Numre",
|
||||
playlists: "Afspilningslister",
|
||||
genres: "Genre",
|
||||
@@ -133,6 +136,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
AppLinkMessage: "Associer Sonos à $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artistes",
|
||||
albums: "Albums",
|
||||
internetRadio: "Radio Internet",
|
||||
tracks: "Pistes",
|
||||
playlists: "Playlists",
|
||||
genres: "Genres",
|
||||
@@ -174,6 +178,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
|
||||
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
|
||||
artists: "Artiesten",
|
||||
albums: "Albums",
|
||||
internetRadio: "Internet Radio",
|
||||
tracks: "Nummers",
|
||||
playlists: "Afspeellijsten",
|
||||
genres: "Genres",
|
||||
|
||||
@@ -163,6 +163,7 @@ export const HOLI_COLORS = [
|
||||
export type ICON =
|
||||
| "artists"
|
||||
| "albums"
|
||||
| "radio"
|
||||
| "playlists"
|
||||
| "genres"
|
||||
| "random"
|
||||
@@ -240,6 +241,7 @@ const iconFrom = (name: string) =>
|
||||
export const ICONS: Record<ICON, SvgIcon> = {
|
||||
artists: iconFrom("navidrome-artists.svg"),
|
||||
albums: iconFrom("navidrome-all.svg"),
|
||||
radio: iconFrom("navidrome-radio.svg"),
|
||||
blank: iconFrom("blank.svg"),
|
||||
playlists: iconFrom("navidrome-playlists.svg"),
|
||||
genres: iconFrom("Theatre-Mask-111172.svg"),
|
||||
|
||||
@@ -69,6 +69,13 @@ export type Track = {
|
||||
rating: Rating;
|
||||
};
|
||||
|
||||
export type RadioStation = {
|
||||
id: string,
|
||||
name: string,
|
||||
url: string,
|
||||
homePage?: string
|
||||
}
|
||||
|
||||
export type Paging = {
|
||||
_index: number;
|
||||
_count: number;
|
||||
@@ -188,4 +195,6 @@ export interface MusicLibrary {
|
||||
removeFromPlaylist(playlistId: string, indicies: number[]): Promise<boolean>
|
||||
similarSongs(id: string): Promise<Track[]>;
|
||||
topSongs(artistId: string): Promise<Track[]>;
|
||||
radioStation(id: string): Promise<RadioStation>
|
||||
radioStations(): Promise<RadioStation[]>
|
||||
}
|
||||
|
||||
116
src/smapi.ts
116
src/smapi.ts
@@ -17,6 +17,7 @@ import {
|
||||
Genre,
|
||||
MusicService,
|
||||
Playlist,
|
||||
RadioStation,
|
||||
Rating,
|
||||
slice2,
|
||||
Track,
|
||||
@@ -299,6 +300,13 @@ export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
|
||||
// canAddToFavorites: true
|
||||
});
|
||||
|
||||
export const internetRadioStation = (station: RadioStation) => ({
|
||||
itemType: "stream",
|
||||
id: `internetRadioStation:${station.id}`,
|
||||
title: station.name,
|
||||
mimeType: "audio/mpeg",
|
||||
});
|
||||
|
||||
export const track = (bonobUrl: URLBuilder, track: Track) => ({
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
@@ -426,9 +434,7 @@ function bindSmapiSoapServiceToExpress(
|
||||
},
|
||||
},
|
||||
})),
|
||||
TE.getOrElse(() =>
|
||||
T.of(SMAPI_FAULT_LOGIN_UNAUTHORIZED)
|
||||
)
|
||||
TE.getOrElse(() => T.of(SMAPI_FAULT_LOGIN_UNAUTHORIZED))
|
||||
)();
|
||||
} else {
|
||||
throw authOrFail.toSmapiFault();
|
||||
@@ -487,27 +493,38 @@ function bindSmapiSoapServiceToExpress(
|
||||
) =>
|
||||
login(soapyHeaders?.credentials)
|
||||
.then(splitId(id))
|
||||
.then(({ credentials, type, typeId }) => ({
|
||||
getMediaURIResult: bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/${type}/${typeId}`,
|
||||
})
|
||||
.href(),
|
||||
httpHeaders: [
|
||||
{
|
||||
httpHeader: {
|
||||
header: "bnbt",
|
||||
value: credentials.loginToken.token,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpHeader: {
|
||||
header: "bnbk",
|
||||
value: credentials.loginToken.key,
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
.then(({ musicLibrary, credentials, type, typeId }) => {
|
||||
switch (type) {
|
||||
case "internetRadioStation":
|
||||
return musicLibrary.radioStation(typeId).then((it) => ({
|
||||
getMediaURIResult: it.url,
|
||||
}));
|
||||
case "track":
|
||||
return {
|
||||
getMediaURIResult: bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/${type}/${typeId}`,
|
||||
})
|
||||
.href(),
|
||||
httpHeaders: [
|
||||
{
|
||||
httpHeader: {
|
||||
header: "bnbt",
|
||||
value: credentials.loginToken.token,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpHeader: {
|
||||
header: "bnbk",
|
||||
value: credentials.loginToken.key,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw `Unsupported type:${type}`;
|
||||
}
|
||||
}),
|
||||
getMediaMetadata: async (
|
||||
{ id }: { id: string },
|
||||
_,
|
||||
@@ -515,11 +532,20 @@ function bindSmapiSoapServiceToExpress(
|
||||
) =>
|
||||
login(soapyHeaders?.credentials)
|
||||
.then(splitId(id))
|
||||
.then(async ({ musicLibrary, apiKey, typeId }) =>
|
||||
musicLibrary.track(typeId!).then((it) => ({
|
||||
getMediaMetadataResult: track(urlWithToken(apiKey), it),
|
||||
}))
|
||||
),
|
||||
.then(async ({ musicLibrary, apiKey, type, typeId }) => {
|
||||
switch (type) {
|
||||
case "internetRadioStation":
|
||||
return musicLibrary.radioStation(typeId).then((it) => ({
|
||||
getMediaMetadataResult: internetRadioStation(it),
|
||||
}));
|
||||
case "track":
|
||||
return musicLibrary.track(typeId!).then((it) => ({
|
||||
getMediaMetadataResult: track(urlWithToken(apiKey), it),
|
||||
}));
|
||||
default:
|
||||
throw `Unsupported type:${type}`;
|
||||
}
|
||||
}),
|
||||
search: async (
|
||||
{ id, term }: { id: string; term: string },
|
||||
_,
|
||||
@@ -741,6 +767,12 @@ function bindSmapiSoapServiceToExpress(
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "internetRadio",
|
||||
title: lang("internetRadio"),
|
||||
albumArtURI: iconArtURI(bonobUrl, "radio").href(),
|
||||
itemType: "stream",
|
||||
},
|
||||
],
|
||||
});
|
||||
case "search":
|
||||
@@ -815,6 +847,19 @@ function bindSmapiSoapServiceToExpress(
|
||||
type: "mostPlayed",
|
||||
...paging,
|
||||
});
|
||||
case "internetRadio":
|
||||
return musicLibrary
|
||||
.radioStations()
|
||||
.then(slice2(paging))
|
||||
.then(([page, total]) =>
|
||||
getMetadataResult({
|
||||
mediaMetadata: page.map((it) =>
|
||||
internetRadioStation(it)
|
||||
),
|
||||
index: paging._index,
|
||||
total,
|
||||
})
|
||||
);
|
||||
case "genres":
|
||||
return musicLibrary
|
||||
.genres()
|
||||
@@ -840,10 +885,9 @@ function bindSmapiSoapServiceToExpress(
|
||||
name: playlist.name,
|
||||
coverArt: playlist.coverArt,
|
||||
// todo: are these every important?
|
||||
entries: []
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(slice2(paging))
|
||||
@@ -875,15 +919,15 @@ function bindSmapiSoapServiceToExpress(
|
||||
.artist(typeId!)
|
||||
.then((artist) => artist.albums)
|
||||
.then(slice2(paging))
|
||||
.then(([page, total]) => {
|
||||
return getMetadataResult({
|
||||
.then(([page, total]) =>
|
||||
getMetadataResult({
|
||||
mediaCollection: page.map((it) =>
|
||||
album(urlWithToken(apiKey), it)
|
||||
),
|
||||
index: paging._index,
|
||||
total,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
case "relatedArtists":
|
||||
return musicLibrary
|
||||
.artist(typeId!)
|
||||
|
||||
@@ -205,6 +205,15 @@ type GetTopSongsResponse = {
|
||||
topSongs: { song: song[] };
|
||||
};
|
||||
|
||||
type GetInternetRadioStationsResponse = {
|
||||
internetRadioStations: { internetRadioStation: {
|
||||
id: string,
|
||||
name: string,
|
||||
streamUrl: string,
|
||||
homePageUrl?: string }[]
|
||||
}
|
||||
}
|
||||
|
||||
type GetSongResponse = {
|
||||
song: song;
|
||||
};
|
||||
@@ -1011,6 +1020,24 @@ export class Subsonic implements MusicService {
|
||||
)
|
||||
)
|
||||
),
|
||||
radioStations: async () => subsonic
|
||||
.getJSON<GetInternetRadioStationsResponse>(
|
||||
credentials,
|
||||
"/rest/getInternetRadioStations"
|
||||
)
|
||||
.then((it) => it.internetRadioStations.internetRadioStation || [])
|
||||
.then((stations) => stations.map((it) => ({
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
url: it.streamUrl,
|
||||
homePage: it.homePageUrl
|
||||
}))),
|
||||
radioStation: async (id: string) => genericSubsonic
|
||||
.radioStations()
|
||||
.then(it =>
|
||||
it.find(station => station.id === id)!
|
||||
),
|
||||
|
||||
};
|
||||
|
||||
if (credentials.type == "navidrome") {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Playlist,
|
||||
SimilarArtist,
|
||||
AlbumSummary,
|
||||
RadioStation,
|
||||
} from "../src/music_service";
|
||||
|
||||
import { b64Encode } from "../src/b64";
|
||||
@@ -204,6 +205,17 @@ export function anAlbum(fields: Partial<Album> = {}): Album {
|
||||
};
|
||||
};
|
||||
|
||||
export function aRadioStation(fields: Partial<RadioStation> = {}): RadioStation {
|
||||
const id = uuid()
|
||||
const name = `Station-${id}`;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
url: `http://example.com/${name}`,
|
||||
...fields
|
||||
}
|
||||
}
|
||||
|
||||
export function anAlbumSummary(fields: Partial<AlbumSummary> = {}): AlbumSummary {
|
||||
const id = uuid();
|
||||
return {
|
||||
|
||||
@@ -161,6 +161,8 @@ export class InMemoryMusicService implements MusicService {
|
||||
Promise.reject("Unsupported operation"),
|
||||
similarSongs: async (_: string) => Promise.resolve([]),
|
||||
topSongs: async (_: string) => Promise.resolve([]),
|
||||
radioStations: async () => Promise.resolve([]),
|
||||
radioStation: async (_: string) => Promise.reject("Unsupported operation"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
sonosifyMimeType,
|
||||
ratingAsInt,
|
||||
ratingFromInt,
|
||||
internetRadioStation
|
||||
} from "../src/smapi";
|
||||
|
||||
import { keys as i8nKeys } from "../src/i8n";
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
TRIP_HOP,
|
||||
PUNK,
|
||||
aPlaylist,
|
||||
aRadioStation,
|
||||
} from "./builders";
|
||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||
import supersoap from "./supersoap";
|
||||
@@ -481,6 +483,18 @@ describe("album", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("internetRadioStation", () => {
|
||||
it("should map to a sonos internet stream", () => {
|
||||
const station = aRadioStation()
|
||||
expect(internetRadioStation(station)).toEqual({
|
||||
itemType: "stream",
|
||||
id: `internetRadioStation:${station.id}`,
|
||||
title: station.name,
|
||||
mimeType: "audio/mpeg"
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("sonosifyMimeType", () => {
|
||||
describe("when is audio/x-flac", () => {
|
||||
it("should be mapped to audio/flac", () => {
|
||||
@@ -577,6 +591,8 @@ describe("wsdl api", () => {
|
||||
scrobble: jest.fn(),
|
||||
nowPlaying: jest.fn(),
|
||||
rate: jest.fn(),
|
||||
radioStation: jest.fn(),
|
||||
radioStations: jest.fn(),
|
||||
};
|
||||
const apiTokens = {
|
||||
mint: jest.fn(),
|
||||
@@ -1158,6 +1174,12 @@ describe("wsdl api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "mostPlayed").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "internetRadio",
|
||||
title: "Internet Radio",
|
||||
albumArtURI: iconArtURI(bonobUrl, "radio").href(),
|
||||
itemType: "stream",
|
||||
},
|
||||
];
|
||||
expect(root[0]).toEqual(
|
||||
getMetadataResult({
|
||||
@@ -1246,6 +1268,12 @@ describe("wsdl api", () => {
|
||||
albumArtURI: iconArtURI(bonobUrl, "mostPlayed").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
id: "internetRadio",
|
||||
title: "Internet Radio",
|
||||
albumArtURI: iconArtURI(bonobUrl, "radio").href(),
|
||||
itemType: "stream",
|
||||
},
|
||||
];
|
||||
expect(root[0]).toEqual(
|
||||
getMetadataResult({
|
||||
@@ -2375,6 +2403,71 @@ describe("wsdl api", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for internet radio stations", () => {
|
||||
const station1 = aRadioStation();
|
||||
const station2 = aRadioStation();
|
||||
const station3 = aRadioStation();
|
||||
const station4 = aRadioStation();
|
||||
|
||||
const stations = [station1, station2, station3, station4];
|
||||
|
||||
beforeEach(() => {
|
||||
musicLibrary.radioStations.mockResolvedValue(stations);
|
||||
});
|
||||
|
||||
describe("when they all fit on the page", () => {
|
||||
it("should return them all", async () => {
|
||||
const paging = {
|
||||
index: 0,
|
||||
count: 100,
|
||||
};
|
||||
|
||||
const result = await ws.getMetadataAsync({
|
||||
id: `internetRadio`,
|
||||
...paging,
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual(
|
||||
getMetadataResult({
|
||||
mediaMetadata: stations.map((it) =>
|
||||
internetRadioStation(it)
|
||||
),
|
||||
index: 0,
|
||||
total: stations.length,
|
||||
})
|
||||
);
|
||||
expect(musicLibrary.radioStations).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for a single page of stations", () => {
|
||||
const pageOfStations = [station3, station4];
|
||||
|
||||
it("should return only that page", async () => {
|
||||
const paging = {
|
||||
index: 2,
|
||||
count: 2,
|
||||
};
|
||||
|
||||
const result = await ws.getMetadataAsync({
|
||||
id: `internetRadio`,
|
||||
...paging,
|
||||
});
|
||||
|
||||
expect(result[0]).toEqual(
|
||||
getMetadataResult({
|
||||
mediaMetadata: pageOfStations.map((it) =>
|
||||
internetRadioStation(it)
|
||||
),
|
||||
index: paging.index,
|
||||
total: stations.length,
|
||||
})
|
||||
);
|
||||
expect(musicLibrary.radioStations).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2752,6 +2845,27 @@ describe("wsdl api", () => {
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for a URI to stream a radio station", () => {
|
||||
const someStation = aRadioStation()
|
||||
|
||||
beforeEach(() => {
|
||||
musicLibrary.radioStation.mockResolvedValue(someStation);
|
||||
})
|
||||
|
||||
it("should return the radio stations uri", async () => {
|
||||
const root = await ws.getMediaURIAsync({
|
||||
id: `internetRadioStation:${someStation.id}`,
|
||||
});
|
||||
|
||||
expect(root[0]).toEqual({
|
||||
getMediaURIResult: someStation.url,
|
||||
});
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.radioStation).toHaveBeenCalledWith(someStation.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2763,7 +2877,6 @@ describe("wsdl api", () => {
|
||||
describe("when valid credentials are provided", () => {
|
||||
let ws: Client;
|
||||
|
||||
const someTrack = aTrack();
|
||||
|
||||
beforeEach(async () => {
|
||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
@@ -2771,10 +2884,15 @@ describe("wsdl api", () => {
|
||||
httpClient: supersoap(server),
|
||||
});
|
||||
setupAuthenticatedRequest(ws);
|
||||
musicLibrary.track.mockResolvedValue(someTrack);
|
||||
});
|
||||
|
||||
describe("asking for media metadata for a track", () => {
|
||||
const someTrack = aTrack();
|
||||
|
||||
beforeEach(async () => {
|
||||
musicLibrary.track.mockResolvedValue(someTrack);
|
||||
});
|
||||
|
||||
it("should return it with auth header", async () => {
|
||||
const root = await ws.getMediaMetadataAsync({
|
||||
id: `track:${someTrack.id}`,
|
||||
@@ -2793,6 +2911,27 @@ describe("wsdl api", () => {
|
||||
expect(musicLibrary.track).toHaveBeenCalledWith(someTrack.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for media metadata for an internet radio station", () => {
|
||||
const someStation = aRadioStation()
|
||||
|
||||
beforeEach(() => {
|
||||
musicLibrary.radioStation.mockResolvedValue(someStation);
|
||||
})
|
||||
|
||||
it("should return it with no auth header", async () => {
|
||||
const root = await ws.getMediaMetadataAsync({
|
||||
id: `internetRadioStation:${someStation.id}`,
|
||||
});
|
||||
|
||||
expect(root[0]).toEqual({
|
||||
getMediaMetadataResult: internetRadioStation(someStation),
|
||||
});
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(apiTokens.mint).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.radioStation).toHaveBeenCalledWith(someStation.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ import {
|
||||
SimilarArtist,
|
||||
Rating,
|
||||
Credentials,
|
||||
AuthFailure
|
||||
AuthFailure,
|
||||
RadioStation
|
||||
} from "../src/music_service";
|
||||
import {
|
||||
aGenre,
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
aTrack,
|
||||
POP,
|
||||
ROCK,
|
||||
aRadioStation
|
||||
} from "./builders";
|
||||
import { b64Encode } from "../src/b64";
|
||||
import { BUrn } from "../src/burn";
|
||||
@@ -348,6 +350,18 @@ const getArtistJson = (artist: Artist, extras: ArtistExtras = { artistImageUrl:
|
||||
artist: asArtistJson(artist, extras),
|
||||
});
|
||||
|
||||
const getRadioStationsJson = (radioStations: RadioStation[]) =>
|
||||
subsonicOK({
|
||||
internetRadioStations: {
|
||||
internetRadioStation: radioStations.map((it) => ({
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
streamUrl: it.url,
|
||||
homePageUrl: it.homePage
|
||||
}))
|
||||
},
|
||||
});
|
||||
|
||||
const asGenreJson = (genre: { name: string; albumCount: number }) => ({
|
||||
songCount: 1475,
|
||||
albumCount: genre.albumCount,
|
||||
@@ -5028,4 +5042,86 @@ describe("Subsonic", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("radioStations", () => {
|
||||
beforeEach(() => {
|
||||
customPlayers.encodingFor.mockReturnValue(O.none);
|
||||
});
|
||||
|
||||
describe("when there some radio stations", () => {
|
||||
const station1 = aRadioStation();
|
||||
const station2 = aRadioStation();
|
||||
const station3 = aRadioStation();
|
||||
|
||||
beforeEach(() => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getRadioStationsJson([
|
||||
station1,
|
||||
station2,
|
||||
station3,
|
||||
])))
|
||||
);
|
||||
});
|
||||
|
||||
describe("asking for all of them", () => {
|
||||
it("should return them all", async () => {
|
||||
const result = await login({ username, password })
|
||||
.then((it) => it.radioStations());
|
||||
|
||||
expect(result).toEqual([station1, station2, station3]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getInternetRadioStations' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json"
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("asking for one of them", () => {
|
||||
it("should return it", async () => {
|
||||
const result = await login({ username, password })
|
||||
.then((it) => it.radioStation(station2.id));
|
||||
|
||||
expect(result).toEqual(station2);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getInternetRadioStations' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json"
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are no radio stations", () => {
|
||||
it("should return []", async () => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getRadioStationsJson([])))
|
||||
);
|
||||
|
||||
const result = await login({ username, password })
|
||||
.then((it) => it.radioStations());
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
expect(mockGET).toHaveBeenCalledWith(url.append({ pathname: '/rest/getInternetRadioStations' }).href(), {
|
||||
params: asURLSearchParams({
|
||||
...authParams,
|
||||
f: "json"
|
||||
}),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
4
web/icons/navidrome-radio.svg
Normal file
4
web/icons/navidrome-radio.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20 6H8.3l8.26-3.34L15.88 1 3.24 6.15C2.51 6.43 2 7.17 2 8v12c0 1.1.89 2 2 2h16c1.11 0 2-.9 2-2V8c0-1.11-.89-2-2-2zm0 2v3h-2V9h-2v2H4V8h16zM4 20v-7h16v7H4z"></path>
|
||||
<circle cx="8" cy="16.48" r="2.5"></circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
Reference in New Issue
Block a user