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:
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user