mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 21:10:12 +02:00
add folder browsing support (#315)
This commit is contained in:
@@ -4,6 +4,7 @@ import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Folder,
|
||||
Genre,
|
||||
LibraryItem,
|
||||
MusicFolder,
|
||||
@@ -496,17 +497,20 @@ const normalizeGenre = (
|
||||
};
|
||||
};
|
||||
|
||||
// const normalizeFolder = (item: any) => {
|
||||
// return {
|
||||
// created: item.DateCreated,
|
||||
// id: item.Id,
|
||||
// image: getCoverArtUrl(item, 150),
|
||||
// isDir: true,
|
||||
// title: item.Name,
|
||||
// type: Item.Folder,
|
||||
// uniqueId: nanoid(),
|
||||
// };
|
||||
// };
|
||||
const normalizeFolder = (
|
||||
item: z.infer<typeof jfType._response.folder>,
|
||||
server: null | ServerListItem,
|
||||
): Folder => {
|
||||
return {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
children: undefined,
|
||||
id: item.Id,
|
||||
name: item.Name || 'Unknown folder',
|
||||
parentId: item.ParentId,
|
||||
};
|
||||
};
|
||||
|
||||
// const normalizeScanStatus = () => {
|
||||
// return {
|
||||
@@ -518,6 +522,7 @@ const normalizeGenre = (
|
||||
export const jfNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
folder: normalizeFolder,
|
||||
genre: normalizeGenre,
|
||||
musicFolder: normalizeMusicFolder,
|
||||
playlist: normalizePlaylist,
|
||||
|
||||
@@ -487,6 +487,7 @@ const song = z.object({
|
||||
MediaType: z.string(),
|
||||
Name: z.string(),
|
||||
NormalizationGain: z.number().optional(),
|
||||
ParentId: z.string().optional(),
|
||||
ParentIndexNumber: z.number(),
|
||||
People: participant.array().optional(),
|
||||
PlaylistItemId: z.string().optional(),
|
||||
@@ -495,7 +496,7 @@ const song = z.object({
|
||||
ProviderIds: providerIds.optional(),
|
||||
RunTimeTicks: z.number(),
|
||||
ServerId: z.string(),
|
||||
SortName: z.string(),
|
||||
SortName: z.string().optional(),
|
||||
Tags: z.string().array().optional(),
|
||||
Type: z.string(),
|
||||
UserData: userData.optional(),
|
||||
@@ -772,6 +773,34 @@ const filters = z.object({
|
||||
Years: z.number().array().optional(),
|
||||
});
|
||||
|
||||
const folder = z.object({
|
||||
BackdropImageTags: z.array(z.string()),
|
||||
ChannelId: z.null(),
|
||||
CollectionType: z.string(),
|
||||
Id: z.string(),
|
||||
ImageBlurHashes: imageBlurHashes,
|
||||
ImageTags: imageTags,
|
||||
IsFolder: z.boolean(),
|
||||
LocationType: z.string(),
|
||||
MediaType: z.string(),
|
||||
Name: z.string(),
|
||||
ParentId: z.string().optional(),
|
||||
ServerId: z.string(),
|
||||
Type: z.string(),
|
||||
UserData: userData.optional(),
|
||||
});
|
||||
|
||||
const folderList = pagination.extend({
|
||||
Items: z.array(folder),
|
||||
});
|
||||
|
||||
const folderParameters = z.object({
|
||||
Fields: z.string().optional(),
|
||||
ParentId: z.string().optional(),
|
||||
SortBy: z.string().optional(),
|
||||
SortOrder: z.enum(sortOrderValues).optional(),
|
||||
});
|
||||
|
||||
export const jfType = {
|
||||
_enum: {
|
||||
albumArtistList: albumArtistListSort,
|
||||
@@ -794,6 +823,7 @@ export const jfType = {
|
||||
deletePlaylist: deletePlaylistParameters,
|
||||
favorite: favoriteParameters,
|
||||
filterList: filterListParameters,
|
||||
folder: folderParameters,
|
||||
genreList: genreListParameters,
|
||||
musicFolderList: musicFolderListParameters,
|
||||
playlistDetail: playlistDetailParameters,
|
||||
@@ -819,6 +849,8 @@ export const jfType = {
|
||||
error,
|
||||
favorite,
|
||||
filters,
|
||||
folder,
|
||||
folderList,
|
||||
genre,
|
||||
genreList,
|
||||
lyrics,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
ExplicitStatus,
|
||||
Folder,
|
||||
Genre,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
@@ -342,9 +343,48 @@ const normalizeGenre = (
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeFolder = (
|
||||
item: z.infer<typeof ssType._response.directory>,
|
||||
server?: null | ServerListItemWithCredential,
|
||||
): Folder => {
|
||||
const results = item.child?.reduce(
|
||||
(acc: { folders: Folder[]; songs: Song[] }, item) => {
|
||||
const isDirectory = item.isDir === true;
|
||||
|
||||
if (isDirectory) {
|
||||
const folder = normalizeFolder(item, server);
|
||||
acc.folders.push(folder);
|
||||
} else {
|
||||
const song = normalizeSong(item, server);
|
||||
acc.songs.push(song);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
folders: [],
|
||||
songs: [],
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
_itemType: LibraryItem.FOLDER,
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
children: {
|
||||
folders: results?.folders || [],
|
||||
songs: results?.songs || [],
|
||||
},
|
||||
id: item.id.toString(),
|
||||
name: item.title,
|
||||
parentId: item.parent,
|
||||
};
|
||||
};
|
||||
|
||||
export const ssNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
folder: normalizeFolder,
|
||||
genre: normalizeGenre,
|
||||
playlist: normalizePlaylist,
|
||||
song: normalizeSong,
|
||||
|
||||
@@ -548,6 +548,50 @@ const albumInfo = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const getMusicDirectoryParameters = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const directory = z.object({
|
||||
artist: z.string().optional(),
|
||||
child: z.array(song).optional(),
|
||||
coverArt: z.string().optional(),
|
||||
id,
|
||||
isDir: z.boolean(),
|
||||
parent: z.string().optional(),
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
const getMusicDirectory = z.object({
|
||||
directory,
|
||||
});
|
||||
|
||||
const getIndexes = z.object({
|
||||
indexes: z.object({
|
||||
child: z.array(song),
|
||||
index: z
|
||||
.object({
|
||||
artist: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.array(),
|
||||
})
|
||||
.array(),
|
||||
shortcut: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.array(),
|
||||
}),
|
||||
});
|
||||
|
||||
const getIndexesParameters = z.object({
|
||||
musicFolderId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ssType = {
|
||||
_parameters: {
|
||||
albumInfo: albumInfoParameters,
|
||||
@@ -563,6 +607,8 @@ export const ssType = {
|
||||
getArtists: getArtistsParameters,
|
||||
getGenre: getGenresParameters,
|
||||
getGenres: getGenresParameters,
|
||||
getIndexes: getIndexesParameters,
|
||||
getMusicDirectory: getMusicDirectoryParameters,
|
||||
getPlaylist: getPlaylistParameters,
|
||||
getPlaylists: getPlaylistsParameters,
|
||||
getSong: getSongParameters,
|
||||
@@ -591,12 +637,15 @@ export const ssType = {
|
||||
baseResponse,
|
||||
createFavorite,
|
||||
createPlaylist,
|
||||
directory,
|
||||
genre,
|
||||
getAlbum,
|
||||
getAlbumList2,
|
||||
getArtist,
|
||||
getArtists,
|
||||
getGenres,
|
||||
getIndexes,
|
||||
getMusicDirectory,
|
||||
getPlaylist,
|
||||
getPlaylists,
|
||||
getSong,
|
||||
|
||||
@@ -245,6 +245,11 @@ export const sortSongsByFetchedOrder = (
|
||||
fetchedIds: string[],
|
||||
itemType: LibraryItem,
|
||||
): Song[] => {
|
||||
// For folders, songs are already in the correct order
|
||||
if (itemType === LibraryItem.FOLDER) {
|
||||
return songs;
|
||||
}
|
||||
|
||||
// Group songs by the fetched ID they belong to
|
||||
const songsByFetchedId = new Map<string, Song[]>();
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
Breadcrumbs as MantineBreadcrumbs,
|
||||
BreadcrumbsProps as MantineBreadcrumbsProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
interface BreadcrumbProps extends MantineBreadcrumbsProps {}
|
||||
|
||||
export const Breadcrumb = ({ children, ...props }: BreadcrumbProps) => {
|
||||
return <MantineBreadcrumbs {...props}>{children}</MantineBreadcrumbs>;
|
||||
};
|
||||
@@ -67,6 +67,7 @@ export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewP
|
||||
<Icon icon="playlist" size="md" />
|
||||
)}
|
||||
{itemType === LibraryItem.GENRE && <Icon icon="genre" size="md" />}
|
||||
{itemType === LibraryItem.FOLDER && <Icon icon="folder" size="md" />}
|
||||
{!itemType && <Icon icon="library" size="md" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip as MantineTooltip, TooltipProps as MantineTooltipProps } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './tooltip.module.css';
|
||||
|
||||
@@ -6,6 +7,7 @@ export interface TooltipProps extends MantineTooltipProps {}
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
classNames,
|
||||
openDelay = 500,
|
||||
transitionProps = {
|
||||
duration: 250,
|
||||
@@ -18,7 +20,8 @@ export const Tooltip = ({
|
||||
<MantineTooltip
|
||||
arrowSize={10}
|
||||
classNames={{
|
||||
tooltip: styles.tooltip,
|
||||
...classNames,
|
||||
tooltip: clsx(styles.tooltip, classNames?.['tooltip']),
|
||||
}}
|
||||
multiline
|
||||
openDelay={openDelay}
|
||||
|
||||
@@ -24,6 +24,7 @@ export enum LibraryItem {
|
||||
ALBUM = 'album',
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
FOLDER = 'folder',
|
||||
GENRE = 'genre',
|
||||
PLAYLIST = 'playlist',
|
||||
PLAYLIST_SONG = 'playlistSong',
|
||||
@@ -257,6 +258,29 @@ export type EndpointDetails = {
|
||||
server: ServerListItem;
|
||||
};
|
||||
|
||||
export type Folder = {
|
||||
_itemType: LibraryItem.FOLDER;
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
children?: {
|
||||
folders: Folder[];
|
||||
songs: Song[];
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
};
|
||||
|
||||
export type FolderArgs = BaseEndpointArgs & { query: FolderQuery };
|
||||
|
||||
export interface FolderQuery extends BaseQuery<SongListSort> {
|
||||
id: string;
|
||||
musicFolderId?: string | string[];
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export type FolderResponse = Folder;
|
||||
|
||||
export type GainInfo = {
|
||||
album?: number;
|
||||
track?: number;
|
||||
@@ -1231,6 +1255,7 @@ export type ControllerEndpoint = {
|
||||
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||
getArtistListCount: (args: ArtistListCountArgs) => Promise<number>;
|
||||
getDownloadUrl: (args: DownloadArgs) => string;
|
||||
getFolder: (args: FolderArgs) => Promise<FolderResponse>;
|
||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
|
||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
||||
@@ -1309,6 +1334,7 @@ export type InternalControllerEndpoint = {
|
||||
getArtistList: (args: ReplaceApiClientProps<ArtistListArgs>) => Promise<ArtistListResponse>;
|
||||
getArtistListCount: (args: ReplaceApiClientProps<ArtistListCountArgs>) => Promise<number>;
|
||||
getDownloadUrl: (args: ReplaceApiClientProps<DownloadArgs>) => string;
|
||||
getFolder: (args: ReplaceApiClientProps<FolderArgs>) => Promise<FolderResponse>;
|
||||
getGenreList: (args: ReplaceApiClientProps<GenreListArgs>) => Promise<GenreListResponse>;
|
||||
getLyrics?: (args: ReplaceApiClientProps<LyricsArgs>) => Promise<LyricsResponse>;
|
||||
getMusicFolderList: (
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum DragTarget {
|
||||
ALBUM = LibraryItem.ALBUM,
|
||||
ALBUM_ARTIST = LibraryItem.ALBUM_ARTIST,
|
||||
ARTIST = LibraryItem.ARTIST,
|
||||
FOLDER = LibraryItem.FOLDER,
|
||||
GENERIC = 'generic',
|
||||
GENRE = LibraryItem.GENRE,
|
||||
GRID_ROW = 'gridRow',
|
||||
@@ -19,6 +20,7 @@ export const DragTargetMap = {
|
||||
[LibraryItem.ALBUM]: DragTarget.ALBUM,
|
||||
[LibraryItem.ALBUM_ARTIST]: DragTarget.ALBUM_ARTIST,
|
||||
[LibraryItem.ARTIST]: DragTarget.ARTIST,
|
||||
[LibraryItem.FOLDER]: DragTarget.FOLDER,
|
||||
[LibraryItem.GENRE]: DragTarget.GENRE,
|
||||
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
|
||||
[LibraryItem.PLAYLIST_SONG]: DragTarget.SONG,
|
||||
|
||||
Reference in New Issue
Block a user