mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-08 21:32:59 +02:00
add native nd radio endpoints, support radio station images
This commit is contained in:
@@ -364,6 +364,9 @@
|
|||||||
"input_name": "name",
|
"input_name": "name",
|
||||||
"input_streamUrl": "stream url"
|
"input_streamUrl": "stream url"
|
||||||
},
|
},
|
||||||
|
"editRadioStation": {
|
||||||
|
"success": "radio station updated successfully"
|
||||||
|
},
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
"input_confirm": "type the name of the $t(entity.playlist, {\"count\": 1}) to confirm",
|
"input_confirm": "type the name of the $t(entity.playlist, {\"count\": 1}) to confirm",
|
||||||
"success": "$t(entity.playlist, {\"count\": 1}) deleted successfully",
|
"success": "$t(entity.playlist, {\"count\": 1}) deleted successfully",
|
||||||
|
|||||||
@@ -175,6 +175,20 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
|
deleteInternetRadioStationImage(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteInternetRadioStationImage`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'deleteInternetRadioStationImage',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
deletePlaylist(args) {
|
deletePlaylist(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
@@ -974,6 +988,20 @@ export const controller: GeneralController = {
|
|||||||
server.type,
|
server.type,
|
||||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
},
|
},
|
||||||
|
uploadInternetRadioStationImage(args) {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadInternetRadioStationImage`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'uploadInternetRadioStationImage',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
uploadPlaylistImage(args) {
|
uploadPlaylistImage(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,24 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
deleteInternetRadioStation: {
|
||||||
|
body: null,
|
||||||
|
method: 'DELETE',
|
||||||
|
path: 'radio/:id',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.deleteInternetRadioStation),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deleteInternetRadioStationImage: {
|
||||||
|
body: null,
|
||||||
|
method: 'DELETE',
|
||||||
|
path: 'radio/:id/image',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.deleteInternetRadioStationImage),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
deletePlaylist: {
|
deletePlaylist: {
|
||||||
body: null,
|
body: null,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -141,6 +159,15 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getRadioList: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'radio',
|
||||||
|
query: ndType._parameters.radioList,
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.radioList),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
getSongDetail: {
|
getSongDetail: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'song/:id',
|
path: 'song/:id',
|
||||||
@@ -214,6 +241,15 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
updateInternetRadioStation: {
|
||||||
|
body: ndType._parameters.updateInternetRadioStation,
|
||||||
|
method: 'PUT',
|
||||||
|
path: 'radio/:id',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.updateInternetRadioStation),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
updatePlaylist: {
|
updatePlaylist: {
|
||||||
body: ndType._parameters.updatePlaylist,
|
body: ndType._parameters.updatePlaylist,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -223,6 +259,15 @@ export const contract = c.router({
|
|||||||
500: resultWithHeaders(ndType._response.error),
|
500: resultWithHeaders(ndType._response.error),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
uploadInternetRadioStationImage: {
|
||||||
|
body: ndType._parameters.uploadInternetRadioStationImage,
|
||||||
|
method: 'POST',
|
||||||
|
path: 'radio/:id/image',
|
||||||
|
responses: {
|
||||||
|
200: resultWithHeaders(ndType._response.uploadInternetRadioStationImage),
|
||||||
|
500: resultWithHeaders(ndType._response.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
uploadPlaylistImage: {
|
uploadPlaylistImage: {
|
||||||
body: ndType._parameters.uploadPlaylistImage,
|
body: ndType._parameters.uploadPlaylistImage,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
|||||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize';
|
import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize';
|
||||||
import { NDSongListSort } from '/@/shared/api/navidrome/navidrome-types';
|
import { NDRadioListSort, NDSongListSort } from '/@/shared/api/navidrome/navidrome-types';
|
||||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||||
import { getFeatures, hasFeature, hasFeatureWithVersion, VersionInfo } from '/@/shared/api/utils';
|
import { getFeatures, hasFeature, hasFeatureWithVersion, VersionInfo } from '/@/shared/api/utils';
|
||||||
import {
|
import {
|
||||||
albumArtistListSortMap,
|
albumArtistListSortMap,
|
||||||
albumListSortMap,
|
albumListSortMap,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
|
DeleteInternetRadioStationImageArgs,
|
||||||
|
DeleteInternetRadioStationImageResponse,
|
||||||
DeletePlaylistImageArgs,
|
DeletePlaylistImageArgs,
|
||||||
DeletePlaylistImageResponse,
|
DeletePlaylistImageResponse,
|
||||||
genreListSortMap,
|
genreListSortMap,
|
||||||
@@ -26,6 +28,8 @@ import {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
tagListSortMap,
|
tagListSortMap,
|
||||||
|
UploadInternetRadioStationImageArgs,
|
||||||
|
UploadInternetRadioStationImageResponse,
|
||||||
UploadPlaylistImageArgs,
|
UploadPlaylistImageArgs,
|
||||||
UploadPlaylistImageResponse,
|
UploadPlaylistImageResponse,
|
||||||
userListSortMap,
|
userListSortMap,
|
||||||
@@ -35,7 +39,13 @@ import { ServerFeature } from '/@/shared/types/features-types';
|
|||||||
const VERSION_INFO: VersionInfo = [
|
const VERSION_INFO: VersionInfo = [
|
||||||
// Why 2? Subsonic controller will return 1 for its own implementation
|
// Why 2? Subsonic controller will return 1 for its own implementation
|
||||||
// Use 2 to denote that Navidrome's own API has a different endpoint
|
// Use 2 to denote that Navidrome's own API has a different endpoint
|
||||||
['0.61.0', { [ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1] }],
|
[
|
||||||
|
'0.61.0',
|
||||||
|
{
|
||||||
|
[ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1],
|
||||||
|
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
|
||||||
|
},
|
||||||
|
],
|
||||||
['0.60.4', { [ServerFeature.TRACK_YES_NO_RATING_FILTER]: [1] }],
|
['0.60.4', { [ServerFeature.TRACK_YES_NO_RATING_FILTER]: [1] }],
|
||||||
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
|
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
|
||||||
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
|
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
|
||||||
@@ -177,7 +187,38 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
deleteFavorite: SubsonicController.deleteFavorite,
|
deleteFavorite: SubsonicController.deleteFavorite,
|
||||||
deleteInternetRadioStation: SubsonicController.deleteInternetRadioStation,
|
deleteInternetRadioStation: async (args) => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps).deleteInternetRadioStation({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to delete internet radio station');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
deleteInternetRadioStationImage: async (
|
||||||
|
args: DeleteInternetRadioStationImageArgs,
|
||||||
|
): Promise<DeleteInternetRadioStationImageResponse> => {
|
||||||
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps as any).deleteInternetRadioStationImage({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to delete internet radio station image');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.data.status === 'ok';
|
||||||
|
},
|
||||||
deletePlaylist: async (args) => {
|
deletePlaylist: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -570,7 +611,24 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
getImageRequest: SubsonicController.getImageRequest,
|
getImageRequest: SubsonicController.getImageRequest,
|
||||||
getImageUrl: SubsonicController.getImageUrl,
|
getImageUrl: SubsonicController.getImageUrl,
|
||||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
getInternetRadioStations: async (args) => {
|
||||||
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps).getRadioList({
|
||||||
|
query: {
|
||||||
|
_end: -1,
|
||||||
|
_order: 'ASC',
|
||||||
|
_sort: NDRadioListSort.NAME,
|
||||||
|
_start: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get internet radio stations');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.body.data.map((station) => ndNormalize.internetRadioStation(station));
|
||||||
|
},
|
||||||
getLyrics: SubsonicController.getLyrics,
|
getLyrics: SubsonicController.getLyrics,
|
||||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||||
getPlaylistDetail: async (args) => {
|
getPlaylistDetail: async (args) => {
|
||||||
@@ -1168,7 +1226,26 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
id: res.body.data.id,
|
id: res.body.data.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateInternetRadioStation: SubsonicController.updateInternetRadioStation,
|
updateInternetRadioStation: async (args) => {
|
||||||
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps).updateInternetRadioStation({
|
||||||
|
body: {
|
||||||
|
homePageUrl: body.homepageUrl ?? '',
|
||||||
|
name: body.name,
|
||||||
|
streamUrl: body.streamUrl,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to update internet radio station');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
updatePlaylist: async (args) => {
|
updatePlaylist: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
@@ -1193,6 +1270,42 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
uploadInternetRadioStationImage: async (
|
||||||
|
args: UploadInternetRadioStationImageArgs,
|
||||||
|
): Promise<UploadInternetRadioStationImageResponse> => {
|
||||||
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
|
const server = apiClientProps.server;
|
||||||
|
const serverUrl = server?.url?.replace(/\/$/, '');
|
||||||
|
|
||||||
|
if (!serverUrl) {
|
||||||
|
throw new Error('Server is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
const bytes = body.image as Uint8Array<ArrayBuffer>;
|
||||||
|
const fileLike =
|
||||||
|
typeof File !== 'undefined'
|
||||||
|
? new File([bytes], 'image', { type: 'application/octet-stream' })
|
||||||
|
: new Blob([bytes], { type: 'application/octet-stream' });
|
||||||
|
form.append('image', fileLike as any);
|
||||||
|
|
||||||
|
const res = await axios.post(`${serverUrl}/api/radio/${query.id}/image`, form, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
...(server?.ndCredential && {
|
||||||
|
'x-nd-authorization': `Bearer ${server.ndCredential}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
signal: apiClientProps.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to upload internet radio station image');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data?.status === 'ok';
|
||||||
|
},
|
||||||
uploadPlaylistImage: async (
|
uploadPlaylistImage: async (
|
||||||
args: UploadPlaylistImageArgs,
|
args: UploadPlaylistImageArgs,
|
||||||
): Promise<UploadPlaylistImageResponse> => {
|
): Promise<UploadPlaylistImageResponse> => {
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
|||||||
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { RadioMetadataDisplay } from '/@/renderer/features/player/components/radio-metadata-display';
|
import { RadioMetadataDisplay } from '/@/renderer/features/player/components/radio-metadata-display';
|
||||||
import { useIsRadioActive } from '/@/renderer/features/radio/hooks/use-radio-player';
|
import {
|
||||||
|
useIsRadioActive,
|
||||||
|
useRadioPlayer,
|
||||||
|
} from '/@/renderer/features/radio/hooks/use-radio-player';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import {
|
import {
|
||||||
useAppStore,
|
useAppStore,
|
||||||
@@ -50,9 +53,11 @@ export const LeftControls = () => {
|
|||||||
|
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const isRadioActive = useIsRadioActive();
|
const isRadioActive = useIsRadioActive();
|
||||||
|
const { currentStationArt } = useRadioPlayer();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
|
|
||||||
const isRadioMode = isRadioActive;
|
const isRadioMode = isRadioActive;
|
||||||
|
const hasRadioStationImage = Boolean(currentStationArt?.imageId || currentStationArt?.imageUrl);
|
||||||
const hideImage = image && !collapsed;
|
const hideImage = image && !collapsed;
|
||||||
const isSongDefined = Boolean(currentSong?.id) && !isRadioMode;
|
const isSongDefined = Boolean(currentSong?.id) && !isRadioMode;
|
||||||
const title = currentSong?.name;
|
const title = currentSong?.name;
|
||||||
@@ -128,7 +133,22 @@ export const LeftControls = () => {
|
|||||||
})}
|
})}
|
||||||
openDelay={0}
|
openDelay={0}
|
||||||
>
|
>
|
||||||
{isRadioMode ? (
|
{isRadioMode && hasRadioStationImage ? (
|
||||||
|
<ItemImage
|
||||||
|
className={clsx(
|
||||||
|
styles.playerbarImage,
|
||||||
|
PlaybackSelectors.playerCoverArt,
|
||||||
|
)}
|
||||||
|
enableDebounce={false}
|
||||||
|
enableViewport={false}
|
||||||
|
fetchPriority="high"
|
||||||
|
id={currentStationArt?.imageId ?? undefined}
|
||||||
|
itemType={LibraryItem.RADIO_STATION}
|
||||||
|
serverId={currentStationArt?.serverId}
|
||||||
|
src={currentStationArt?.imageUrl ?? ''}
|
||||||
|
type="table"
|
||||||
|
/>
|
||||||
|
) : isRadioMode ? (
|
||||||
<Center
|
<Center
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.playerbarImage,
|
styles.playerbarImage,
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent, type ReactNode, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
|
import { useDeleteInternetRadioStationImage } from '/@/renderer/features/radio/mutations/delete-internet-radio-station-image-mutation';
|
||||||
import { useUpdateRadioStation } from '/@/renderer/features/radio/mutations/update-radio-station-mutation';
|
import { useUpdateRadioStation } from '/@/renderer/features/radio/mutations/update-radio-station-mutation';
|
||||||
|
import { useUploadInternetRadioStationImage } from '/@/renderer/features/radio/mutations/upload-internet-radio-station-image-mutation';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { logFn } from '/@/renderer/utils/logger';
|
import { logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Box } from '/@/shared/components/box/box';
|
||||||
|
import { FileButton } from '/@/shared/components/file-button/file-button';
|
||||||
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { closeAllModals, openModal } from '/@/shared/components/modal/modal';
|
import { closeAllModals, openModal } from '/@/shared/components/modal/modal';
|
||||||
import { ModalButton } from '/@/shared/components/modal/model-shared';
|
import { ModalButton } from '/@/shared/components/modal/model-shared';
|
||||||
@@ -15,19 +23,51 @@ import { toast } from '/@/shared/components/toast/toast';
|
|||||||
import { useForm } from '/@/shared/hooks/use-form';
|
import { useForm } from '/@/shared/hooks/use-form';
|
||||||
import {
|
import {
|
||||||
InternetRadioStation,
|
InternetRadioStation,
|
||||||
|
LibraryItem,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
UpdateInternetRadioStationBody,
|
UpdateInternetRadioStationBody,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
interface EditRadioStationFormProps {
|
interface EditRadioStationFormProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
station: InternetRadioStation;
|
station: InternetRadioStation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RadioStationImageProps = {
|
||||||
|
imageId: null | string;
|
||||||
|
imageUrl: null | string;
|
||||||
|
uploadedImage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const EditRadioStationForm = ({ onCancel, station }: EditRadioStationFormProps) => {
|
export const EditRadioStationForm = ({ onCancel, station }: EditRadioStationFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mutation = useUpdateRadioStation({});
|
const updateMutation = useUpdateRadioStation({});
|
||||||
|
const uploadImageMutation = useUploadInternetRadioStationImage({});
|
||||||
|
const deleteImageMutation = useDeleteInternetRadioStationImage({});
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const isCoverImageDisplayed = hasFeature(server, ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD);
|
||||||
|
|
||||||
|
const stationImage: RadioStationImageProps = {
|
||||||
|
imageId: station.imageId ?? null,
|
||||||
|
imageUrl: station.imageUrl ?? null,
|
||||||
|
uploadedImage: station.uploadedImage ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||||
|
const [pendingPreviewUrl, setPendingPreviewUrl] = useState<null | string>(null);
|
||||||
|
const [removeCustomCover, setRemoveCustomCover] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingFile) {
|
||||||
|
setPendingPreviewUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(pendingFile);
|
||||||
|
setPendingPreviewUrl(url);
|
||||||
|
return () => URL.revokeObjectURL(url);
|
||||||
|
}, [pendingFile]);
|
||||||
|
|
||||||
const form = useForm<UpdateInternetRadioStationBody>({
|
const form = useForm<UpdateInternetRadioStationBody>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -37,74 +77,234 @@ export const EditRadioStationForm = ({ onCancel, station }: EditRadioStationForm
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = form.onSubmit((values) => {
|
const handleSubmit = form.onSubmit(async (values) => {
|
||||||
if (!server) return;
|
if (!server?.id) return;
|
||||||
|
|
||||||
mutation.mutate(
|
setIsSaving(true);
|
||||||
{
|
try {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
apiClientProps: { serverId: server.id },
|
apiClientProps: { serverId: server.id },
|
||||||
body: values,
|
body: values,
|
||||||
query: { id: station.id },
|
query: { id: station.id },
|
||||||
},
|
|
||||||
{
|
|
||||||
onError: (error) => {
|
|
||||||
logFn.error(logMsg.other.error, {
|
|
||||||
meta: { error: error as Error },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.error({
|
if (pendingFile) {
|
||||||
message: (error as Error).message,
|
const buffer = await pendingFile.arrayBuffer();
|
||||||
title: t('error.genericError', {
|
await uploadImageMutation.mutateAsync({
|
||||||
|
apiClientProps: { serverId: server.id },
|
||||||
|
body: { image: new Uint8Array(buffer) },
|
||||||
|
query: { id: station.id },
|
||||||
|
});
|
||||||
|
} else if (removeCustomCover && stationImage.uploadedImage) {
|
||||||
|
await deleteImageMutation.mutateAsync({
|
||||||
|
apiClientProps: { serverId: server.id },
|
||||||
|
query: { id: station.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success({
|
||||||
|
message: t('form.editRadioStation.success', {
|
||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
}) as string,
|
}) as string,
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
closeAllModals();
|
closeAllModals();
|
||||||
},
|
} catch (err: unknown) {
|
||||||
},
|
logFn.error(logMsg.other.error, {
|
||||||
);
|
meta: { error: err as Error },
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
toast.error({
|
||||||
<form onSubmit={handleSubmit}>
|
message: (err as Error)?.message,
|
||||||
<Stack gap="md">
|
title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSubmitDisabled = !form.values.name || !form.values.streamUrl || isSaving;
|
||||||
|
const hadUploadedCover = !!stationImage.uploadedImage;
|
||||||
|
|
||||||
|
const fieldNodes: ReactNode[] = [
|
||||||
<TextInput
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
key="name"
|
||||||
label={t('form.createRadioStation.input', {
|
label={t('form.createRadioStation.input', {
|
||||||
context: 'name',
|
context: 'name',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
required
|
required
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>,
|
||||||
<TextInput
|
<TextInput
|
||||||
|
key="streamUrl"
|
||||||
label={t('form.createRadioStation.input', {
|
label={t('form.createRadioStation.input', {
|
||||||
context: 'streamUrl',
|
context: 'streamUrl',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
required
|
required
|
||||||
{...form.getInputProps('streamUrl')}
|
{...form.getInputProps('streamUrl')}
|
||||||
/>
|
/>,
|
||||||
<TextInput
|
<TextInput
|
||||||
|
key="homepageUrl"
|
||||||
label={t('form.createRadioStation.input', {
|
label={t('form.createRadioStation.input', {
|
||||||
context: 'homepageUrl',
|
context: 'homepageUrl',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
{...form.getInputProps('homepageUrl')}
|
{...form.getInputProps('homepageUrl')}
|
||||||
|
/>,
|
||||||
|
<Group justify="flex-end" key="actions">
|
||||||
|
<ModalButton disabled={isSaving} onClick={onCancel}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</ModalButton>
|
||||||
|
<ModalButton
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
loading={isSaving}
|
||||||
|
type="submit"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</ModalButton>
|
||||||
|
</Group>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{isCoverImageDisplayed && server?.id ? (
|
||||||
|
<Flex align="flex-start" gap="lg" wrap="wrap">
|
||||||
|
<RadioStationCoverField
|
||||||
|
hadUploadedCover={hadUploadedCover}
|
||||||
|
onClearPending={() => setPendingFile(null)}
|
||||||
|
onFileSelect={(file) => {
|
||||||
|
if (!file) return;
|
||||||
|
setRemoveCustomCover(false);
|
||||||
|
setPendingFile(file);
|
||||||
|
}}
|
||||||
|
onToggleRemoveCover={() => setRemoveCustomCover((v) => !v)}
|
||||||
|
pendingFile={pendingFile}
|
||||||
|
pendingPreviewUrl={pendingPreviewUrl}
|
||||||
|
removeCustomCover={removeCustomCover}
|
||||||
|
stationImage={stationImage}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<Stack gap="md" style={{ flex: '1 1 220px', minWidth: 0 }}>
|
||||||
<ModalButton onClick={onCancel} variant="subtle">
|
{fieldNodes}
|
||||||
{t('common.cancel', { postProcess: 'sentenceCase' })}
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton loading={mutation.isPending} type="submit" variant="filled">
|
|
||||||
{t('common.save', { postProcess: 'sentenceCase' })}
|
|
||||||
</ModalButton>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Stack gap="md">{fieldNodes}</Stack>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const COVER_SIZE = 240;
|
||||||
|
|
||||||
|
function RadioStationCoverField({
|
||||||
|
hadUploadedCover,
|
||||||
|
onClearPending,
|
||||||
|
onFileSelect,
|
||||||
|
onToggleRemoveCover,
|
||||||
|
pendingFile,
|
||||||
|
pendingPreviewUrl,
|
||||||
|
removeCustomCover,
|
||||||
|
stationImage,
|
||||||
|
}: {
|
||||||
|
hadUploadedCover: boolean;
|
||||||
|
onClearPending: () => void;
|
||||||
|
onFileSelect: (file: File | null) => void;
|
||||||
|
onToggleRemoveCover: () => void;
|
||||||
|
pendingFile: File | null;
|
||||||
|
pendingPreviewUrl: null | string;
|
||||||
|
removeCustomCover: boolean;
|
||||||
|
stationImage: RadioStationImageProps;
|
||||||
|
}) {
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const showServerCover = !pendingPreviewUrl && !removeCustomCover;
|
||||||
|
const previewId = showServerCover ? stationImage.imageId || undefined : undefined;
|
||||||
|
const previewSrc = pendingPreviewUrl || (showServerCover ? stationImage.imageUrl || '' : '');
|
||||||
|
|
||||||
|
const secondaryAction = () => {
|
||||||
|
if (pendingFile) {
|
||||||
|
onClearPending();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hadUploadedCover) {
|
||||||
|
onToggleRemoveCover();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondaryDisabled = !pendingFile && !hadUploadedCover;
|
||||||
|
|
||||||
|
const secondaryIcon = pendingFile ? 'x' : removeCustomCover ? 'arrowLeft' : 'delete';
|
||||||
|
|
||||||
|
const iconControls = (
|
||||||
|
<>
|
||||||
|
<FileButton accept="image/*" onChange={onFileSelect}>
|
||||||
|
{(props) => (
|
||||||
|
<ActionIcon
|
||||||
|
icon="uploadImage"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={secondaryDisabled}
|
||||||
|
icon={secondaryIcon}
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={secondaryAction}
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const coverArt = (
|
||||||
|
<ItemImage
|
||||||
|
enableViewport={false}
|
||||||
|
id={previewId}
|
||||||
|
itemType={LibraryItem.RADIO_STATION}
|
||||||
|
serverId={server?.id}
|
||||||
|
src={previewSrc}
|
||||||
|
type="header"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
borderRadius: 'var(--mantine-radius-md)',
|
||||||
|
flexShrink: 0,
|
||||||
|
height: COVER_SIZE,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
width: COVER_SIZE,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{coverArt}
|
||||||
|
<Group
|
||||||
|
gap={4}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(0, 0, 0, 0.55)',
|
||||||
|
borderRadius: 'var(--mantine-radius-md)',
|
||||||
|
bottom: 6,
|
||||||
|
padding: 4,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 6,
|
||||||
|
}}
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
|
{iconControls}
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const openEditRadioStationModal = (
|
export const openEditRadioStationModal = (
|
||||||
station: InternetRadioStation,
|
station: InternetRadioStation,
|
||||||
server: null | ServerListItem,
|
server: null | ServerListItem,
|
||||||
@@ -119,8 +319,11 @@ export const openEditRadioStationModal = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasImageUpload = hasFeature(server, ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD);
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: <EditRadioStationForm onCancel={closeAllModals} station={station} />,
|
children: <EditRadioStationForm onCancel={closeAllModals} station={station} />,
|
||||||
|
size: hasImageUpload ? 'lg' : 'md',
|
||||||
title: t('common.edit', { postProcess: 'titleCase' }) as string,
|
title: t('common.edit', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,11 +20,55 @@
|
|||||||
|
|
||||||
.radio-item-button {
|
.radio-item-button {
|
||||||
all: unset;
|
all: unset;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--mantine-radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-line {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-item-link {
|
.radio-item-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-item-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-item:hover .radio-item-actions,
|
||||||
|
.radio-item:focus-within .radio-item-actions {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import styles from './radio-list-items.module.css';
|
import styles from './radio-list-items.module.css';
|
||||||
|
|
||||||
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
||||||
import { openEditRadioStationModal } from '/@/renderer/features/radio/components/edit-radio-station-form';
|
import { openEditRadioStationModal } from '/@/renderer/features/radio/components/edit-radio-station-form';
|
||||||
import {
|
import {
|
||||||
useRadioControls,
|
useRadioControls,
|
||||||
@@ -12,15 +13,15 @@ import {
|
|||||||
import { useDeleteRadioStation } from '/@/renderer/features/radio/mutations/delete-radio-station-mutation';
|
import { useDeleteRadioStation } from '/@/renderer/features/radio/mutations/delete-radio-station-mutation';
|
||||||
import { useCurrentServer, usePermissions } from '/@/renderer/store';
|
import { useCurrentServer, usePermissions } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Box } from '/@/shared/components/box/box';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
import { closeAllModals, ConfirmModal, openModal } from '/@/shared/components/modal/modal';
|
import { closeAllModals, ConfirmModal, openModal } from '/@/shared/components/modal/modal';
|
||||||
import { Paper } from '/@/shared/components/paper/paper';
|
import { Paper } from '/@/shared/components/paper/paper';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { InternetRadioStation } from '/@/shared/types/domain-types';
|
import { InternetRadioStation, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
interface RadioListItemProps {
|
interface RadioListItemProps {
|
||||||
station: InternetRadioStation;
|
station: InternetRadioStation;
|
||||||
@@ -44,8 +45,13 @@ const RadioListItem = ({ station }: RadioListItemProps) => {
|
|||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (stationIsPlaying) {
|
if (stationIsPlaying) {
|
||||||
stop();
|
stop();
|
||||||
} else {
|
} else if (server?.id) {
|
||||||
play(station.streamUrl, station.name);
|
play(station.streamUrl, station.name, {
|
||||||
|
id: station.id,
|
||||||
|
imageId: station.imageId,
|
||||||
|
imageUrl: station.imageUrl,
|
||||||
|
serverId: server.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,27 +113,39 @@ const RadioListItem = ({ station }: RadioListItemProps) => {
|
|||||||
})}
|
})}
|
||||||
p="md"
|
p="md"
|
||||||
>
|
>
|
||||||
<Flex align="flex-start" gap="md" justify="space-between">
|
<Flex align="center" gap="md" justify="space-between" wrap="nowrap">
|
||||||
<button className={styles['radio-item-button']} onClick={handleClick} role="button">
|
<button className={styles['radio-item-button']} onClick={handleClick} type="button">
|
||||||
<Stack gap="xs">
|
<Group align="center" gap="md" wrap="nowrap">
|
||||||
<Group gap="xs">
|
<Box className={styles.thumbnail}>
|
||||||
<Icon color="muted" icon="radio" size="md" />
|
<ItemImage
|
||||||
|
enableViewport={false}
|
||||||
|
id={station.imageId ?? undefined}
|
||||||
|
imageContainerProps={{
|
||||||
|
className: styles['image-container'],
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.RADIO_STATION}
|
||||||
|
serverId={server?.id}
|
||||||
|
src={station.imageUrl ?? ''}
|
||||||
|
type="table"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Stack className={styles.meta} gap={4}>
|
||||||
<Text fw={500} size="md">
|
<Text fw={500} size="md">
|
||||||
{station.name}
|
{station.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
<Text className={styles['meta-line']} isMuted size="sm">
|
||||||
<Text isMuted size="sm">
|
|
||||||
{station.streamUrl}
|
{station.streamUrl}
|
||||||
</Text>
|
</Text>
|
||||||
{station.homepageUrl && (
|
{station.homepageUrl ? (
|
||||||
<Text isMuted size="sm">
|
<Text className={styles['meta-line']} isMuted size="sm">
|
||||||
{station.homepageUrl}
|
{station.homepageUrl}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Group>
|
||||||
</button>
|
</button>
|
||||||
{(permissions.radio.edit || permissions.radio.delete) && (
|
{(permissions.radio.edit || permissions.radio.delete) && (
|
||||||
<Group gap="xs">
|
<Group className={styles['radio-item-actions']} gap="xs">
|
||||||
{permissions.radio.edit && (
|
{permissions.radio.edit && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="edit"
|
icon="edit"
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/
|
|||||||
import { usePlaybackType, usePlayerStoreBase, useSettingsStore } from '/@/renderer/store';
|
import { usePlaybackType, usePlayerStoreBase, useSettingsStore } from '/@/renderer/store';
|
||||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export type RadioCurrentStationArt = {
|
||||||
|
id: string;
|
||||||
|
imageId?: null | string;
|
||||||
|
imageUrl?: null | string;
|
||||||
|
serverId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface RadioMetadata {
|
export interface RadioMetadata {
|
||||||
artist: null | string;
|
artist: null | string;
|
||||||
title: null | string;
|
title: null | string;
|
||||||
@@ -15,13 +22,18 @@ export interface RadioMetadata {
|
|||||||
interface RadioStore {
|
interface RadioStore {
|
||||||
actions: {
|
actions: {
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
play: (streamUrl?: string, stationName?: string) => void;
|
play: (
|
||||||
|
streamUrl?: string,
|
||||||
|
stationName?: string,
|
||||||
|
stationArt?: null | RadioCurrentStationArt,
|
||||||
|
) => void;
|
||||||
setCurrentStreamUrl: (currentStreamUrl: null | string) => void;
|
setCurrentStreamUrl: (currentStreamUrl: null | string) => void;
|
||||||
setIsPlaying: (isPlaying: boolean) => void;
|
setIsPlaying: (isPlaying: boolean) => void;
|
||||||
setMetadata: (metadata: null | RadioMetadata) => void;
|
setMetadata: (metadata: null | RadioMetadata) => void;
|
||||||
setStationName: (stationName: null | string) => void;
|
setStationName: (stationName: null | string) => void;
|
||||||
stop: () => void;
|
stop: () => void;
|
||||||
};
|
};
|
||||||
|
currentStationArt: null | RadioCurrentStationArt;
|
||||||
currentStreamUrl: null | string;
|
currentStreamUrl: null | string;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
metadata: null | RadioMetadata;
|
metadata: null | RadioMetadata;
|
||||||
@@ -34,7 +46,11 @@ export const useRadioStore = createWithEqualityFn<RadioStore>((set) => ({
|
|||||||
set({ isPlaying: false });
|
set({ isPlaying: false });
|
||||||
usePlayerStoreBase.getState().mediaPause();
|
usePlayerStoreBase.getState().mediaPause();
|
||||||
},
|
},
|
||||||
play: (streamUrl?: string, stationName?: string) => {
|
play: (
|
||||||
|
streamUrl?: string,
|
||||||
|
stationName?: string,
|
||||||
|
stationArt?: null | RadioCurrentStationArt,
|
||||||
|
) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newStreamUrl = streamUrl ?? state.currentStreamUrl;
|
const newStreamUrl = streamUrl ?? state.currentStreamUrl;
|
||||||
const newStationName = stationName ?? state.stationName;
|
const newStationName = stationName ?? state.stationName;
|
||||||
@@ -43,12 +59,19 @@ export const useRadioStore = createWithEqualityFn<RadioStore>((set) => ({
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset metadata when switching stations (streamUrl changes)
|
const streamUrlExplicit = streamUrl !== undefined;
|
||||||
const isSwitchingStation = newStreamUrl !== state.currentStreamUrl;
|
const isSwitchingStation =
|
||||||
|
streamUrlExplicit && streamUrl !== state.currentStreamUrl;
|
||||||
|
|
||||||
|
let nextStationArt = state.currentStationArt;
|
||||||
|
if (isSwitchingStation) {
|
||||||
|
nextStationArt = stationArt ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
usePlayerStoreBase.getState().mediaPlay();
|
usePlayerStoreBase.getState().mediaPlay();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
currentStationArt: nextStationArt,
|
||||||
currentStreamUrl: newStreamUrl,
|
currentStreamUrl: newStreamUrl,
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
metadata: isSwitchingStation ? null : state.metadata,
|
metadata: isSwitchingStation ? null : state.metadata,
|
||||||
@@ -64,6 +87,7 @@ export const useRadioStore = createWithEqualityFn<RadioStore>((set) => ({
|
|||||||
const playbackType = useSettingsStore.getState().playback.type;
|
const playbackType = useSettingsStore.getState().playback.type;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
|
currentStationArt: null,
|
||||||
currentStreamUrl: null,
|
currentStreamUrl: null,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
@@ -79,6 +103,7 @@ export const useRadioStore = createWithEqualityFn<RadioStore>((set) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
currentStationArt: null,
|
||||||
currentStreamUrl: null,
|
currentStreamUrl: null,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
@@ -90,12 +115,14 @@ export const useIsPlayingRadio = () => useRadioStore((state) => state.isPlaying)
|
|||||||
export const useIsRadioActive = () => useRadioStore((state) => Boolean(state.currentStreamUrl));
|
export const useIsRadioActive = () => useRadioStore((state) => Boolean(state.currentStreamUrl));
|
||||||
|
|
||||||
export const useRadioPlayer = () => {
|
export const useRadioPlayer = () => {
|
||||||
|
const currentStationArt = useRadioStore((state) => state.currentStationArt);
|
||||||
const currentStreamUrl = useRadioStore((state) => state.currentStreamUrl);
|
const currentStreamUrl = useRadioStore((state) => state.currentStreamUrl);
|
||||||
const isPlaying = useRadioStore((state) => state.isPlaying);
|
const isPlaying = useRadioStore((state) => state.isPlaying);
|
||||||
const metadata = useRadioStore((state) => state.metadata);
|
const metadata = useRadioStore((state) => state.metadata);
|
||||||
const stationName = useRadioStore((state) => state.stationName);
|
const stationName = useRadioStore((state) => state.stationName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
currentStationArt,
|
||||||
currentStreamUrl,
|
currentStreamUrl,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
metadata,
|
metadata,
|
||||||
@@ -163,6 +190,7 @@ export const useRadioAudioInstance = () => {
|
|||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentStreamUrl(null);
|
setCurrentStreamUrl(null);
|
||||||
setStationName(null);
|
setStationName(null);
|
||||||
|
useRadioStore.setState({ currentStationArt: null, metadata: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
mpvPlayerListener.rendererPlay(handleMpvPlay);
|
mpvPlayerListener.rendererPlay(handleMpvPlay);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import {
|
||||||
|
DeleteInternetRadioStationImageArgs,
|
||||||
|
DeleteInternetRadioStationImageResponse,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useDeleteInternetRadioStationImage = (args: MutationHookArgs) => {
|
||||||
|
const { options } = args || {};
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
DeleteInternetRadioStationImageResponse,
|
||||||
|
AxiosError,
|
||||||
|
DeleteInternetRadioStationImageArgs,
|
||||||
|
null
|
||||||
|
>({
|
||||||
|
mutationFn: (args) => {
|
||||||
|
return api.controller.deleteInternetRadioStationImage({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
const { apiClientProps } = variables;
|
||||||
|
const serverId = apiClientProps.serverId;
|
||||||
|
|
||||||
|
if (!serverId) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.radio.list(serverId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import {
|
||||||
|
UploadInternetRadioStationImageArgs,
|
||||||
|
UploadInternetRadioStationImageResponse,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useUploadInternetRadioStationImage = (args: MutationHookArgs) => {
|
||||||
|
const { options } = args || {};
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
UploadInternetRadioStationImageResponse,
|
||||||
|
AxiosError,
|
||||||
|
UploadInternetRadioStationImageArgs,
|
||||||
|
null
|
||||||
|
>({
|
||||||
|
mutationFn: (args) => {
|
||||||
|
return api.controller.uploadInternetRadioStationImage({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
const { apiClientProps } = variables;
|
||||||
|
const serverId = apiClientProps.serverId;
|
||||||
|
|
||||||
|
if (!serverId) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.radio.list(serverId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -167,7 +167,7 @@ const SidebarImage = () => {
|
|||||||
const { setSideBar } = useAppStoreActions();
|
const { setSideBar } = useAppStoreActions();
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const isRadioActive = useIsRadioActive();
|
const isRadioActive = useIsRadioActive();
|
||||||
const { isPlaying: isRadioPlaying } = useRadioPlayer();
|
const { currentStationArt, isPlaying: isRadioPlaying } = useRadioPlayer();
|
||||||
const { blurExplicitImages } = useGeneralSettings();
|
const { blurExplicitImages } = useGeneralSettings();
|
||||||
|
|
||||||
const imageUrl = useItemImageUrl({
|
const imageUrl = useItemImageUrl({
|
||||||
@@ -177,6 +177,14 @@ const SidebarImage = () => {
|
|||||||
type: 'sidebar',
|
type: 'sidebar',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const radioImageUrl = useItemImageUrl({
|
||||||
|
id: isRadioActive ? currentStationArt?.imageId || undefined : undefined,
|
||||||
|
imageUrl: isRadioActive ? currentStationArt?.imageUrl || undefined : undefined,
|
||||||
|
itemType: LibraryItem.RADIO_STATION,
|
||||||
|
serverId: isRadioActive ? currentStationArt?.serverId : undefined,
|
||||||
|
type: 'sidebar',
|
||||||
|
});
|
||||||
|
|
||||||
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
const isPlayingRadio = isRadioActive && isRadioPlaying;
|
||||||
const isSongDefined = Boolean(currentSong?.id);
|
const isSongDefined = Boolean(currentSong?.id);
|
||||||
|
|
||||||
@@ -224,7 +232,9 @@ const SidebarImage = () => {
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isPlayingRadio ? (
|
{isRadioActive && radioImageUrl ? (
|
||||||
|
<img className={styles.sidebarImage} loading="eager" src={radioImageUrl} />
|
||||||
|
) : isRadioActive ? (
|
||||||
<Center
|
<Center
|
||||||
className={styles.sidebarImage}
|
className={styles.sidebarImage}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
ExplicitStatus,
|
ExplicitStatus,
|
||||||
Genre,
|
Genre,
|
||||||
|
InternetRadioStation,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
Playlist,
|
Playlist,
|
||||||
RelatedArtist,
|
RelatedArtist,
|
||||||
@@ -543,10 +544,28 @@ const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeInternetRadioStation = (
|
||||||
|
item: z.infer<typeof ndType._response.radioStation>,
|
||||||
|
): InternetRadioStation => {
|
||||||
|
const homepageUrl = item.homePageUrl?.trim() ? item.homePageUrl : null;
|
||||||
|
const imageId = item.uploadedImage ? `ra-${item.id}&square=true&_=${item.updatedAt}` : item.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
homepageUrl,
|
||||||
|
id: item.id,
|
||||||
|
imageId,
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.name,
|
||||||
|
streamUrl: item.streamUrl,
|
||||||
|
uploadedImage: item.uploadedImage || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const ndNormalize = {
|
export const ndNormalize = {
|
||||||
album: normalizeAlbum,
|
album: normalizeAlbum,
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
genre: normalizeGenre,
|
genre: normalizeGenre,
|
||||||
|
internetRadioStation: normalizeInternetRadioStation,
|
||||||
playlist: normalizePlaylist,
|
playlist: normalizePlaylist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
user: normalizeUser,
|
user: normalizeUser,
|
||||||
|
|||||||
@@ -660,6 +660,12 @@ const updatePlaylist = playlist;
|
|||||||
|
|
||||||
const updatePlaylistParameters = createPlaylistParameters.partial();
|
const updatePlaylistParameters = createPlaylistParameters.partial();
|
||||||
|
|
||||||
|
const updateInternetRadioStationParameters = z.object({
|
||||||
|
homePageUrl: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
streamUrl: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const uploadPlaylistImage = z.object({
|
const uploadPlaylistImage = z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
});
|
});
|
||||||
@@ -672,8 +678,14 @@ const deletePlaylistImage = z.object({
|
|||||||
status: z.string(),
|
status: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadInternetRadioStationImage = uploadPlaylistImage;
|
||||||
|
const uploadInternetRadioStationImageParameters = uploadPlaylistImageParameters;
|
||||||
|
const deleteInternetRadioStationImage = deletePlaylistImage;
|
||||||
|
|
||||||
const deletePlaylist = z.null();
|
const deletePlaylist = z.null();
|
||||||
|
|
||||||
|
const deleteInternetRadioStation = deletePlaylist;
|
||||||
|
|
||||||
const addToPlaylist = z.object({
|
const addToPlaylist = z.object({
|
||||||
added: z.number(),
|
added: z.number(),
|
||||||
});
|
});
|
||||||
@@ -748,12 +760,35 @@ const queue = z.object({
|
|||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export enum NDRadioListSort {
|
||||||
|
NAME = 'name',
|
||||||
|
}
|
||||||
|
|
||||||
|
const radioStation = z.object({
|
||||||
|
createdAt: z.string(),
|
||||||
|
homePageUrl: z.string().optional(),
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
streamUrl: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
uploadedImage: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const radioList = z.array(radioStation);
|
||||||
|
|
||||||
|
const updateInternetRadioStation = radioStation;
|
||||||
|
|
||||||
|
const radioListParameters = optionalPaginationParameters.extend({
|
||||||
|
_sort: z.nativeEnum(NDRadioListSort).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ndType = {
|
export const ndType = {
|
||||||
_enum: {
|
_enum: {
|
||||||
albumArtistList: NDAlbumArtistListSort,
|
albumArtistList: NDAlbumArtistListSort,
|
||||||
albumList: NDAlbumListSort,
|
albumList: NDAlbumListSort,
|
||||||
genreList: genreListSort,
|
genreList: genreListSort,
|
||||||
playlistList: NDPlaylistListSort,
|
playlistList: NDPlaylistListSort,
|
||||||
|
radioList: NDRadioListSort,
|
||||||
songList: NDSongListSort,
|
songList: NDSongListSort,
|
||||||
tagList: NDTagListSort,
|
tagList: NDTagListSort,
|
||||||
userList: ndUserListSort,
|
userList: ndUserListSort,
|
||||||
@@ -767,12 +802,15 @@ export const ndType = {
|
|||||||
genreList: genreListParameters,
|
genreList: genreListParameters,
|
||||||
moveItem: moveItemParameters,
|
moveItem: moveItemParameters,
|
||||||
playlistList: playlistListParameters,
|
playlistList: playlistListParameters,
|
||||||
|
radioList: radioListParameters,
|
||||||
removeFromPlaylist: removeFromPlaylistParameters,
|
removeFromPlaylist: removeFromPlaylistParameters,
|
||||||
saveQueue: saveQueueParameters,
|
saveQueue: saveQueueParameters,
|
||||||
shareItem: shareItemParameters,
|
shareItem: shareItemParameters,
|
||||||
songList: songListParameters,
|
songList: songListParameters,
|
||||||
tagList: tagListParameters,
|
tagList: tagListParameters,
|
||||||
|
updateInternetRadioStation: updateInternetRadioStationParameters,
|
||||||
updatePlaylist: updatePlaylistParameters,
|
updatePlaylist: updatePlaylistParameters,
|
||||||
|
uploadInternetRadioStationImage: uploadInternetRadioStationImageParameters,
|
||||||
uploadPlaylistImage: uploadPlaylistImageParameters,
|
uploadPlaylistImage: uploadPlaylistImageParameters,
|
||||||
userList: userListParameters,
|
userList: userListParameters,
|
||||||
},
|
},
|
||||||
@@ -784,6 +822,8 @@ export const ndType = {
|
|||||||
albumList,
|
albumList,
|
||||||
authenticate,
|
authenticate,
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
|
deleteInternetRadioStation,
|
||||||
|
deleteInternetRadioStationImage,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
deletePlaylistImage,
|
deletePlaylistImage,
|
||||||
error,
|
error,
|
||||||
@@ -795,13 +835,17 @@ export const ndType = {
|
|||||||
playlistSong,
|
playlistSong,
|
||||||
playlistSongList,
|
playlistSongList,
|
||||||
queue,
|
queue,
|
||||||
|
radioList,
|
||||||
|
radioStation,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
saveQueue,
|
saveQueue,
|
||||||
shareItem,
|
shareItem,
|
||||||
song,
|
song,
|
||||||
songList,
|
songList,
|
||||||
tagList,
|
tagList,
|
||||||
|
updateInternetRadioStation,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
|
uploadInternetRadioStationImage,
|
||||||
uploadPlaylistImage,
|
uploadPlaylistImage,
|
||||||
user,
|
user,
|
||||||
userList,
|
userList,
|
||||||
|
|||||||
@@ -432,6 +432,8 @@ const normalizeInternetRadioStation = (
|
|||||||
return {
|
return {
|
||||||
homepageUrl: item.homepageUrl || null,
|
homepageUrl: item.homepageUrl || null,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
imageId: item.coverArt?.toString() || null,
|
||||||
|
imageUrl: null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
streamUrl: item.streamUrl,
|
streamUrl: item.streamUrl,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -755,6 +755,7 @@ const playQueueByIndex = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const internetRadioStation = z.object({
|
const internetRadioStation = z.object({
|
||||||
|
coverArt: z.string().optional(),
|
||||||
homepageUrl: z.string().optional(),
|
homepageUrl: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
@@ -959,6 +959,16 @@ export type DeleteInternetRadioStationArgs = BaseEndpointArgs & {
|
|||||||
query: DeleteInternetRadioStationQuery;
|
query: DeleteInternetRadioStationQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteInternetRadioStationImageArgs = BaseEndpointArgs & {
|
||||||
|
query: DeleteInternetRadioStationImageQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteInternetRadioStationImageQuery = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteInternetRadioStationImageResponse = boolean;
|
||||||
|
|
||||||
export type DeleteInternetRadioStationQuery = {
|
export type DeleteInternetRadioStationQuery = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@@ -999,10 +1009,13 @@ export type GetInternetRadioStationsArgs = BaseEndpointArgs;
|
|||||||
export type GetInternetRadioStationsResponse = InternetRadioStation[];
|
export type GetInternetRadioStationsResponse = InternetRadioStation[];
|
||||||
|
|
||||||
export type InternetRadioStation = {
|
export type InternetRadioStation = {
|
||||||
homepageUrl?: null | string;
|
homepageUrl: null | string;
|
||||||
id: string;
|
id: string;
|
||||||
|
imageId?: null | string;
|
||||||
|
imageUrl?: null | string;
|
||||||
name: string;
|
name: string;
|
||||||
streamUrl: string;
|
streamUrl: string;
|
||||||
|
uploadedImage?: null | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlaylistListArgs = BaseEndpointArgs & { query: PlaylistListQuery };
|
export type PlaylistListArgs = BaseEndpointArgs & { query: PlaylistListQuery };
|
||||||
@@ -1117,6 +1130,21 @@ export type UpdatePlaylistQuery = {
|
|||||||
// Update Playlist
|
// Update Playlist
|
||||||
export type UpdatePlaylistResponse = null | undefined;
|
export type UpdatePlaylistResponse = null | undefined;
|
||||||
|
|
||||||
|
export type UploadInternetRadioStationImageArgs = BaseEndpointArgs & {
|
||||||
|
body: UploadInternetRadioStationImageBody;
|
||||||
|
query: UploadInternetRadioStationImageQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadInternetRadioStationImageBody = {
|
||||||
|
image: Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadInternetRadioStationImageQuery = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadInternetRadioStationImageResponse = boolean;
|
||||||
|
|
||||||
export type UploadPlaylistImageArgs = BaseEndpointArgs & {
|
export type UploadPlaylistImageArgs = BaseEndpointArgs & {
|
||||||
body: UploadPlaylistImageBody;
|
body: UploadPlaylistImageBody;
|
||||||
query: UploadPlaylistImageQuery;
|
query: UploadPlaylistImageQuery;
|
||||||
@@ -1415,6 +1443,9 @@ export type ControllerEndpoint = {
|
|||||||
deleteInternetRadioStation: (
|
deleteInternetRadioStation: (
|
||||||
args: DeleteInternetRadioStationArgs,
|
args: DeleteInternetRadioStationArgs,
|
||||||
) => Promise<DeleteInternetRadioStationResponse>;
|
) => Promise<DeleteInternetRadioStationResponse>;
|
||||||
|
deleteInternetRadioStationImage?: (
|
||||||
|
args: DeleteInternetRadioStationImageArgs,
|
||||||
|
) => Promise<DeleteInternetRadioStationImageResponse>;
|
||||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||||
deletePlaylistImage?: (args: DeletePlaylistImageArgs) => Promise<DeletePlaylistImageResponse>;
|
deletePlaylistImage?: (args: DeletePlaylistImageArgs) => Promise<DeletePlaylistImageResponse>;
|
||||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||||
@@ -1470,6 +1501,9 @@ export type ControllerEndpoint = {
|
|||||||
args: UpdateInternetRadioStationArgs,
|
args: UpdateInternetRadioStationArgs,
|
||||||
) => Promise<UpdateInternetRadioStationResponse>;
|
) => Promise<UpdateInternetRadioStationResponse>;
|
||||||
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
||||||
|
uploadInternetRadioStationImage?: (
|
||||||
|
args: UploadInternetRadioStationImageArgs,
|
||||||
|
) => Promise<UploadInternetRadioStationImageResponse>;
|
||||||
uploadPlaylistImage?: (args: UploadPlaylistImageArgs) => Promise<UploadPlaylistImageResponse>;
|
uploadPlaylistImage?: (args: UploadPlaylistImageArgs) => Promise<UploadPlaylistImageResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1540,6 +1574,9 @@ export type InternalControllerEndpoint = {
|
|||||||
deleteInternetRadioStation: (
|
deleteInternetRadioStation: (
|
||||||
args: ReplaceApiClientProps<DeleteInternetRadioStationArgs>,
|
args: ReplaceApiClientProps<DeleteInternetRadioStationArgs>,
|
||||||
) => Promise<DeleteInternetRadioStationResponse>;
|
) => Promise<DeleteInternetRadioStationResponse>;
|
||||||
|
deleteInternetRadioStationImage?: (
|
||||||
|
args: ReplaceApiClientProps<DeleteInternetRadioStationImageArgs>,
|
||||||
|
) => Promise<DeleteInternetRadioStationImageResponse>;
|
||||||
deletePlaylist: (
|
deletePlaylist: (
|
||||||
args: ReplaceApiClientProps<DeletePlaylistArgs>,
|
args: ReplaceApiClientProps<DeletePlaylistArgs>,
|
||||||
) => Promise<DeletePlaylistResponse>;
|
) => Promise<DeletePlaylistResponse>;
|
||||||
@@ -1630,6 +1667,9 @@ export type InternalControllerEndpoint = {
|
|||||||
updatePlaylist: (
|
updatePlaylist: (
|
||||||
args: ReplaceApiClientProps<UpdatePlaylistArgs>,
|
args: ReplaceApiClientProps<UpdatePlaylistArgs>,
|
||||||
) => Promise<UpdatePlaylistResponse>;
|
) => Promise<UpdatePlaylistResponse>;
|
||||||
|
uploadInternetRadioStationImage?: (
|
||||||
|
args: ReplaceApiClientProps<UploadInternetRadioStationImageArgs>,
|
||||||
|
) => Promise<UploadInternetRadioStationImageResponse>;
|
||||||
uploadPlaylistImage?: (
|
uploadPlaylistImage?: (
|
||||||
args: ReplaceApiClientProps<UploadPlaylistImageArgs>,
|
args: ReplaceApiClientProps<UploadPlaylistImageArgs>,
|
||||||
) => Promise<UploadPlaylistImageResponse>;
|
) => Promise<UploadPlaylistImageResponse>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
export enum ServerFeature {
|
export enum ServerFeature {
|
||||||
ALBUM_YES_NO_RATING_FILTER = 'albumYesNoRatingFilter',
|
ALBUM_YES_NO_RATING_FILTER = 'albumYesNoRatingFilter',
|
||||||
BFR = 'bfr',
|
BFR = 'bfr',
|
||||||
|
INTERNET_RADIO_IMAGE_UPLOAD = 'internetRadioImageUpload',
|
||||||
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
||||||
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
||||||
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
||||||
|
|||||||
Reference in New Issue
Block a user