fix jellyfin playlists, add is public

This commit is contained in:
Kendall Garner 2024-09-01 09:37:37 -07:00
parent 528bef01f0
commit 93377dcc4f
No known key found for this signature in database
GPG key ID: 18D2767419676C87
11 changed files with 81 additions and 51 deletions

View file

@ -249,6 +249,8 @@
"title": "delete $t(entity.playlist_one)" "title": "delete $t(entity.playlist_one)"
}, },
"editPlaylist": { "editPlaylist": {
"publicJellyfinNote": "Jellyfin for some reason does not expose whether a playlist is public or not. If you wish for this to remain public, please have the following input selected",
"success": "$t(entity.playlist_one) updated successfully",
"title": "edit $t(entity.playlist_one)" "title": "edit $t(entity.playlist_one)"
}, },
"lyricSearch": { "lyricSearch": {

View file

@ -4,6 +4,7 @@ export enum ServerFeature {
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured', LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
PLAYLISTS_SMART = 'playlistsSmart', PLAYLISTS_SMART = 'playlistsSmart',
PUBLIC_PLAYLIST = 'publicPlaylist',
SHARING_ALBUM_SONG = 'sharingAlbumSong', SHARING_ALBUM_SONG = 'sharingAlbumSong',
} }

View file

@ -292,8 +292,8 @@ export const contract = c.router({
}, },
updatePlaylist: { updatePlaylist: {
body: jfType._parameters.updatePlaylist, body: jfType._parameters.updatePlaylist,
method: 'PUT', method: 'POST',
path: 'items/:id', path: 'playlists/:id',
responses: { responses: {
200: jfType._response.updatePlaylist, 200: jfType._response.updatePlaylist,
400: jfType._response.error, 400: jfType._response.error,

View file

@ -607,9 +607,9 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
const res = await jfApiClient(apiClientProps).createPlaylist({ const res = await jfApiClient(apiClientProps).createPlaylist({
body: { body: {
IsPublic: body.public,
MediaType: 'Audio', MediaType: 'Audio',
Name: body.name, Name: body.name,
Overview: body.comment || '',
UserId: apiClientProps.server.userId, UserId: apiClientProps.server.userId,
}, },
}); });
@ -633,9 +633,9 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
const res = await jfApiClient(apiClientProps).updatePlaylist({ const res = await jfApiClient(apiClientProps).updatePlaylist({
body: { body: {
Genres: body.genres?.map((item) => ({ Id: item.id, Name: item.name })) || [], Genres: body.genres?.map((item) => ({ Id: item.id, Name: item.name })) || [],
IsPublic: body.public,
MediaType: 'Audio', MediaType: 'Audio',
Name: body.name, Name: body.name,
Overview: body.comment || '',
PremiereDate: null, PremiereDate: null,
ProviderIds: {}, ProviderIds: {},
Tags: [], Tags: [],
@ -646,7 +646,7 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
}, },
}); });
if (res.status !== 200) { if (res.status !== 204) {
throw new Error('Failed to update playlist'); throw new Error('Failed to update playlist');
} }
@ -954,7 +954,12 @@ const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse>
return jfNormalize.song(res.body, apiClientProps.server, ''); return jfNormalize.song(res.body, apiClientProps.server, '');
}; };
const VERSION_INFO: VersionInfo = [['10.9.0', { [ServerFeature.LYRICS_SINGLE_STRUCTURED]: [1] }]]; const VERSION_INFO: VersionInfo = [
[
'10.9.0',
{ [ServerFeature.LYRICS_SINGLE_STRUCTURED]: [1], [ServerFeature.PUBLIC_PLAYLIST]: [1] },
],
];
const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => { const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
const { apiClientProps } = args; const { apiClientProps } = args;

View file

@ -581,9 +581,9 @@ const playlistDetailParameters = baseParameters.extend({
}); });
const createPlaylistParameters = z.object({ const createPlaylistParameters = z.object({
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'), MediaType: z.literal('Audio'),
Name: z.string(), Name: z.string(),
Overview: z.string(),
UserId: z.string(), UserId: z.string(),
}); });
@ -595,9 +595,9 @@ const updatePlaylist = z.null();
const updatePlaylistParameters = z.object({ const updatePlaylistParameters = z.object({
Genres: z.array(genreItem), Genres: z.array(genreItem),
IsPublic: z.boolean().optional(),
MediaType: z.literal('Audio'), MediaType: z.literal('Audio'),
Name: z.string(), Name: z.string(),
Overview: z.string(),
PremiereDate: z.null(), PremiereDate: z.null(),
ProviderIds: z.object({}), ProviderIds: z.object({}),
Tags: z.array(genericItem), Tags: z.array(genericItem),

View file

@ -317,7 +317,7 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
body: { body: {
comment: body.comment, comment: body.comment,
name: body.name, name: body.name,
public: body._custom?.navidrome?.public, public: body.public,
rules: body._custom?.navidrome?.rules, rules: body._custom?.navidrome?.rules,
sync: body._custom?.navidrome?.sync, sync: body._custom?.navidrome?.sync,
}, },
@ -339,7 +339,7 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
body: { body: {
comment: body.comment || '', comment: body.comment || '',
name: body.name, name: body.name,
public: body._custom?.navidrome?.public || false, public: body?.public || false,
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined, rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
sync: body._custom?.navidrome?.sync || undefined, sync: body._custom?.navidrome?.sync || undefined,
}, },
@ -534,6 +534,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise<ServerInfo> => {
const features: ServerFeatures = { const features: ServerFeatures = {
lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS], lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS],
playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART], playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART],
publicPlaylist: true,
sharingAlbumSong: !!navidromeFeatures[ServerFeature.SHARING_ALBUM_SONG], sharingAlbumSong: !!navidromeFeatures[ServerFeature.SHARING_ALBUM_SONG],
}; };

View file

@ -818,13 +818,13 @@ export type CreatePlaylistBody = {
navidrome?: { navidrome?: {
owner?: string; owner?: string;
ownerId?: string; ownerId?: string;
public?: boolean;
rules?: Record<string, any>; rules?: Record<string, any>;
sync?: boolean; sync?: boolean;
}; };
}; };
comment?: string; comment?: string;
name: string; name: string;
public?: boolean;
}; };
export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs; export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs;
@ -841,7 +841,6 @@ export type UpdatePlaylistBody = {
navidrome?: { navidrome?: {
owner?: string; owner?: string;
ownerId?: string; ownerId?: string;
public?: boolean;
rules?: Record<string, any>; rules?: Record<string, any>;
sync?: boolean; sync?: boolean;
}; };
@ -849,6 +848,7 @@ export type UpdatePlaylistBody = {
comment?: string; comment?: string;
genres?: Genre[]; genres?: Genre[];
name: string; name: string;
public?: boolean;
}; };
export type UpdatePlaylistArgs = { export type UpdatePlaylistArgs = {

View file

@ -28,7 +28,6 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
initialValues: { initialValues: {
_custom: { _custom: {
navidrome: { navidrome: {
public: false,
rules: undefined, rules: undefined,
}, },
}, },
@ -88,7 +87,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
); );
}); });
const isPublicDisplayed = server?.type === ServerType.NAVIDROME; const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isLoading;
return ( return (
@ -103,6 +102,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
})} })}
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
{server?.type === ServerType.NAVIDROME && (
<TextInput <TextInput
label={t('form.createPlaylist.input', { label={t('form.createPlaylist.input', {
context: 'description', context: 'description',
@ -110,6 +110,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
})} })}
{...form.getInputProps('comment')} {...form.getInputProps('comment')}
/> />
)}
<Group> <Group>
{isPublicDisplayed && ( {isPublicDisplayed && (
<Switch <Switch
@ -117,7 +118,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
context: 'public', context: 'public',
postProcess: 'titleCase', postProcess: 'titleCase',
})} })}
{...form.getInputProps('_custom.navidrome.public', { {...form.getInputProps('public', {
type: 'checkbox', type: 'checkbox',
})} })}
/> />

