Add owner to playlist update query
- Support smart playlist rules - Add user list query
This commit is contained in:
parent
75ef43dffb
commit
d63e5f5784
13 changed files with 309 additions and 59 deletions
|
@ -35,6 +35,8 @@ import type {
|
||||||
RawArtistListResponse,
|
RawArtistListResponse,
|
||||||
UpdatePlaylistArgs,
|
UpdatePlaylistArgs,
|
||||||
RawUpdatePlaylistResponse,
|
RawUpdatePlaylistResponse,
|
||||||
|
UserListArgs,
|
||||||
|
RawUserListResponse,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
||||||
import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
|
import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
|
||||||
|
@ -62,6 +64,7 @@ export type ControllerEndpoint = Partial<{
|
||||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
|
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
|
||||||
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
|
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
|
||||||
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
|
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
|
||||||
|
getUserList: (args: UserListArgs) => Promise<RawUserListResponse>;
|
||||||
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<RawUpdatePlaylistResponse>;
|
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<RawUpdatePlaylistResponse>;
|
||||||
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
|
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
|
||||||
}>;
|
}>;
|
||||||
|
@ -96,6 +99,7 @@ const endpoints: ApiController = {
|
||||||
getPlaylistSongList: jellyfinApi.getPlaylistSongList,
|
getPlaylistSongList: jellyfinApi.getPlaylistSongList,
|
||||||
getSongDetail: undefined,
|
getSongDetail: undefined,
|
||||||
getSongList: jellyfinApi.getSongList,
|
getSongList: jellyfinApi.getSongList,
|
||||||
|
getUserList: undefined,
|
||||||
updatePlaylist: jellyfinApi.updatePlaylist,
|
updatePlaylist: jellyfinApi.updatePlaylist,
|
||||||
updateRating: undefined,
|
updateRating: undefined,
|
||||||
},
|
},
|
||||||
|
@ -122,6 +126,7 @@ const endpoints: ApiController = {
|
||||||
getPlaylistSongList: navidromeApi.getPlaylistSongList,
|
getPlaylistSongList: navidromeApi.getPlaylistSongList,
|
||||||
getSongDetail: navidromeApi.getSongDetail,
|
getSongDetail: navidromeApi.getSongDetail,
|
||||||
getSongList: navidromeApi.getSongList,
|
getSongList: navidromeApi.getSongList,
|
||||||
|
getUserList: navidromeApi.getUserList,
|
||||||
updatePlaylist: navidromeApi.updatePlaylist,
|
updatePlaylist: navidromeApi.updatePlaylist,
|
||||||
updateRating: subsonicApi.updateRating,
|
updateRating: subsonicApi.updateRating,
|
||||||
},
|
},
|
||||||
|
@ -147,6 +152,7 @@ const endpoints: ApiController = {
|
||||||
getPlaylistList: undefined,
|
getPlaylistList: undefined,
|
||||||
getSongDetail: undefined,
|
getSongDetail: undefined,
|
||||||
getSongList: undefined,
|
getSongList: undefined,
|
||||||
|
getUserList: undefined,
|
||||||
updatePlaylist: undefined,
|
updatePlaylist: undefined,
|
||||||
updateRating: undefined,
|
updateRating: undefined,
|
||||||
},
|
},
|
||||||
|
@ -227,6 +233,10 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUserList = async (args: UserListArgs) => {
|
||||||
|
return (apiController('getUserList') as ControllerEndpoint['getUserList'])?.(args);
|
||||||
|
};
|
||||||
|
|
||||||
export const controller = {
|
export const controller = {
|
||||||
createPlaylist,
|
createPlaylist,
|
||||||
deletePlaylist,
|
deletePlaylist,
|
||||||
|
@ -240,5 +250,6 @@ export const controller = {
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
getPlaylistSongList,
|
getPlaylistSongList,
|
||||||
getSongList,
|
getSongList,
|
||||||
|
getUserList,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
};
|
};
|
||||||
|
|
|
@ -433,26 +433,26 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<JFPlaylistList>
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||||
const { query, server } = args;
|
const { body, server } = args;
|
||||||
|
|
||||||
const body = {
|
const json = {
|
||||||
MediaType: 'Audio',
|
MediaType: 'Audio',
|
||||||
Name: query.name,
|
Name: body.name,
|
||||||
Overview: query.comment || '',
|
Overview: body.comment || '',
|
||||||
UserId: server?.userId,
|
UserId: server?.userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await api
|
const data = await api
|
||||||
.post('playlists', {
|
.post('playlists', {
|
||||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||||
json: body,
|
json,
|
||||||
prefixUrl: server?.url,
|
prefixUrl: server?.url,
|
||||||
})
|
})
|
||||||
.json<JFCreatePlaylistResponse>();
|
.json<JFCreatePlaylistResponse>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: data.Id,
|
id: data.Id,
|
||||||
name: query.name,
|
name: body.name,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -760,12 +760,13 @@ const normalizePlaylist = (
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl: imageUrl || null,
|
imageUrl: imageUrl || null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
|
owner: null,
|
||||||
|
ownerId: null,
|
||||||
public: null,
|
public: null,
|
||||||
rules: null,
|
rules: null,
|
||||||
size: null,
|
size: null,
|
||||||
songCount: item?.ChildCount || null,
|
songCount: item?.ChildCount || null,
|
||||||
userId: null,
|
sync: null,
|
||||||
username: null,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,13 @@ import type {
|
||||||
NDPlaylistSongListResponse,
|
NDPlaylistSongListResponse,
|
||||||
NDPlaylistSongList,
|
NDPlaylistSongList,
|
||||||
NDPlaylistSong,
|
NDPlaylistSong,
|
||||||
|
NDUserList,
|
||||||
|
NDUserListResponse,
|
||||||
|
NDUserListParams,
|
||||||
|
NDUser,
|
||||||
} from '/@/renderer/api/navidrome.types';
|
} from '/@/renderer/api/navidrome.types';
|
||||||
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
import { NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
||||||
import type {
|
import {
|
||||||
Album,
|
Album,
|
||||||
Song,
|
Song,
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
|
@ -60,13 +64,14 @@ import type {
|
||||||
Playlist,
|
Playlist,
|
||||||
UpdatePlaylistResponse,
|
UpdatePlaylistResponse,
|
||||||
UpdatePlaylistArgs,
|
UpdatePlaylistArgs,
|
||||||
} from '/@/renderer/api/types';
|
UserListArgs,
|
||||||
import {
|
userListSortMap,
|
||||||
playlistListSortMap,
|
playlistListSortMap,
|
||||||
albumArtistListSortMap,
|
albumArtistListSortMap,
|
||||||
songListSortMap,
|
songListSortMap,
|
||||||
albumListSortMap,
|
albumListSortMap,
|
||||||
sortOrderMap,
|
sortOrderMap,
|
||||||
|
User,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { toast } from '/@/renderer/components/toast';
|
import { toast } from '/@/renderer/components/toast';
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
|
@ -132,6 +137,34 @@ const authenticate = async (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUserList = async (args: UserListArgs): Promise<NDUserList> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
const searchParams: NDUserListParams = {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: userListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
...query.ndParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await api.get('api/user', {
|
||||||
|
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||||
|
prefixUrl: server?.url,
|
||||||
|
searchParams: parseSearchParams(searchParams),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json<NDUserListResponse>();
|
||||||
|
const itemCount = res.headers.get('x-total-count');
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: data,
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(itemCount),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getGenreList = async (args: GenreListArgs): Promise<NDGenreList> => {
|
const getGenreList = async (args: GenreListArgs): Promise<NDGenreList> => {
|
||||||
const { server, signal } = args;
|
const { server, signal } = args;
|
||||||
|
|
||||||
|
@ -293,12 +326,14 @@ const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||||
const { query, server } = args;
|
const { body, server } = args;
|
||||||
|
|
||||||
const json: NDCreatePlaylistParams = {
|
const json: NDCreatePlaylistParams = {
|
||||||
comment: query.comment,
|
comment: body.comment,
|
||||||
name: query.name,
|
name: body.name,
|
||||||
public: query.public || false,
|
...body.ndParams,
|
||||||
|
public: body.ndParams?.public || false,
|
||||||
|
rules: body.ndParams?.rules ? body.ndParams.rules : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await api
|
const data = await api
|
||||||
|
@ -311,7 +346,7 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: query.name,
|
name: body.name,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -321,7 +356,11 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
|
||||||
const json: NDUpdatePlaylistParams = {
|
const json: NDUpdatePlaylistParams = {
|
||||||
comment: body.comment || '',
|
comment: body.comment || '',
|
||||||
name: body.name,
|
name: body.name,
|
||||||
public: body.public || false,
|
ownerId: body.ndParams?.ownerId || undefined,
|
||||||
|
ownerName: body.ndParams?.owner || undefined,
|
||||||
|
public: body.ndParams?.public || false,
|
||||||
|
rules: body.ndParams?.rules ? body.ndParams?.rules : undefined,
|
||||||
|
sync: body.ndParams?.sync || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await api
|
const data = await api
|
||||||
|
@ -357,8 +396,8 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<NDPlaylistList>
|
||||||
|
|
||||||
const searchParams: NDPlaylistListParams = {
|
const searchParams: NDPlaylistListParams = {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : NDSortOrder.ASC,
|
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : undefined,
|
||||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : NDPlaylistListSort.NAME,
|
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
...query.ndParams,
|
...query.ndParams,
|
||||||
};
|
};
|
||||||
|
@ -583,12 +622,25 @@ const normalizePlaylist = (
|
||||||
imagePlaceholderUrl,
|
imagePlaceholderUrl,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
owner: item.ownerName,
|
||||||
|
ownerId: item.ownerId,
|
||||||
public: item.public,
|
public: item.public,
|
||||||
rules: item?.rules || null,
|
rules: item?.rules || null,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
userId: item.ownerId,
|
sync: item.sync,
|
||||||
username: item.ownerName,
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeUser = (item: NDUser): User => {
|
||||||
|
return {
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
email: item.email,
|
||||||
|
id: item.id,
|
||||||
|
isAdmin: item.isAdmin,
|
||||||
|
lastLoginAt: item.lastLoginAt,
|
||||||
|
name: item.userName,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -606,6 +658,7 @@ export const navidromeApi = {
|
||||||
getPlaylistSongList,
|
getPlaylistSongList,
|
||||||
getSongDetail,
|
getSongDetail,
|
||||||
getSongList,
|
getSongList,
|
||||||
|
getUserList,
|
||||||
updatePlaylist,
|
updatePlaylist,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -614,4 +667,5 @@ export const ndNormalize = {
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
playlist: normalizePlaylist,
|
playlist: normalizePlaylist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
|
user: normalizeUser,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,18 @@ export type NDAuthenticate = {
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NDUser = {
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
id: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
lastAccessAt: string;
|
||||||
|
lastLoginAt: string;
|
||||||
|
name: string;
|
||||||
|
updatedAt: string;
|
||||||
|
userName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type NDGenre = {
|
export type NDGenre = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -376,3 +388,20 @@ export const NDSongQueryFields = [
|
||||||
{ label: 'Play count', value: 'playcount' },
|
{ label: 'Play count', value: 'playcount' },
|
||||||
{ label: 'Rating', value: 'rating' },
|
{ label: 'Rating', value: 'rating' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export type NDUserListParams = {
|
||||||
|
_sort?: NDUserListSort;
|
||||||
|
} & NDPagination &
|
||||||
|
NDOrder;
|
||||||
|
|
||||||
|
export type NDUserListResponse = NDUser[];
|
||||||
|
|
||||||
|
export type NDUserList = {
|
||||||
|
items: NDUser[];
|
||||||
|
startIndex: number;
|
||||||
|
totalRecordCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum NDUserListSort {
|
||||||
|
NAME = 'name',
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
NDGenreList,
|
NDGenreList,
|
||||||
NDPlaylist,
|
NDPlaylist,
|
||||||
NDSong,
|
NDSong,
|
||||||
|
NDUser,
|
||||||
} from '/@/renderer/api/navidrome.types';
|
} from '/@/renderer/api/navidrome.types';
|
||||||
import { SSGenreList, SSMusicFolderList } from '/@/renderer/api/subsonic.types';
|
import { SSGenreList, SSMusicFolderList } from '/@/renderer/api/subsonic.types';
|
||||||
import type {
|
import type {
|
||||||
|
@ -26,6 +27,7 @@ import type {
|
||||||
RawPlaylistDetailResponse,
|
RawPlaylistDetailResponse,
|
||||||
RawPlaylistListResponse,
|
RawPlaylistListResponse,
|
||||||
RawSongListResponse,
|
RawSongListResponse,
|
||||||
|
RawUserListResponse,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { ServerListItem } from '/@/renderer/types';
|
import { ServerListItem } from '/@/renderer/types';
|
||||||
|
|
||||||
|
@ -211,6 +213,25 @@ const playlistDetail = (
|
||||||
return playlist;
|
return playlist;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userList = (data: RawUserListResponse | undefined, server: ServerListItem | null) => {
|
||||||
|
let users;
|
||||||
|
switch (server?.type) {
|
||||||
|
case 'jellyfin':
|
||||||
|
break;
|
||||||
|
case 'navidrome':
|
||||||
|
users = data?.items.map((item) => ndNormalize.user(item as NDUser));
|
||||||
|
break;
|
||||||
|
case 'subsonic':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: users,
|
||||||
|
startIndex: data?.startIndex,
|
||||||
|
totalRecordCount: data?.totalRecordCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const normalize = {
|
export const normalize = {
|
||||||
albumArtistList,
|
albumArtistList,
|
||||||
albumDetail,
|
albumDetail,
|
||||||
|
@ -220,4 +241,5 @@ export const normalize = {
|
||||||
playlistDetail,
|
playlistDetail,
|
||||||
playlistList,
|
playlistList,
|
||||||
songList,
|
songList,
|
||||||
|
userList,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
PlaylistListQuery,
|
PlaylistListQuery,
|
||||||
PlaylistDetailQuery,
|
PlaylistDetailQuery,
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
|
UserListQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
|
@ -79,4 +80,11 @@ export const queryKeys = {
|
||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'songs'] as const,
|
root: (serverId: string) => [serverId, 'songs'] as const,
|
||||||
},
|
},
|
||||||
|
users: {
|
||||||
|
list: (serverId: string, query?: UserListQuery) => {
|
||||||
|
if (query) return [serverId, 'users', 'list', query] as const;
|
||||||
|
return [serverId, 'users', 'list'] as const;
|
||||||
|
},
|
||||||
|
root: (serverId: string) => [serverId, 'users'] as const,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,6 +33,8 @@ import {
|
||||||
NDPlaylistListSort,
|
NDPlaylistListSort,
|
||||||
NDPlaylistDetail,
|
NDPlaylistDetail,
|
||||||
NDSongListSort,
|
NDSongListSort,
|
||||||
|
NDUserList,
|
||||||
|
NDUserListSort,
|
||||||
} from '/@/renderer/api/navidrome.types';
|
} from '/@/renderer/api/navidrome.types';
|
||||||
import {
|
import {
|
||||||
SSAlbumList,
|
SSAlbumList,
|
||||||
|
@ -48,6 +50,16 @@ export enum SortOrder {
|
||||||
DESC = 'DESC',
|
DESC = 'DESC',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
createdAt: string | null;
|
||||||
|
email: string | null;
|
||||||
|
id: string;
|
||||||
|
isAdmin: boolean | null;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
name: string;
|
||||||
|
updatedAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ServerListItem = {
|
export type ServerListItem = {
|
||||||
credential: string;
|
credential: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -242,12 +254,13 @@ export type Playlist = {
|
||||||
imagePlaceholderUrl: string | null;
|
imagePlaceholderUrl: string | null;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
owner: string | null;
|
||||||
|
ownerId: string | null;
|
||||||
public: boolean | null;
|
public: boolean | null;
|
||||||
rules?: Record<string, any> | null;
|
rules?: Record<string, any> | null;
|
||||||
size: number | null;
|
size: number | null;
|
||||||
songCount: number | null;
|
songCount: number | null;
|
||||||
userId: string | null;
|
sync?: boolean | null;
|
||||||
username: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GenresResponse = Genre[];
|
export type GenresResponse = Genre[];
|
||||||
|
@ -739,14 +752,19 @@ export type RawCreatePlaylistResponse = CreatePlaylistResponse | undefined;
|
||||||
|
|
||||||
export type CreatePlaylistResponse = { id: string; name: string };
|
export type CreatePlaylistResponse = { id: string; name: string };
|
||||||
|
|
||||||
export type CreatePlaylistQuery = {
|
export type CreatePlaylistBody = {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
ndParams?: {
|
||||||
|
owner?: string;
|
||||||
|
ownerId?: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
rules?: Record<string, any>;
|
rules?: Record<string, any>;
|
||||||
|
sync?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
|
export type CreatePlaylistArgs = { body: CreatePlaylistBody } & BaseEndpointArgs;
|
||||||
|
|
||||||
// Update Playlist
|
// Update Playlist
|
||||||
export type RawUpdatePlaylistResponse = UpdatePlaylistResponse | undefined;
|
export type RawUpdatePlaylistResponse = UpdatePlaylistResponse | undefined;
|
||||||
|
@ -761,8 +779,13 @@ export type UpdatePlaylistBody = {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
genres?: Genre[];
|
genres?: Genre[];
|
||||||
name: string;
|
name: string;
|
||||||
|
ndParams?: {
|
||||||
|
owner?: string;
|
||||||
|
ownerId?: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
rules?: Record<string, any>;
|
rules?: Record<string, any>;
|
||||||
|
sync?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdatePlaylistArgs = {
|
export type UpdatePlaylistArgs = {
|
||||||
|
@ -880,3 +903,44 @@ export type CreateFavoriteResponse = { id: string };
|
||||||
export type CreateFavoriteQuery = { comment?: string; name: string; public?: boolean };
|
export type CreateFavoriteQuery = { comment?: string; name: string; public?: boolean };
|
||||||
|
|
||||||
export type CreateFavoriteArgs = { query: CreateFavoriteQuery } & BaseEndpointArgs;
|
export type CreateFavoriteArgs = { query: CreateFavoriteQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
// User list
|
||||||
|
// Playlist List
|
||||||
|
export type RawUserListResponse = NDUserList | undefined;
|
||||||
|
|
||||||
|
export type UserListResponse = BasePaginatedResponse<User[]>;
|
||||||
|
|
||||||
|
export enum UserListSort {
|
||||||
|
NAME = 'name',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserListQuery = {
|
||||||
|
limit?: number;
|
||||||
|
ndParams?: {
|
||||||
|
owner_id?: string;
|
||||||
|
};
|
||||||
|
searchTerm?: string;
|
||||||
|
sortBy: UserListSort;
|
||||||
|
sortOrder: SortOrder;
|
||||||
|
startIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserListArgs = { query: UserListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
type UserListSortMap = {
|
||||||
|
jellyfin: Record<UserListSort, undefined>;
|
||||||
|
navidrome: Record<UserListSort, NDUserListSort | undefined>;
|
||||||
|
subsonic: Record<UserListSort, undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userListSortMap: UserListSortMap = {
|
||||||
|
jellyfin: {
|
||||||
|
name: undefined,
|
||||||
|
},
|
||||||
|
navidrome: {
|
||||||
|
name: NDUserListSort.NAME,
|
||||||
|
},
|
||||||
|
subsonic: {
|
||||||
|
name: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { CreatePlaylistQuery, ServerType } from '/@/renderer/api/types';
|
import { CreatePlaylistBody, ServerType } from '/@/renderer/api/types';
|
||||||
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
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';
|
||||||
|
@ -13,18 +13,20 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||||
const mutation = useCreatePlaylist();
|
const mutation = useCreatePlaylist();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const form = useForm<CreatePlaylistQuery>({
|
const form = useForm<CreatePlaylistBody>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
comment: '',
|
comment: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
ndParams: {
|
||||||
public: false,
|
public: false,
|
||||||
rules: undefined,
|
rules: undefined,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = form.onSubmit((values) => {
|
const handleSubmit = form.onSubmit((values) => {
|
||||||
mutation.mutate(
|
mutation.mutate(
|
||||||
{ query: values },
|
{ body: values },
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error({ message: err.message, title: 'Error creating playlist' });
|
toast.error({ message: err.message, title: 'Error creating playlist' });
|
||||||
|
@ -56,7 +58,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||||
{isPublicDisplayed && (
|
{isPublicDisplayed && (
|
||||||
<Switch
|
<Switch
|
||||||
label="Is Public?"
|
label="Is Public?"
|
||||||
{...form.getInputProps('public')}
|
{...form.getInputProps('ndParams.public')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Group position="right">
|
<Group position="right">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { forwardRef, Fragment, Ref } from 'react';
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { forwardRef, Fragment, Ref } from 'react';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { RiMoreFill } from 'react-icons/ri';
|
import { RiMoreFill } from 'react-icons/ri';
|
||||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -14,6 +15,10 @@ import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { LibraryItem, Play } from '/@/renderer/types';
|
import { LibraryItem, Play } from '/@/renderer/types';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
|
import { UserListSort, SortOrder, UserListQuery } from '/@/renderer/api/types';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
|
||||||
interface PlaylistDetailHeaderProps {
|
interface PlaylistDetailHeaderProps {
|
||||||
background: string;
|
background: string;
|
||||||
|
@ -27,10 +32,12 @@ export const PlaylistDetailHeader = forwardRef(
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const handlePlay = (playType?: Play) => {
|
const handlePlay = (playType?: Play) => {
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
|
@ -42,7 +49,20 @@ export const PlaylistDetailHeader = forwardRef(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUpdatePlaylistModal = () => {
|
const openUpdatePlaylistModal = async () => {
|
||||||
|
const query: UserListQuery = {
|
||||||
|
sortBy: UserListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await queryClient.fetchQuery({
|
||||||
|
queryFn: ({ signal }) => api.controller.getUserList({ query, server, signal }),
|
||||||
|
queryKey: queryKeys.users.list(server?.id || '', query),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedUsers = api.normalize.userList(users, server);
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<UpdatePlaylistForm
|
<UpdatePlaylistForm
|
||||||
|
@ -50,10 +70,16 @@ export const PlaylistDetailHeader = forwardRef(
|
||||||
comment: detailQuery?.data?.description || undefined,
|
comment: detailQuery?.data?.description || undefined,
|
||||||
genres: detailQuery?.data?.genres,
|
genres: detailQuery?.data?.genres,
|
||||||
name: detailQuery?.data?.name,
|
name: detailQuery?.data?.name,
|
||||||
|
ndParams: {
|
||||||
|
owner: detailQuery?.data?.owner || undefined,
|
||||||
|
ownerId: detailQuery?.data?.ownerId || undefined,
|
||||||
public: detailQuery?.data?.public || false,
|
public: detailQuery?.data?.public || false,
|
||||||
rules: detailQuery?.data?.rules || undefined,
|
rules: detailQuery?.data?.rules || undefined,
|
||||||
|
sync: detailQuery?.data?.sync || undefined,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
query={{ id: playlistId }}
|
query={{ id: playlistId }}
|
||||||
|
users={normalizedUsers.items}
|
||||||
onCancel={closeAllModals}
|
onCancel={closeAllModals}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,27 +1,37 @@
|
||||||
import { Group, Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { ServerType, UpdatePlaylistBody, UpdatePlaylistQuery } from '/@/renderer/api/types';
|
import { ServerType, UpdatePlaylistBody, UpdatePlaylistQuery, User } from '/@/renderer/api/types';
|
||||||
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components';
|
||||||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
interface CreatePlaylistFormProps {
|
interface UpdatePlaylistFormProps {
|
||||||
body: Partial<UpdatePlaylistBody>;
|
body: Partial<UpdatePlaylistBody>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
query: UpdatePlaylistQuery;
|
query: UpdatePlaylistQuery;
|
||||||
|
users?: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePlaylistForm = ({ query, body, onCancel }: CreatePlaylistFormProps) => {
|
export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => {
|
||||||
const mutation = useUpdatePlaylist();
|
const mutation = useUpdatePlaylist();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const userList = users?.map((user) => ({
|
||||||
|
label: user.name,
|
||||||
|
value: user.id,
|
||||||
|
}));
|
||||||
|
|
||||||
const form = useForm<UpdatePlaylistBody>({
|
const form = useForm<UpdatePlaylistBody>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
comment: '',
|
comment: body?.comment || '',
|
||||||
name: '',
|
name: body?.name || '',
|
||||||
public: false,
|
ndParams: {
|
||||||
|
owner: body?.ndParams?.owner || '',
|
||||||
|
ownerId: body?.ndParams?.ownerId || '',
|
||||||
|
public: body?.ndParams?.public || false,
|
||||||
rules: undefined,
|
rules: undefined,
|
||||||
...body,
|
sync: body?.ndParams?.sync || false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,6 +66,11 @@ export const UpdatePlaylistForm = ({ query, body, onCancel }: CreatePlaylistForm
|
||||||
label="Description"
|
label="Description"
|
||||||
{...form.getInputProps('comment')}
|
{...form.getInputProps('comment')}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
data={userList || []}
|
||||||
|
{...form.getInputProps('ndParams.ownerId')}
|
||||||
|
label="Owner"
|
||||||
|
/>
|
||||||
{isPublicDisplayed && (
|
{isPublicDisplayed && (
|
||||||
<Switch
|
<Switch
|
||||||
label="Is Public?"
|
label="Is Public?"
|
||||||
|
|
1
src/renderer/features/users/index.ts
Normal file
1
src/renderer/features/users/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './queries/user-list-query';
|
22
src/renderer/features/users/queries/user-list-query.ts
Normal file
22
src/renderer/features/users/queries/user-list-query.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import type { RawUserListResponse, UserListQuery } from '/@/renderer/api/types';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import type { QueryOptions } from '/@/renderer/lib/react-query';
|
||||||
|
|
||||||
|
export const useUserList = (query: UserListQuery, options?: QueryOptions) => {
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!server?.id,
|
||||||
|
queryFn: ({ signal }) => api.controller.getUserList({ query, server, signal }),
|
||||||
|
queryKey: queryKeys.users.list(server?.id || '', query),
|
||||||
|
select: useCallback(
|
||||||
|
(data: RawUserListResponse | undefined) => api.normalize.userList(data, server),
|
||||||
|
[server],
|
||||||
|
),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -111,22 +111,17 @@ export interface UniqueId {
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FilterGroupType {
|
export type QueryBuilderRule = {
|
||||||
AND = 'AND',
|
|
||||||
OR = 'OR',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdvancedFilterRule = {
|
|
||||||
field?: string | null;
|
field?: string | null;
|
||||||
operator?: string | null;
|
operator?: string | null;
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
value?: string | number | Date | undefined | null | any;
|
value?: string | number | Date | undefined | null | any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdvancedFilterGroup = {
|
export type QueryBuilderGroup = {
|
||||||
group: AdvancedFilterGroup[];
|
group: QueryBuilderGroup[];
|
||||||
rules: AdvancedFilterRule[];
|
rules: QueryBuilderRule[];
|
||||||
type: FilterGroupType;
|
type: 'any' | 'all';
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Reference in a new issue