mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
support navidrome playlist image upload
This commit is contained in:
@@ -189,6 +189,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
deletePlaylistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deletePlaylistImage`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'deletePlaylistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
getAlbumArtistDetail(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
@@ -960,4 +974,18 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
uploadPlaylistImage(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadPlaylistImage`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'uploadPlaylistImage',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,6 +55,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
deletePlaylistImage: {
|
||||
body: null,
|
||||
method: 'DELETE',
|
||||
path: 'playlist/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.deletePlaylistImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
getAlbumArtistDetail: {
|
||||
method: 'GET',
|
||||
path: 'artist/:id',
|
||||
@@ -214,6 +223,15 @@ export const contract = c.router({
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
uploadPlaylistImage: {
|
||||
body: ndType._parameters.uploadPlaylistImage,
|
||||
method: 'POST',
|
||||
path: 'playlist/:id/image',
|
||||
responses: {
|
||||
200: resultWithHeaders(ndType._response.uploadPlaylistImage),
|
||||
500: resultWithHeaders(ndType._response.error),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const axiosClient = axios.create({});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import { set } from 'idb-keyval';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
albumArtistListSortMap,
|
||||
albumListSortMap,
|
||||
AuthenticationResponse,
|
||||
DeletePlaylistImageArgs,
|
||||
DeletePlaylistImageResponse,
|
||||
genreListSortMap,
|
||||
InternalControllerEndpoint,
|
||||
playlistListSortMap,
|
||||
@@ -23,6 +26,8 @@ import {
|
||||
SortOrder,
|
||||
sortOrderMap,
|
||||
tagListSortMap,
|
||||
UploadPlaylistImageArgs,
|
||||
UploadPlaylistImageResponse,
|
||||
userListSortMap,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
@@ -30,6 +35,7 @@ import { ServerFeature } from '/@/shared/types/features-types';
|
||||
const VERSION_INFO: VersionInfo = [
|
||||
// Why 2? Subsonic controller will return 1 for its own implementation
|
||||
// Use 2 to denote that Navidrome's own API has a different endpoint
|
||||
['0.61.0', { [ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1] }],
|
||||
['0.60.4', { [ServerFeature.TRACK_YES_NO_RATING_FILTER]: [1] }],
|
||||
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
|
||||
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
|
||||
@@ -187,6 +193,23 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
deletePlaylistImage: async (
|
||||
args: DeletePlaylistImageArgs,
|
||||
): Promise<DeletePlaylistImageResponse> => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps as any).deletePlaylistImage({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete playlist image');
|
||||
}
|
||||
|
||||
return res.body.data.status === 'ok';
|
||||
},
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1170,4 +1193,40 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
uploadPlaylistImage: async (
|
||||
args: UploadPlaylistImageArgs,
|
||||
): Promise<UploadPlaylistImageResponse> => {
|
||||
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/playlist/${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 playlist image');
|
||||
}
|
||||
|
||||
return res.data?.status === 'ok';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
||||
import { useDeletePlaylistImage } from '/@/renderer/features/playlists/mutations/delete-playlist-image-mutation';
|
||||
import { useUploadPlaylistImage } from '/@/renderer/features/playlists/mutations/upload-playlist-image-mutation';
|
||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||
import {
|
||||
LibraryHeader,
|
||||
@@ -18,9 +20,14 @@ import { ListSearchInput } from '/@/renderer/features/shared/components/list-sea
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { FileButton } from '/@/shared/components/file-button/file-button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, Playlist, Song } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistDetailSongListHeaderProps {
|
||||
@@ -30,6 +37,64 @@ interface PlaylistDetailSongListHeaderProps {
|
||||
onToggleQueryBuilder?: () => void;
|
||||
}
|
||||
|
||||
function ImageUploadOverlay({ data }: { data?: Playlist }) {
|
||||
const uploadPlaylistImageMutation = useUploadPlaylistImage({});
|
||||
const deletePlaylistImageMutation = useDeletePlaylistImage({});
|
||||
const server = useCurrentServer();
|
||||
|
||||
if (!data) return null;
|
||||
if (!hasFeature(server, ServerFeature.PLAYLIST_IMAGE_UPLOAD)) return null;
|
||||
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<FileButton
|
||||
accept="image/*"
|
||||
onChange={async (file) => {
|
||||
if (!file || !data?._serverId) return;
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
uploadPlaylistImageMutation.mutate({
|
||||
apiClientProps: {
|
||||
serverId: data._serverId,
|
||||
},
|
||||
body: { image: new Uint8Array(buffer) },
|
||||
query: { id: data.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<ActionIcon
|
||||
icon="uploadImage"
|
||||
iconProps={{ size: 'lg' }}
|
||||
radius="xl"
|
||||
size="xs"
|
||||
variant="default"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</FileButton>
|
||||
<ActionIcon
|
||||
disabled={!data?.uploadedImage}
|
||||
icon="delete"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!data?._serverId) return;
|
||||
deletePlaylistImageMutation.mutate({
|
||||
apiClientProps: {
|
||||
serverId: data._serverId,
|
||||
},
|
||||
query: { id: data.id },
|
||||
});
|
||||
}}
|
||||
radius="xl"
|
||||
size="xs"
|
||||
variant="default"
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeader = ({
|
||||
isSmartPlaylist,
|
||||
}: PlaylistDetailSongListHeaderProps) => {
|
||||
@@ -94,6 +159,7 @@ export const PlaylistDetailSongListHeader = ({
|
||||
) : (
|
||||
<LibraryHeader
|
||||
compact
|
||||
imageOverlay={<ImageUploadOverlay data={detailQuery?.data} />}
|
||||
imageUrl={imageUrl}
|
||||
item={{
|
||||
imageId: detailQuery?.data?.imageId,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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 { DeletePlaylistImageArgs, DeletePlaylistImageResponse } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useDeletePlaylistImage = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DeletePlaylistImageResponse, AxiosError, DeletePlaylistImageArgs, null>({
|
||||
mutationFn: (args) => {
|
||||
return api.controller.deletePlaylistImage({
|
||||
...args,
|
||||
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
const { apiClientProps, query } = variables;
|
||||
const serverId = apiClientProps.serverId;
|
||||
|
||||
if (!serverId) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.list(serverId),
|
||||
});
|
||||
|
||||
if (query?.id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.detail(serverId, query.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
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 { UploadPlaylistImageArgs, UploadPlaylistImageResponse } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useUploadPlaylistImage = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UploadPlaylistImageResponse, AxiosError, UploadPlaylistImageArgs, null>({
|
||||
mutationFn: (args) => {
|
||||
return api.controller.uploadPlaylistImage({
|
||||
...args,
|
||||
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
const { apiClientProps, query } = variables;
|
||||
const serverId = apiClientProps.serverId;
|
||||
|
||||
if (!serverId) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.list(serverId),
|
||||
});
|
||||
|
||||
if (query?.id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.detail(serverId, query.id),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -112,6 +112,7 @@
|
||||
}
|
||||
|
||||
.image-section {
|
||||
position: relative;
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
@@ -124,6 +125,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
right: var(--theme-spacing-xs);
|
||||
bottom: var(--theme-spacing-xs);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.image-section:hover .image-overlay {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
|
||||
@@ -35,6 +35,7 @@ interface LibraryHeaderProps {
|
||||
children?: ReactNode;
|
||||
compact?: boolean;
|
||||
containerClassName?: string;
|
||||
imageOverlay?: ReactNode;
|
||||
imagePlaceholderUrl?: null | string;
|
||||
imageUrl?: null | string;
|
||||
item: {
|
||||
@@ -56,6 +57,7 @@ export const LibraryHeader = forwardRef(
|
||||
children,
|
||||
compact,
|
||||
containerClassName,
|
||||
imageOverlay,
|
||||
imageUrl,
|
||||
item,
|
||||
title,
|
||||
@@ -168,6 +170,16 @@ export const LibraryHeader = forwardRef(
|
||||
src={imageUrl || ''}
|
||||
type="header"
|
||||
/>
|
||||
{imageOverlay && (
|
||||
<div
|
||||
className={styles.imageOverlay}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
{imageOverlay}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{title && (
|
||||
<div className={styles.metadataSection}>
|
||||
|
||||
@@ -490,6 +490,8 @@ const normalizePlaylist = (
|
||||
item: z.infer<typeof ndType._response.playlist>,
|
||||
server?: null | ServerListItem,
|
||||
): Playlist => {
|
||||
const imageId = !item.uploadedImage ? item.id : `pl-${item.id}&square=true&_=${item.updatedAt}`;
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.PLAYLIST,
|
||||
_serverId: server?.id || 'unknown',
|
||||
@@ -498,7 +500,7 @@ const normalizePlaylist = (
|
||||
duration: item.duration * 1000,
|
||||
genres: [],
|
||||
id: item.id,
|
||||
imageId: item.id,
|
||||
imageId,
|
||||
imageUrl: null,
|
||||
name: item.name,
|
||||
owner: item.ownerName,
|
||||
@@ -508,6 +510,7 @@ const normalizePlaylist = (
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
sync: item.sync,
|
||||
uploadedImage: item.uploadedImage,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -624,6 +624,7 @@ const playlist = z.object({
|
||||
songCount: z.number(),
|
||||
sync: z.boolean(),
|
||||
updatedAt: z.string(),
|
||||
uploadedImage: z.string().optional(),
|
||||
});
|
||||
|
||||
const playlistList = z.array(playlist);
|
||||
@@ -659,6 +660,18 @@ const updatePlaylist = playlist;
|
||||
|
||||
const updatePlaylistParameters = createPlaylistParameters.partial();
|
||||
|
||||
const uploadPlaylistImage = z.object({
|
||||
status: z.string(),
|
||||
});
|
||||
|
||||
const uploadPlaylistImageParameters = z.object({
|
||||
image: z.instanceof(Uint8Array),
|
||||
});
|
||||
|
||||
const deletePlaylistImage = z.object({
|
||||
status: z.string(),
|
||||
});
|
||||
|
||||
const deletePlaylist = z.null();
|
||||
|
||||
const addToPlaylist = z.object({
|
||||
@@ -760,6 +773,7 @@ export const ndType = {
|
||||
songList: songListParameters,
|
||||
tagList: tagListParameters,
|
||||
updatePlaylist: updatePlaylistParameters,
|
||||
uploadPlaylistImage: uploadPlaylistImageParameters,
|
||||
userList: userListParameters,
|
||||
},
|
||||
_response: {
|
||||
@@ -771,6 +785,7 @@ export const ndType = {
|
||||
authenticate,
|
||||
createPlaylist,
|
||||
deletePlaylist,
|
||||
deletePlaylistImage,
|
||||
error,
|
||||
genre,
|
||||
genreList,
|
||||
@@ -787,6 +802,7 @@ export const ndType = {
|
||||
songList,
|
||||
tagList,
|
||||
updatePlaylist,
|
||||
uploadPlaylistImage,
|
||||
user,
|
||||
userList,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
FileButton as MantineFileButton,
|
||||
FileButtonProps as MantineFileButtonProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export interface FileButtonProps extends MantineFileButtonProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const FileButton = MantineFileButton;
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
LuArrowUpToLine,
|
||||
LuBookOpen,
|
||||
LuBraces,
|
||||
LuCamera,
|
||||
LuCheck,
|
||||
LuChevronDown,
|
||||
LuChevronLast,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
LuCloudDownload,
|
||||
LuCornerDownRight,
|
||||
LuCornerUpRight,
|
||||
LuDelete,
|
||||
LuDisc,
|
||||
LuDisc3,
|
||||
LuDownload,
|
||||
@@ -117,6 +117,7 @@ import {
|
||||
LuTable,
|
||||
LuTimer,
|
||||
LuTimerOff,
|
||||
LuTrash,
|
||||
LuTriangleAlert,
|
||||
LuUpload,
|
||||
LuUser,
|
||||
@@ -248,7 +249,7 @@ export const AppIcon = {
|
||||
check: LuCheck,
|
||||
clipboardCopy: LuClipboardCopy,
|
||||
collection: LuPackage2,
|
||||
delete: LuDelete,
|
||||
delete: LuTrash,
|
||||
disc: LuDisc,
|
||||
download: LuDownload,
|
||||
dragHorizontal: LuGripHorizontal,
|
||||
@@ -351,6 +352,7 @@ export const AppIcon = {
|
||||
unfavorite: LuHeartCrack,
|
||||
unpin: LuPinOff,
|
||||
upload: LuUpload,
|
||||
uploadImage: LuCamera,
|
||||
user: LuUser,
|
||||
userManage: LuUserRoundCog,
|
||||
visibility: MdOutlineVisibility,
|
||||
|
||||
@@ -344,6 +344,7 @@ export type Playlist = {
|
||||
size: null | number;
|
||||
songCount: null | number;
|
||||
sync?: boolean | null;
|
||||
uploadedImage?: string;
|
||||
};
|
||||
|
||||
export type RelatedAlbumArtist = {
|
||||
@@ -968,6 +969,16 @@ export type DeletePlaylistArgs = BaseEndpointArgs & {
|
||||
query: DeletePlaylistQuery;
|
||||
};
|
||||
|
||||
export type DeletePlaylistImageArgs = BaseEndpointArgs & {
|
||||
query: DeletePlaylistImageQuery;
|
||||
};
|
||||
|
||||
export type DeletePlaylistImageQuery = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DeletePlaylistImageResponse = boolean;
|
||||
|
||||
export type DeletePlaylistQuery = { id: string };
|
||||
|
||||
// Delete Playlist
|
||||
@@ -1106,6 +1117,21 @@ export type UpdatePlaylistQuery = {
|
||||
// Update Playlist
|
||||
export type UpdatePlaylistResponse = null | undefined;
|
||||
|
||||
export type UploadPlaylistImageArgs = BaseEndpointArgs & {
|
||||
body: UploadPlaylistImageBody;
|
||||
query: UploadPlaylistImageQuery;
|
||||
};
|
||||
|
||||
export type UploadPlaylistImageBody = {
|
||||
image: Uint8Array;
|
||||
};
|
||||
|
||||
export type UploadPlaylistImageQuery = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type UploadPlaylistImageResponse = boolean;
|
||||
|
||||
type PlaylistListSortMap = {
|
||||
jellyfin: Record<PlaylistListSort, JFPlaylistListSort | undefined>;
|
||||
navidrome: Record<PlaylistListSort, NDPlaylistListSort | undefined>;
|
||||
@@ -1390,6 +1416,7 @@ export type ControllerEndpoint = {
|
||||
args: DeleteInternetRadioStationArgs,
|
||||
) => Promise<DeleteInternetRadioStationResponse>;
|
||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||
deletePlaylistImage?: (args: DeletePlaylistImageArgs) => Promise<DeletePlaylistImageResponse>;
|
||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||
getAlbumArtistInfo?: (args: AlbumArtistInfoArgs) => Promise<AlbumArtistInfoResponse | null>;
|
||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
||||
@@ -1443,6 +1470,7 @@ export type ControllerEndpoint = {
|
||||
args: UpdateInternetRadioStationArgs,
|
||||
) => Promise<UpdateInternetRadioStationResponse>;
|
||||
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
||||
uploadPlaylistImage?: (args: UploadPlaylistImageArgs) => Promise<UploadPlaylistImageResponse>;
|
||||
};
|
||||
|
||||
export type DownloadArgs = BaseEndpointArgs & {
|
||||
@@ -1515,6 +1543,9 @@ export type InternalControllerEndpoint = {
|
||||
deletePlaylist: (
|
||||
args: ReplaceApiClientProps<DeletePlaylistArgs>,
|
||||
) => Promise<DeletePlaylistResponse>;
|
||||
deletePlaylistImage?: (
|
||||
args: ReplaceApiClientProps<DeletePlaylistImageArgs>,
|
||||
) => Promise<DeletePlaylistImageResponse>;
|
||||
getAlbumArtistDetail: (
|
||||
args: ReplaceApiClientProps<AlbumArtistDetailArgs>,
|
||||
) => Promise<AlbumArtistDetailResponse>;
|
||||
@@ -1599,6 +1630,9 @@ export type InternalControllerEndpoint = {
|
||||
updatePlaylist: (
|
||||
args: ReplaceApiClientProps<UpdatePlaylistArgs>,
|
||||
) => Promise<UpdatePlaylistResponse>;
|
||||
uploadPlaylistImage?: (
|
||||
args: ReplaceApiClientProps<UploadPlaylistImageArgs>,
|
||||
) => Promise<UploadPlaylistImageResponse>;
|
||||
};
|
||||
|
||||
export type LyricGetQuery = {
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum ServerFeature {
|
||||
MUSIC_FOLDER_MULTISELECT = 'musicFolderMultiselect',
|
||||
OS_FORM_POST = 'osFormPost',
|
||||
OS_TRANSCODE_DECISION = 'osTranscodeDecision',
|
||||
PLAYLIST_IMAGE_UPLOAD = 'playlistImageUpload',
|
||||
PLAYLISTS_SMART = 'playlistsSmart',
|
||||
PUBLIC_PLAYLIST = 'publicPlaylist',
|
||||
SERVER_PLAY_QUEUE = 'serverPlayQueue',
|
||||
|
||||
Reference in New Issue
Block a user