View file

@ -5,6 +5,8 @@ import { Button, Switch, TextInput, toast } from '/@/renderer/components';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ServerFeature } from '/@/renderer/api/features-types';
import { hasFeature } from '/@/renderer/api/utils';
interface SaveAsPlaylistFormProps { interface SaveAsPlaylistFormProps {
body: Partial<CreatePlaylistBody>; body: Partial<CreatePlaylistBody>;
@ -27,13 +29,13 @@ export const SaveAsPlaylistForm = ({
initialValues: { initialValues: {
_custom: { _custom: {
navidrome: { navidrome: {
public: false,
rules: undefined, rules: undefined,
...body?._custom?.navidrome, ...body?._custom?.navidrome,
}, },
}, },
comment: body.comment || '', comment: body.comment || '',
name: body.name || '', name: body.name || '',
public: body.public,
}, },
}); });
@ -58,7 +60,7 @@ export const SaveAsPlaylistForm = ({
); );
}); });
const isPublicDisplayed = server?.type === ServerType.NAVIDROME; const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isLoading;
return ( return (
@ -73,6 +75,7 @@ export const SaveAsPlaylistForm = ({
})} })}
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
{server?.type === ServerType.NAVIDROME && (
<TextInput <TextInput
label={t('form.createPlaylist.input', { label={t('form.createPlaylist.input', {
context: 'description', context: 'description',
@ -80,13 +83,14 @@ export const SaveAsPlaylistForm = ({
})} })}
{...form.getInputProps('comment')} {...form.getInputProps('comment')}
/> />
)}
{isPublicDisplayed && ( {isPublicDisplayed && (
<Switch <Switch
label={t('form.createPlaylist.input', { label={t('form.createPlaylist.input', {
context: 'public', context: 'public',
postProcess: 'titleCase', postProcess: 'titleCase',
})} })}
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })} {...form.getInputProps('public', { type: 'checkbox' })}
/> />
)} )}
<Group position="right"> <Group position="right">

