mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
support navidrome artist image upload/delete
This commit is contained in:
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user