support navidrome artist image upload/delete

This commit is contained in:
jeffvli
2026-04-06 11:06:18 -07:00
parent 4a986069f8
commit 918f453066
10 changed files with 327 additions and 36 deletions
+28
View File
@@ -147,6 +147,20 @@ export const controller: GeneralController = {
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
deleteArtistImage(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: deleteArtistImage`,
);
}
return apiController(
'deleteArtistImage',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
deleteFavorite(args) { deleteFavorite(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -988,6 +1002,20 @@ export const controller: GeneralController = {
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
uploadArtistImage(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: uploadArtistImage`,
);
}
return apiController(
'uploadArtistImage',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
uploadInternetRadioStationImage(args) { uploadInternetRadioStationImage(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -46,6 +46,15 @@ export const contract = c.router({
500: resultWithHeaders(ndType._response.error), 500: resultWithHeaders(ndType._response.error),
}, },
}, },
deleteArtistImage: {
body: null,
method: 'DELETE',
path: 'artist/:id/image',
responses: {
200: resultWithHeaders(ndType._response.deleteArtistImage),
500: resultWithHeaders(ndType._response.error),
},
},
deleteInternetRadioStation: { deleteInternetRadioStation: {
body: null, body: null,
method: 'DELETE', method: 'DELETE',
@@ -259,6 +268,15 @@ export const contract = c.router({
500: resultWithHeaders(ndType._response.error), 500: resultWithHeaders(ndType._response.error),
}, },
}, },
uploadArtistImage: {
body: ndType._parameters.uploadArtistImage,
method: 'POST',
path: 'artist/:id/image',
responses: {
200: resultWithHeaders(ndType._response.uploadArtistImage),
500: resultWithHeaders(ndType._response.error),
},
},
uploadInternetRadioStationImage: { uploadInternetRadioStationImage: {
body: ndType._parameters.uploadInternetRadioStationImage, body: ndType._parameters.uploadInternetRadioStationImage,
method: 'POST', method: 'POST',
@@ -13,6 +13,8 @@ import {
albumArtistListSortMap, albumArtistListSortMap,
albumListSortMap, albumListSortMap,
AuthenticationResponse, AuthenticationResponse,
DeleteArtistImageArgs,
DeleteArtistImageResponse,
DeleteInternetRadioStationImageArgs, DeleteInternetRadioStationImageArgs,
DeleteInternetRadioStationImageResponse, DeleteInternetRadioStationImageResponse,
DeletePlaylistImageArgs, DeletePlaylistImageArgs,
@@ -28,6 +30,8 @@ import {
SortOrder, SortOrder,
sortOrderMap, sortOrderMap,
tagListSortMap, tagListSortMap,
UploadArtistImageArgs,
UploadArtistImageResponse,
UploadInternetRadioStationImageArgs, UploadInternetRadioStationImageArgs,
UploadInternetRadioStationImageResponse, UploadInternetRadioStationImageResponse,
UploadPlaylistImageArgs, UploadPlaylistImageArgs,
@@ -42,6 +46,7 @@ const VERSION_INFO: VersionInfo = [
[ [
'0.61.0', '0.61.0',
{ {
[ServerFeature.ARTIST_IMAGE_UPLOAD]: [1],
[ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1], [ServerFeature.INTERNET_RADIO_IMAGE_UPLOAD]: [1],
[ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1], [ServerFeature.PLAYLIST_IMAGE_UPLOAD]: [1],
}, },
@@ -186,6 +191,21 @@ export const NavidromeController: InternalControllerEndpoint = {
id: res.body.data.id, id: res.body.data.id,
}; };
}, },
deleteArtistImage: async (args: DeleteArtistImageArgs): Promise<DeleteArtistImageResponse> => {
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps as any).deleteArtistImage({
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to delete artist image');
}
return res.body.data.status === 'ok';
},
deleteFavorite: SubsonicController.deleteFavorite, deleteFavorite: SubsonicController.deleteFavorite,
deleteInternetRadioStation: async (args) => { deleteInternetRadioStation: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1270,6 +1290,40 @@ export const NavidromeController: InternalControllerEndpoint = {
return null; return null;
}, },
uploadArtistImage: async (args: UploadArtistImageArgs): Promise<UploadArtistImageResponse> => {
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/artist/${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 artist image');
}
return res.data?.status === 'ok';
},
uploadInternetRadioStationImage: async ( uploadInternetRadioStationImage: async (
args: UploadInternetRadioStationImageArgs, args: UploadInternetRadioStationImageArgs,
): Promise<UploadInternetRadioStationImageResponse> => { ): Promise<UploadInternetRadioStationImageResponse> => {
@@ -1,5 +1,5 @@
import { useQuery, useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query'; import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
import { forwardRef, Fragment, useCallback, useMemo } from 'react'; import { forwardRef, Fragment, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
@@ -8,6 +8,8 @@ import styles from './album-artist-detail-header.module.css';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { getArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped'; import { getArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped';
import { useDeleteArtistImage } from '/@/renderer/features/artists/mutations/delete-artist-image-mutation';
import { useUploadArtistImage } from '/@/renderer/features/artists/mutations/upload-artist-image-mutation';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller'; import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { import {
@@ -20,17 +22,83 @@ import { AppRoute } from '/@/renderer/router/routes';
import { useAppStore, useCurrentServer, useShowRatings } from '/@/renderer/store'; import { useAppStore, useCurrentServer, useShowRatings } from '/@/renderer/store';
import { useArtistReleaseTypeItems, usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { useArtistReleaseTypeItems, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { SEPARATOR_STRING, sortAlbumList } from '/@/shared/api/utils'; import { hasFeature, SEPARATOR_STRING, sortAlbumList } 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 { Group } from '/@/shared/components/group/group';
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 { AlbumListResponse, LibraryItem, ServerType } from '/@/shared/types/domain-types'; import {
AlbumArtistDetailResponse,
AlbumListResponse,
LibraryItem,
ServerType,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
interface AlbumArtistDetailHeaderProps { interface AlbumArtistDetailHeaderProps {
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>; albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
} }
function ArtistImageUploadOverlay({ data }: { data?: AlbumArtistDetailResponse }) {
const uploadArtistImageMutation = useUploadArtistImage({});
const deleteArtistImageMutation = useDeleteArtistImage({});
const server = useCurrentServer();
if (!data) return null;
if (!hasFeature(server, ServerFeature.ARTIST_IMAGE_UPLOAD)) return null;
return (
<Group gap="xs">
<FileButton
accept="image/*"
onChange={async (file) => {
if (!file || !data?._serverId) return;
const buffer = await file.arrayBuffer();
uploadArtistImageMutation.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;
deleteArtistImageMutation.mutate({
apiClientProps: {
serverId: data._serverId,
},
query: { id: data.id },
});
}}
radius="xl"
size="xs"
variant="default"
/>
</Group>
);
}
export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDetailHeaderProps>( export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDetailHeaderProps>(
({ albumsQuery }, ref) => { ({ albumsQuery }, ref) => {
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
@@ -167,37 +235,23 @@ export const AlbumArtistDetailHeader = forwardRef<HTMLDivElement, AlbumArtistDet
[detailQuery.data], [detailQuery.data],
); );
const imageUrl = useItemImageUrl({ const headerImageUrl = useItemImageUrl({
id: detailQuery.data?.imageId || undefined, id: detailQuery.data?.imageId || undefined,
imageUrl: detailQuery.data?.imageUrl, imageUrl: detailQuery.data?.imageUrl,
itemType: LibraryItem.ALBUM_ARTIST, itemType: LibraryItem.ALBUM_ARTIST,
type: 'itemCard', type: 'header',
});
const artistInfoQuery = useQuery({
...artistsQueries.albumArtistInfo({
query: { id: routeId, limit: 10 },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
}); });
const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME; const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME;
const selectedImageUrl = useMemo(() => {
return detailQuery.data?.imageUrl || imageUrl;
}, [detailQuery.data?.imageUrl, imageUrl]);
const alternateImageUrl = artistInfoQuery.data?.imageUrl;
const hasImageId = Boolean(detailQuery.data?.imageId);
const fallbackHeaderImageUrl = alternateImageUrl || selectedImageUrl;
return ( return (
<LibraryHeader <LibraryHeader
imageUrl={hasImageId ? undefined : fallbackHeaderImageUrl} compact
imageOverlay={<ArtistImageUploadOverlay data={detailQuery.data} />}
imageUrl={headerImageUrl}
item={{ item={{
imageId: detailQuery.data?.imageId, imageId: detailQuery.data?.imageId,
imageUrl: hasImageId ? undefined : fallbackHeaderImageUrl, imageUrl: detailQuery.data?.imageUrl,
route: AppRoute.LIBRARY_ALBUM_ARTISTS, route: AppRoute.LIBRARY_ALBUM_ARTISTS,
type: LibraryItem.ALBUM_ARTIST, type: LibraryItem.ALBUM_ARTIST,
}} }}
@@ -0,0 +1,41 @@
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 { DeleteArtistImageArgs, DeleteArtistImageResponse } from '/@/shared/types/domain-types';
export const useDeleteArtistImage = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
return useMutation<DeleteArtistImageResponse, AxiosError, DeleteArtistImageArgs, null>({
mutationFn: (args) => {
return api.controller.deleteArtistImage({
...args,
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onSuccess: (_data, variables) => {
const { apiClientProps, query } = variables;
const serverId = apiClientProps.serverId;
if (!serverId) return;
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.list(serverId),
});
if (query?.id) {
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.detail(serverId, { id: query.id }),
});
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.info(serverId, { id: query.id }),
});
}
},
...options,
});
};
@@ -0,0 +1,41 @@
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 { UploadArtistImageArgs, UploadArtistImageResponse } from '/@/shared/types/domain-types';
export const useUploadArtistImage = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
return useMutation<UploadArtistImageResponse, AxiosError, UploadArtistImageArgs, null>({
mutationFn: (args) => {
return api.controller.uploadArtistImage({
...args,
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onSuccess: (_data, variables) => {
const { apiClientProps, query } = variables;
const serverId = apiClientProps.serverId;
if (!serverId) return;
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.list(serverId),
});
if (query?.id) {
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.detail(serverId, { id: query.id }),
});
queryClient.invalidateQueries({
queryKey: queryKeys.albumArtists.info(serverId, { id: query.id }),
});
}
},
...options,
});
};
+25 -12
View File
@@ -18,14 +18,20 @@ import {
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/types'; import { ServerListItem, ServerType } from '/@/shared/types/types';
const getImageUrl = (args: { url: null | string }) => { // const getImageUrl = (args: { url: null | string }) => {
const { url } = args; // const { url } = args;
if (url === '/app/artist-placeholder.webp') { // if (url === '/app/artist-placeholder.webp') {
return null; // return null;
} // }
return url; // return url;
}; // };
const navidromeImageIdWithCacheBust = (
id: string,
uploadedImage: string | undefined,
updatedAt: string | undefined,
): string => (!uploadedImage ? id : `${id}&_=${updatedAt ?? ''}`);
interface WithDate { interface WithDate {
playDate?: string; playDate?: string;
@@ -397,7 +403,7 @@ const normalizeAlbumArtist = (
}, },
server?: null | ServerListItem, server?: null | ServerListItem,
): AlbumArtist => { ): AlbumArtist => {
const imageUrl = getImageUrl({ url: item?.largeImageUrl?.replace(/\?size=\d+/, '') || null }); // const imageUrl = getImageUrl({ url: item?.largeImageUrl?.replace(/\?size=\d+/, '') || null });
let albumCount: number; let albumCount: number;
let songCount: number; let songCount: number;
@@ -416,6 +422,12 @@ const normalizeAlbumArtist = (
songCount = item.songCount; songCount = item.songCount;
} }
const imageId = navidromeImageIdWithCacheBust(
item.id,
item.uploadedImage,
item.updatedAt ?? item.externalInfoUpdatedAt,
);
return { return {
_itemType: LibraryItem.ALBUM_ARTIST, _itemType: LibraryItem.ALBUM_ARTIST,
_serverId: server?.id || 'unknown', _serverId: server?.id || 'unknown',
@@ -435,8 +447,8 @@ const normalizeAlbumArtist = (
songCount: null, songCount: null,
})), })),
id: item.id, id: item.id,
imageId: item.id, imageId,
imageUrl: imageUrl || null, imageUrl: null,
lastPlayedAt: normalizePlayDate(item), lastPlayedAt: normalizePlayDate(item),
mbz: item.mbzArtistId || null, mbz: item.mbzArtistId || null,
name: item.name, name: item.name,
@@ -451,6 +463,7 @@ const normalizeAlbumArtist = (
userRating: artist.userRating || null, userRating: artist.userRating || null,
})) || [], })) || [],
songCount, songCount,
uploadedImage: item.uploadedImage,
userFavorite: item.starred || false, userFavorite: item.starred || false,
userRating: item.rating || null, userRating: item.rating || null,
}; };
@@ -460,7 +473,7 @@ const normalizePlaylist = (
item: z.infer<typeof ndType._response.playlist>, item: z.infer<typeof ndType._response.playlist>,
server?: null | ServerListItem, server?: null | ServerListItem,
): Playlist => { ): Playlist => {
const imageId = !item.uploadedImage ? item.id : `${item.id}&_=${item.updatedAt}`; const imageId = navidromeImageIdWithCacheBust(item.id, item.uploadedImage, item.updatedAt);
return { return {
_itemType: LibraryItem.PLAYLIST, _itemType: LibraryItem.PLAYLIST,
@@ -517,7 +530,7 @@ const normalizeInternetRadioStation = (
item: z.infer<typeof ndType._response.radioStation>, item: z.infer<typeof ndType._response.radioStation>,
): InternetRadioStation => { ): InternetRadioStation => {
const homepageUrl = item.homePageUrl?.trim() ? item.homePageUrl : null; const homepageUrl = item.homePageUrl?.trim() ? item.homePageUrl : null;
const imageId = item.uploadedImage ? `${item.id}&_=${item.updatedAt}` : item.id; const imageId = navidromeImageIdWithCacheBust(item.id, item.uploadedImage, item.updatedAt);
return { return {
homepageUrl, homepageUrl,
@@ -428,6 +428,7 @@ const albumArtist = z.object({
starredAt: z.string(), starredAt: z.string(),
stats: z.record(z.string(), stats).optional(), stats: z.record(z.string(), stats).optional(),
updatedAt: z.string().optional(), updatedAt: z.string().optional(),
uploadedImage: z.string().optional(),
}); });
const albumArtistList = z.array(albumArtist); const albumArtistList = z.array(albumArtist);
@@ -683,6 +684,9 @@ const deletePlaylistImage = z.object({
const uploadInternetRadioStationImage = uploadPlaylistImage; const uploadInternetRadioStationImage = uploadPlaylistImage;
const uploadInternetRadioStationImageParameters = uploadPlaylistImageParameters; const uploadInternetRadioStationImageParameters = uploadPlaylistImageParameters;
const uploadArtistImage = uploadPlaylistImage;
const uploadArtistImageParameters = uploadPlaylistImageParameters;
const deleteArtistImage = deletePlaylistImage;
const deleteInternetRadioStationImage = deletePlaylistImage; const deleteInternetRadioStationImage = deletePlaylistImage;
const deletePlaylist = z.null(); const deletePlaylist = z.null();
@@ -813,6 +817,7 @@ export const ndType = {
tagList: tagListParameters, tagList: tagListParameters,
updateInternetRadioStation: updateInternetRadioStationParameters, updateInternetRadioStation: updateInternetRadioStationParameters,
updatePlaylist: updatePlaylistParameters, updatePlaylist: updatePlaylistParameters,
uploadArtistImage: uploadArtistImageParameters,
uploadInternetRadioStationImage: uploadInternetRadioStationImageParameters, uploadInternetRadioStationImage: uploadInternetRadioStationImageParameters,
uploadPlaylistImage: uploadPlaylistImageParameters, uploadPlaylistImage: uploadPlaylistImageParameters,
userList: userListParameters, userList: userListParameters,
@@ -825,6 +830,7 @@ export const ndType = {
albumList, albumList,
authenticate, authenticate,
createPlaylist, createPlaylist,
deleteArtistImage,
deleteInternetRadioStation, deleteInternetRadioStation,
deleteInternetRadioStationImage, deleteInternetRadioStationImage,
deletePlaylist, deletePlaylist,
@@ -848,6 +854,7 @@ export const ndType = {
tagList, tagList,
updateInternetRadioStation, updateInternetRadioStation,
updatePlaylist, updatePlaylist,
uploadArtistImage,
uploadInternetRadioStationImage, uploadInternetRadioStationImage,
uploadPlaylistImage, uploadPlaylistImage,
user, user,
+34
View File
@@ -225,6 +225,7 @@ export type AlbumArtist = {
playCount: null | number; playCount: null | number;
similarArtists: null | RelatedArtist[]; similarArtists: null | RelatedArtist[];
songCount: null | number; songCount: null | number;
uploadedImage?: string;
userFavorite: boolean; userFavorite: boolean;
userRating: null | number; userRating: null | number;
}; };
@@ -957,6 +958,16 @@ export type CreatePlaylistBody = {
// Create Playlist // Create Playlist
export type CreatePlaylistResponse = undefined | { id: string }; export type CreatePlaylistResponse = undefined | { id: string };
export type DeleteArtistImageArgs = BaseEndpointArgs & {
query: DeleteArtistImageQuery;
};
export type DeleteArtistImageQuery = {
id: string;
};
export type DeleteArtistImageResponse = boolean;
export type DeleteInternetRadioStationArgs = BaseEndpointArgs & { export type DeleteInternetRadioStationArgs = BaseEndpointArgs & {
query: DeleteInternetRadioStationQuery; query: DeleteInternetRadioStationQuery;
}; };
@@ -1132,6 +1143,21 @@ export type UpdatePlaylistQuery = {
// Update Playlist // Update Playlist
export type UpdatePlaylistResponse = null | undefined; export type UpdatePlaylistResponse = null | undefined;
export type UploadArtistImageArgs = BaseEndpointArgs & {
body: UploadArtistImageBody;
query: UploadArtistImageQuery;
};
export type UploadArtistImageBody = {
image: Uint8Array;
};
export type UploadArtistImageQuery = {
id: string;
};
export type UploadArtistImageResponse = boolean;
export type UploadInternetRadioStationImageArgs = BaseEndpointArgs & { export type UploadInternetRadioStationImageArgs = BaseEndpointArgs & {
body: UploadInternetRadioStationImageBody; body: UploadInternetRadioStationImageBody;
query: UploadInternetRadioStationImageQuery; query: UploadInternetRadioStationImageQuery;
@@ -1441,6 +1467,7 @@ export type ControllerEndpoint = {
args: CreateInternetRadioStationArgs, args: CreateInternetRadioStationArgs,
) => Promise<CreateInternetRadioStationResponse>; ) => Promise<CreateInternetRadioStationResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>; createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteArtistImage?: (args: DeleteArtistImageArgs) => Promise<DeleteArtistImageResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>; deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deleteInternetRadioStation: ( deleteInternetRadioStation: (
args: DeleteInternetRadioStationArgs, args: DeleteInternetRadioStationArgs,
@@ -1503,6 +1530,7 @@ export type ControllerEndpoint = {
args: UpdateInternetRadioStationArgs, args: UpdateInternetRadioStationArgs,
) => Promise<UpdateInternetRadioStationResponse>; ) => Promise<UpdateInternetRadioStationResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>; updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
uploadArtistImage?: (args: UploadArtistImageArgs) => Promise<UploadArtistImageResponse>;
uploadInternetRadioStationImage?: ( uploadInternetRadioStationImage?: (
args: UploadInternetRadioStationImageArgs, args: UploadInternetRadioStationImageArgs,
) => Promise<UploadInternetRadioStationImageResponse>; ) => Promise<UploadInternetRadioStationImageResponse>;
@@ -1572,6 +1600,9 @@ export type InternalControllerEndpoint = {
createPlaylist: ( createPlaylist: (
args: ReplaceApiClientProps<CreatePlaylistArgs>, args: ReplaceApiClientProps<CreatePlaylistArgs>,
) => Promise<CreatePlaylistResponse>; ) => Promise<CreatePlaylistResponse>;
deleteArtistImage?: (
args: ReplaceApiClientProps<DeleteArtistImageArgs>,
) => Promise<DeleteArtistImageResponse>;
deleteFavorite: (args: ReplaceApiClientProps<FavoriteArgs>) => Promise<FavoriteResponse>; deleteFavorite: (args: ReplaceApiClientProps<FavoriteArgs>) => Promise<FavoriteResponse>;
deleteInternetRadioStation: ( deleteInternetRadioStation: (
args: ReplaceApiClientProps<DeleteInternetRadioStationArgs>, args: ReplaceApiClientProps<DeleteInternetRadioStationArgs>,
@@ -1669,6 +1700,9 @@ export type InternalControllerEndpoint = {
updatePlaylist: ( updatePlaylist: (
args: ReplaceApiClientProps<UpdatePlaylistArgs>, args: ReplaceApiClientProps<UpdatePlaylistArgs>,
) => Promise<UpdatePlaylistResponse>; ) => Promise<UpdatePlaylistResponse>;
uploadArtistImage?: (
args: ReplaceApiClientProps<UploadArtistImageArgs>,
) => Promise<UploadArtistImageResponse>;
uploadInternetRadioStationImage?: ( uploadInternetRadioStationImage?: (
args: ReplaceApiClientProps<UploadInternetRadioStationImageArgs>, args: ReplaceApiClientProps<UploadInternetRadioStationImageArgs>,
) => Promise<UploadInternetRadioStationImageResponse>; ) => Promise<UploadInternetRadioStationImageResponse>;
+1
View File
@@ -2,6 +2,7 @@
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART" // For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
export enum ServerFeature { export enum ServerFeature {
ALBUM_YES_NO_RATING_FILTER = 'albumYesNoRatingFilter', ALBUM_YES_NO_RATING_FILTER = 'albumYesNoRatingFilter',
ARTIST_IMAGE_UPLOAD = 'artistImageUpload',
BFR = 'bfr', BFR = 'bfr',
INTERNET_RADIO_IMAGE_UPLOAD = 'internetRadioImageUpload', INTERNET_RADIO_IMAGE_UPLOAD = 'internetRadioImageUpload',
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',