View file

@ -20,6 +20,8 @@ import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features-types';
interface UpdatePlaylistFormProps { interface UpdatePlaylistFormProps {
body: Partial<UpdatePlaylistBody>; body: Partial<UpdatePlaylistBody>;
@ -44,13 +46,13 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
navidrome: { navidrome: {
owner: body?._custom?.navidrome?.owner || '', owner: body?._custom?.navidrome?.owner || '',
ownerId: body?._custom?.navidrome?.ownerId || '', ownerId: body?._custom?.navidrome?.ownerId || '',
public: body?._custom?.navidrome?.public || false,
rules: undefined, rules: undefined,
sync: body?._custom?.navidrome?.sync || false, sync: body?._custom?.navidrome?.sync || false,
}, },
}, },
comment: body?.comment || '', comment: body?.comment || '',
name: body?.name || '', name: body?.name || '',
public: body.public,
}, },
}); });
@ -69,13 +71,16 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
}); });
}, },
onSuccess: () => { onSuccess: () => {
toast.success({
message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }),
});
onCancel(); onCancel();
}, },
}, },
); );
}); });
const isPublicDisplayed = server?.type === ServerType.NAVIDROME; const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList; const isOwnerDisplayed = server?.type === ServerType.NAVIDROME && userList;
const isSubmitDisabled = !form.values.name || mutation.isLoading; const isSubmitDisabled = !form.values.name || mutation.isLoading;
@ -91,6 +96,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
})} })}
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
{server?.type === ServerType.NAVIDROME && (
<TextInput <TextInput
label={t('form.createPlaylist.input', { label={t('form.createPlaylist.input', {
context: 'description', context: 'description',
@ -98,6 +104,7 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
})} })}
{...form.getInputProps('comment')} {...form.getInputProps('comment')}
/> />
)}
{isOwnerDisplayed && ( {isOwnerDisplayed && (
<Select <Select
data={userList || []} data={userList || []}
@ -109,13 +116,22 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
/> />
)} )}
{isPublicDisplayed && ( {isPublicDisplayed && (
<>
{server?.type === ServerType.JELLYFIN && (
<div>
{t('form.editPlaylist.publicJellyfinNote', {
postProcess: 'sentenceCase',
})}
</div>
)}
<Switch <Switch
label={t('form.createPlaylist.input', { label={t('form.createPlaylist.input', {
context: 'public', context: 'public',
postProcess: 'titleCase', postProcess: 'titleCase',
})} })}
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })} {...form.getInputProps('public', { type: 'checkbox' })}
/> />
</>
)} )}
<Group position="right"> <Group position="right">
<Button <Button
@ -175,7 +191,6 @@ export const openUpdatePlaylistModal = async (args: {
navidrome: { navidrome: {
owner: playlist?.owner || undefined, owner: playlist?.owner || undefined,
ownerId: playlist?.ownerId || undefined, ownerId: playlist?.ownerId || undefined,
public: playlist?.public || false,
rules: playlist?.rules || undefined, rules: playlist?.rules || undefined,
sync: playlist?.sync || undefined, sync: playlist?.sync || undefined,
}, },
@ -183,6 +198,7 @@ export const openUpdatePlaylistModal = async (args: {
comment: playlist?.description || undefined, comment: playlist?.description || undefined,
genres: playlist?.genres, genres: playlist?.genres,
name: playlist?.name, name: playlist?.name,
public: playlist?.public || false,
}} }}
query={{ id: playlist?.id }} query={{ id: playlist?.id }}
users={users?.items} users={users?.items}

View file

@ -50,13 +50,13 @@ const PlaylistDetailSongListRoute = () => {
navidrome: { navidrome: {
owner: detailQuery?.data?.owner || '', owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '', ownerId: detailQuery?.data?.ownerId || '',
public: detailQuery?.data?.public || false,
rules, rules,
sync: detailQuery?.data?.sync || false, sync: detailQuery?.data?.sync || false,
}, },
}, },
comment: detailQuery?.data?.description || '', comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name, name: detailQuery?.data?.name,
public: detailQuery?.data?.public || false,
}, },
serverId: detailQuery?.data?.serverId, serverId: detailQuery?.data?.serverId,
}, },
@ -92,7 +92,6 @@ const PlaylistDetailSongListRoute = () => {
navidrome: { navidrome: {
owner: detailQuery?.data?.owner || '', owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '', ownerId: detailQuery?.data?.ownerId || '',
public: detailQuery?.data?.public || false,
rules: { rules: {
...filter, ...filter,
limit: extraFilters.limit || undefined, limit: extraFilters.limit || undefined,
@ -104,6 +103,7 @@ const PlaylistDetailSongListRoute = () => {
}, },
comment: detailQuery?.data?.description || '', comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name, name: detailQuery?.data?.name,
public: detailQuery?.data?.public || false,
}} }}
serverId={detailQuery?.data?.serverId} serverId={detailQuery?.data?.serverId}
onCancel={closeAllModals} onCancel={closeAllModals}