Add internet radio (#1384)

This commit is contained in:
Jeff
2025-12-13 21:26:33 -08:00
committed by GitHub
parent f61d34c340
commit 7ed847fecb
46 changed files with 2229 additions and 118 deletions
+55
View File
@@ -100,6 +100,20 @@ export const controller: GeneralController = {
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
createInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: createInternetRadioStation`,
);
}
return apiController(
'createInternetRadioStation',
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
createPlaylist(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -128,6 +142,20 @@ export const controller: GeneralController = {
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
deleteInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStation`,
);
}
return apiController(
'deleteInternetRadioStation',
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
deletePlaylist(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -342,6 +370,19 @@ export const controller: GeneralController = {
query: mergeMusicFolderId(args.query, server),
});
},
getInternetRadioStations(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getInternetRadioStations`,
);
}
return apiController(
'getInternetRadioStations',
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
getLyrics(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -744,6 +785,20 @@ export const controller: GeneralController = {
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
updateInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: updateInternetRadioStation`,
);
}
return apiController(
'updateInternetRadioStation',
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
},
updatePlaylist(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -5,6 +5,7 @@ import orderBy from 'lodash/orderBy';
import { z } from 'zod';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { useRadioStore } from '/@/renderer/features/radio/store/radio-store';
import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize';
import { JFSongListSort, JFSortOrder, jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/api/utils';
@@ -115,6 +116,26 @@ export const JellyfinController: InternalControllerEndpoint = {
return null;
},
createInternetRadioStation: async (args) => {
const { apiClientProps, body } = args;
if (!apiClientProps.serverId) {
throw new Error('No serverId found');
}
const state = useRadioStore.getState();
if (!state?.actions?.createStation) {
throw new Error('Radio store not initialized');
}
state.actions.createStation(apiClientProps.serverId, {
homepageUrl: body.homepageUrl || null,
name: body.name,
streamUrl: body.streamUrl,
});
return null;
},
createPlaylist: async (args) => {
const { apiClientProps, body } = args;
@@ -158,6 +179,22 @@ export const JellyfinController: InternalControllerEndpoint = {
return null;
},
deleteInternetRadioStation: async (args) => {
const { apiClientProps, query } = args;
if (!apiClientProps.serverId) {
throw new Error('No serverId found');
}
const state = useRadioStore.getState();
if (!state?.actions?.deleteStation) {
throw new Error('Radio store not initialized');
}
state.actions.deleteStation(apiClientProps.serverId, query.id);
return null;
},
deletePlaylist: async (args) => {
const { apiClientProps, query } = args;
@@ -633,6 +670,20 @@ export const JellyfinController: InternalControllerEndpoint = {
totalRecordCount: res.body?.TotalRecordCount || 0,
};
},
getInternetRadioStations: async (args) => {
const { apiClientProps } = args;
if (!apiClientProps.serverId) {
throw new Error('No serverId found');
}
const state = useRadioStore.getState();
if (!state?.actions?.getStations) {
throw new Error('Radio store not initialized');
}
return state.actions.getStations(apiClientProps.serverId);
},
getLyrics: async (args) => {
const { apiClientProps, query } = args;
@@ -1455,6 +1506,26 @@ export const JellyfinController: InternalControllerEndpoint = {
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server)),
};
},
updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args;
if (!apiClientProps.serverId) {
throw new Error('No serverId found');
}
const state = useRadioStore.getState();
if (!state?.actions?.updateStation) {
throw new Error('Radio store not initialized');
}
state.actions.updateStation(apiClientProps.serverId, query.id, {
homepageUrl: body.homepageUrl || null,
name: body.name,
streamUrl: body.streamUrl,
});
return null;
},
updatePlaylist: async (args) => {
const { apiClientProps, body, query } = args;
@@ -141,6 +141,7 @@ export const NavidromeController: InternalControllerEndpoint = {
};
},
createFavorite: SubsonicController.createFavorite,
createInternetRadioStation: SubsonicController.createInternetRadioStation,
createPlaylist: async (args) => {
const { apiClientProps, body } = args;
@@ -164,6 +165,7 @@ export const NavidromeController: InternalControllerEndpoint = {
};
},
deleteFavorite: SubsonicController.deleteFavorite,
deleteInternetRadioStation: SubsonicController.deleteInternetRadioStation,
deletePlaylist: async (args) => {
const { apiClientProps, query } = args;
@@ -459,6 +461,7 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getInternetRadioStations: SubsonicController.getInternetRadioStations,
getLyrics: SubsonicController.getLyrics,
getMusicFolderList: SubsonicController.getMusicFolderList,
getPlaylistDetail: async (args) => {
@@ -931,6 +934,7 @@ export const NavidromeController: InternalControllerEndpoint = {
id: res.body.data.id,
};
},
updateInternetRadioStation: SubsonicController.updateInternetRadioStation,
updatePlaylist: async (args) => {
const { apiClientProps, body, query } = args;
+4
View File
@@ -322,6 +322,10 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'songList'] as const;
},
},
radio: {
list: (serverId: string) => [serverId, 'radio', 'list'] as const,
root: (serverId: string) => [serverId, 'radio'] as const,
},
roles: {
list: (serverId: string) => [serverId, 'roles'] as const,
},
+31
View File
@@ -30,6 +30,14 @@ export const contract = c.router({
200: ssType._response.createFavorite,
},
},
createInternetRadioStation: {
method: 'GET',
path: 'createInternetRadioStation.view',
query: ssType._parameters.createInternetRadioStation,
responses: {
200: ssType._response.createInternetRadioStation,
},
},
createPlaylist: {
method: 'GET',
path: 'createPlaylist.view',
@@ -38,6 +46,14 @@ export const contract = c.router({
200: ssType._response.createPlaylist,
},
},
deleteInternetRadioStation: {
method: 'GET',
path: 'deleteInternetRadioStation.view',
query: ssType._parameters.deleteInternetRadioStation,
responses: {
200: ssType._response.deleteInternetRadioStation,
},
},
deletePlaylist: {
method: 'GET',
path: 'deletePlaylist.view',
@@ -110,6 +126,13 @@ export const contract = c.router({
200: ssType._response.getIndexes,
},
},
getInternetRadioStations: {
method: 'GET',
path: 'getInternetRadioStations.view',
responses: {
200: ssType._response.getInternetRadioStations,
},
},
getMusicDirectory: {
method: 'GET',
path: 'getMusicDirectory.view',
@@ -281,6 +304,14 @@ export const contract = c.router({
200: ssType._response.setRating,
},
},
updateInternetRadioStation: {
method: 'GET',
path: 'updateInternetRadioStation.view',
query: ssType._parameters.updateInternetRadioStation,
responses: {
200: ssType._response.updateInternetRadioStation,
},
},
updatePlaylist: {
method: 'GET',
path: 'updatePlaylist.view',
@@ -166,6 +166,23 @@ export const SubsonicController: InternalControllerEndpoint = {
return null;
},
createInternetRadioStation: async (args) => {
const { apiClientProps, body } = args;
const res = await ssApiClient(apiClientProps).createInternetRadioStation({
query: {
homepageUrl: body.homepageUrl,
name: body.name,
streamUrl: body.streamUrl,
},
});
if (res.status !== 200) {
throw new Error('Failed to create internet radio station');
}
return null;
},
createPlaylist: async ({ apiClientProps, body }) => {
const res = await ssApiClient(apiClientProps).createPlaylist({
query: {
@@ -199,6 +216,21 @@ export const SubsonicController: InternalControllerEndpoint = {
return null;
},
deleteInternetRadioStation: async (args) => {
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).deleteInternetRadioStation({
query: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to delete internet radio station');
}
return null;
},
deletePlaylist: async (args) => {
const { apiClientProps, query } = args;
@@ -789,6 +821,19 @@ export const SubsonicController: InternalControllerEndpoint = {
startIndex: query.startIndex,
});
},
getInternetRadioStations: async (args) => {
const { apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getInternetRadioStations();
if (res.status !== 200) {
throw new Error('Failed to get internet radio stations');
}
const stations = res.body.internetRadioStations?.internetRadioStation || [];
return stations.map((station) => ssNormalize.internetRadioStation(station));
},
getMusicFolderList: async (args) => {
const { apiClientProps } = args;
@@ -822,6 +867,7 @@ export const SubsonicController: InternalControllerEndpoint = {
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
},
getPlaylistList: async ({ apiClientProps, query }) => {
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
@@ -1005,7 +1051,6 @@ export const SubsonicController: InternalControllerEndpoint = {
final.splice(0, 0, { label: 'all artists', value: '' });
return final;
},
getServerInfo: async (args) => {
const { apiClientProps } = args;
@@ -1722,6 +1767,24 @@ export const SubsonicController: InternalControllerEndpoint = {
return null;
},
updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args;
const res = await ssApiClient(apiClientProps).updateInternetRadioStation({
query: {
homepageUrl: body.homepageUrl,
id: query.id,
name: body.name,
streamUrl: body.streamUrl,
},
});
if (res.status !== 200) {
throw new Error('Failed to update internet radio station');
}
return null;
},
updatePlaylist: async (args) => {
const { apiClientProps, body, query } = args;