1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Combine search queries and make them synchronous

This commit is contained in:
viown 2025-03-03 18:18:39 +03:00
parent a63e80ec46
commit 5b81d4a2fc
13 changed files with 600 additions and 491 deletions

View file

@ -0,0 +1,23 @@
import { Api } from '@jellyfin/sdk/lib/api';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { AxiosRequestConfig } from 'axios';
import { QUERY_OPTIONS } from '../constants/queryOptions';
export const fetchItemsByType = async (
api: Api,
userId?: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await getItemsApi(api).getItems(
{
...QUERY_OPTIONS,
userId: userId,
recursive: true,
...params
},
options
);
return response.data;
};

View file

@ -0,0 +1,53 @@
import { Api } from '@jellyfin/sdk';
import { ArtistsApiGetArtistsRequest } from '@jellyfin/sdk/lib/generated-client/api/artists-api';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { isMusic } from '../utils/search';
const fetchArtists = async (
api: Api,
userId: string,
params?: ArtistsApiGetArtistsRequest,
options?: AxiosRequestConfig
) => {
const response = await getArtistsApi(api).getArtists(
{
...QUERY_OPTIONS,
userId: userId,
...params
},
options
);
return response.data;
};
export const useArtistsSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['ArtistsSearch', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) => {
const artistsData = await fetchArtists(
api!,
userId!,
{
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
return artistsData;
},
enabled: !!api && !!userId && (!collectionType || isMusic(collectionType))
});
};

View file

@ -0,0 +1,143 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { addSection, isLivetv } from '../utils/search';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
import { CardShape } from 'utils/card';
import { Section } from '../types';
import { fetchItemsByType } from './fetchItemsByType';
const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: string | undefined, signal: AbortSignal) => {
const sections: Section[] = [];
// Movies row
const moviesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: true,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Movies', moviesData.Items, {
...LIVETV_CARD_OPTIONS,
shape: CardShape.PortraitOverflow
});
// Episodes row
const episodesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: false,
isSeries: true,
isSports: false,
isKids: false,
isNews: false,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Episodes', episodesData.Items, {
...LIVETV_CARD_OPTIONS
});
// Sports row
const sportsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isSports: true,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Sports', sportsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Kids row
const kidsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isKids: true,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Kids', kidsData.Items, {
...LIVETV_CARD_OPTIONS
});
// News row
const newsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isNews: true,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'News', newsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Programs row
const programsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: false,
isSeries: false,
isSports: false,
isKids: false,
isNews: false,
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Programs', programsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Channels row
const channelsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.TvChannel],
searchTerm: searchTerm
},
{ signal }
);
addSection(sections, 'Channels', channelsData.Items);
return sections;
};
export const useLiveTvSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['LiveTv', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) =>
fetchLiveTv(api!, userId!, searchTerm, signal),
enabled: !!api && !!userId && !!collectionType && !!isLivetv(collectionType)
});
};

View file

@ -0,0 +1,54 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { isMovies, isTVShows } from '../utils/search';
import { PersonsApiGetPersonsRequest } from '@jellyfin/sdk/lib/generated-client/api/persons-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
const fetchPeople = async (
api: Api,
userId: string,
params?: PersonsApiGetPersonsRequest,
options?: AxiosRequestConfig
) => {
const response = await getPersonsApi(api).getPersons(
{
...QUERY_OPTIONS,
userId: userId,
...params
},
options
);
return response.data;
};
export const usePeopleSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
const isPeopleEnabled = (!collectionType || isMovies(collectionType) || isTVShows(collectionType));
return useQuery({
queryKey: ['PeopleSearch', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) => {
const peopleData = await fetchPeople(
api!,
userId!,
{
searchTerm: searchTerm
},
{ signal }
);
return peopleData;
},
enabled: !!api && !!userId && isPeopleEnabled
});
};

View file

@ -1,509 +1,84 @@
import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk';
import type {
ArtistsApiGetArtistsRequest,
BaseItemDto,
ItemsApiGetItemsRequest,
PersonsApiGetPersonsRequest
BaseItemKind
} from '@jellyfin/sdk/lib/generated-client';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from '../../../../../hooks/useApi';
import type { CardOptions } from 'types/cardOptions';
import { CardShape } from 'utils/card';
const QUERY_OPTIONS = {
limit: 100,
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.CanDelete,
ItemFields.MediaSourceCount
],
enableTotalRecordCount: false,
imageTypeLimit: 1
};
const fetchItemsByType = async (
api: Api,
userId?: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await getItemsApi(api).getItems(
{
...QUERY_OPTIONS,
userId: userId,
recursive: true,
...params
},
options
);
return response.data;
};
const fetchPeople = async (
api: Api,
userId: string,
params?: PersonsApiGetPersonsRequest,
options?: AxiosRequestConfig
) => {
const response = await getPersonsApi(api).getPersons(
{
...QUERY_OPTIONS,
userId: userId,
...params
},
options
);
return response.data;
};
const fetchArtists = async (
api: Api,
userId: string,
params?: ArtistsApiGetArtistsRequest,
options?: AxiosRequestConfig
) => {
const response = await getArtistsApi(api).getArtists(
{
...QUERY_OPTIONS,
userId: userId,
...params
},
options
);
return response.data;
};
const isMovies = (collectionType: string) =>
collectionType === CollectionType.Movies;
const isMusic = (collectionType: string) =>
collectionType === CollectionType.Music;
const isTVShows = (collectionType: string) =>
collectionType === CollectionType.Tvshows;
const isLivetv = (collectionType: string) =>
collectionType === CollectionType.Livetv;
const LIVETV_CARD_OPTIONS = {
preferThumb: true,
inheritThumb: false,
showParentTitleOrTitle: true,
showTitle: false,
coverImage: true,
overlayMoreButton: true,
showAirTime: true,
showAirDateTime: true,
showChannelName: true
};
export interface Section {
title: string
items: BaseItemDto[];
cardOptions?: CardOptions;
}
import { addSection, getCardOptionsFromType, getItemTypesFromCollectionType, getTitleFromType, isLivetv, isMovies, isMusic, isTVShows, sortSections } from '../utils/search';
import { useArtistsSearch } from './useArtistsSearch';
import { usePeopleSearch } from './usePeopleSearch';
import { useVideoSearch } from './useVideoSearch';
import { Section } from '../types';
import { useLiveTvSearch } from './useLiveTvSearch';
import { fetchItemsByType } from './fetchItemsByType';
export const useSearchItems = (
parentId?: string,
collectionType?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { data: artists, isPending: isArtistsPending } = useArtistsSearch(parentId, collectionType, searchTerm);
const { data: people, isPending: isPeoplePending } = usePeopleSearch(parentId, collectionType, searchTerm);
const { data: videos, isPending: isVideosPending } = useVideoSearch(parentId, collectionType, searchTerm);
const { data: liveTvSections, isPending: isLiveTvPending } = useLiveTvSearch(parentId, collectionType, searchTerm);
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['SearchItems', { parentId, collectionType, searchTerm }],
queryFn: async ({ signal }) => {
if (!api) throw new Error('No API instance available');
if (!userId) throw new Error('No User ID provided');
const isArtistsEnabled = !isArtistsPending || (collectionType && !isMusic(collectionType));
const isPeopleEnabled = !isPeoplePending || (collectionType && !isMovies(collectionType) && !isTVShows(collectionType));
const isVideosEnabled = !isVideosPending || collectionType;
const isLiveTvEnabled = !isLiveTvPending || !collectionType || !isLivetv(collectionType);
return useQuery({
queryKey: ['SearchItems', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) => {
const sections: Section[] = [];
const addSection = (
title: string,
items: BaseItemDto[] | null | undefined,
cardOptions?: CardOptions
) => {
if (items && items?.length > 0) {
sections.push({ title, items, cardOptions });
addSection(sections, 'Artists', artists?.Items, {
coverImage: true
});
addSection(sections, 'People', people?.Items, {
coverImage: true
});
addSection(sections, 'HeaderVideos', videos?.Items, {
showParentTitle: true
});
if (liveTvSections) {
sections.push(...liveTvSections);
}
const itemTypes: BaseItemKind[] = getItemTypesFromCollectionType(collectionType);
const searchData = await fetchItemsByType(
api!,
userId,
{
includeItemTypes: itemTypes,
parentId: parentId,
searchTerm: searchTerm,
limit: 800
},
{ signal }
);
if (searchData.Items) {
for (const itemType of itemTypes) {
const items: BaseItemDto[] = [];
for (const searchItem of searchData.Items) {
if (searchItem.Type === itemType) {
items.push(searchItem);
}
}
addSection(sections, getTitleFromType(itemType), items, getCardOptionsFromType(itemType));
}
};
// Livetv libraries
if (collectionType && isLivetv(collectionType)) {
// Movies row
const moviesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: true,
searchTerm: searchTerm
},
{ signal }
);
addSection('Movies', moviesData.Items, {
...LIVETV_CARD_OPTIONS,
shape: CardShape.PortraitOverflow
});
// Episodes row
const episodesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: false,
isSeries: true,
isSports: false,
isKids: false,
isNews: false,
searchTerm: searchTerm
},
{ signal }
);
addSection('Episodes', episodesData.Items, {
...LIVETV_CARD_OPTIONS
});
// Sports row
const sportsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isSports: true,
searchTerm: searchTerm
},
{ signal }
);
addSection('Sports', sportsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Kids row
const kidsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isKids: true,
searchTerm: searchTerm
},
{ signal }
);
addSection('Kids', kidsData.Items, {
...LIVETV_CARD_OPTIONS
});
// News row
const newsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isNews: true,
searchTerm: searchTerm
},
{ signal }
);
addSection('News', newsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Programs row
const programsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
isMovie: false,
isSeries: false,
isSports: false,
isKids: false,
isNews: false,
searchTerm: searchTerm
},
{ signal }
);
addSection('Programs', programsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Channels row
const channelsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.TvChannel],
searchTerm: searchTerm
},
{ signal }
);
addSection('Channels', channelsData.Items);
}
// Movie libraries
if (!collectionType || isMovies(collectionType)) {
// Movies row
const moviesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Movie],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Movies', moviesData.Items, {
showYear: true
});
}
// TV Show libraries
if (!collectionType || isTVShows(collectionType)) {
// Shows row
const showsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Series],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Shows', showsData.Items, {
showYear: true
});
// Episodes row
const episodesData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Episode],
parentId: parentId,
isMissing: user?.Configuration?.DisplayMissingEpisodes,
searchTerm: searchTerm
},
{ signal }
);
addSection('Episodes', episodesData.Items, {
coverImage: true,
showParentTitle: true
});
}
// People are included for Movies and TV Shows
if (
!collectionType
|| isMovies(collectionType)
|| isTVShows(collectionType)
) {
// People row
const peopleData = await fetchPeople(
api,
userId,
{
searchTerm: searchTerm
},
{ signal }
);
addSection('People', peopleData.Items, {
coverImage: true
});
}
// Music libraries
if (!collectionType || isMusic(collectionType)) {
// Playlists row
const playlistsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Playlist],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Playlists', playlistsData.Items);
// Artists row
const artistsData = await fetchArtists(
api,
userId,
{
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Artists', artistsData.Items, {
coverImage: true
});
// Albums row
const albumsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.MusicAlbum],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Albums', albumsData.Items, {
showYear: true
});
// Songs row
const songsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Audio],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Songs', songsData.Items, {
showParentTitle: true,
shape: CardShape.SquareOverflow
});
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
const videosData = await fetchItemsByType(
api,
userId,
{
mediaTypes: [MediaType.Video],
excludeItemTypes: [
BaseItemKind.Movie,
BaseItemKind.Episode,
BaseItemKind.TvChannel
],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('HeaderVideos', videosData.Items, {
showParentTitle: true
});
// Programs row
const programsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.LiveTvProgram],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Programs', programsData.Items, {
...LIVETV_CARD_OPTIONS
});
// Channels row
const channelsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.TvChannel],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Channels', channelsData.Items);
// Photo Albums row
const photoAlbumsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.PhotoAlbum],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('HeaderPhotoAlbums', photoAlbumsData.Items);
// Photos row
const photosData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Photo],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Photos', photosData.Items);
// Audio Books row
const audioBooksData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.AudioBook],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('HeaderAudioBooks', audioBooksData.Items);
// Books row
const booksData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.Book],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Books', booksData.Items);
// Collections row
const collectionsData = await fetchItemsByType(
api,
userId,
{
includeItemTypes: [BaseItemKind.BoxSet],
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
addSection('Collections', collectionsData.Items);
}
return sections;
return sortSections(sections);
},
enabled: !!api && !!userId
enabled: !!api && !!userId && !!isArtistsEnabled && !!isPeopleEnabled && !!isVideosEnabled && !!isLiveTvEnabled
});
};

View file

@ -0,0 +1,61 @@
import { Api } from '@jellyfin/sdk';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QUERY_OPTIONS } from '../constants/queryOptions';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
const fetchPeople = async (
api: Api,
userId: string,
params?: ItemsApiGetItemsRequest,
options?: AxiosRequestConfig
) => {
const response = await getItemsApi(api).getItems(
{
...QUERY_OPTIONS,
userId: userId,
recursive: true,
mediaTypes: [MediaType.Video],
excludeItemTypes: [
BaseItemKind.Movie,
BaseItemKind.Episode,
BaseItemKind.TvChannel
],
...params
},
options
);
return response.data;
};
export const useVideoSearch = (
parentId?: string,
collectionType?: CollectionType,
searchTerm?: string
) => {
const { api, user } = useApi();
const userId = user?.Id;
return useQuery({
queryKey: ['VideoSearch', collectionType, parentId, searchTerm],
queryFn: async ({ signal }) => {
const videosData = await fetchPeople(
api!,
userId!,
{
parentId: parentId,
searchTerm: searchTerm
},
{ signal }
);
return videosData;
},
enabled: !!api && !!userId && !collectionType
});
};

View file

@ -1,13 +1,15 @@
import React, { type FC } from 'react';
import { Section, useSearchItems } from '../api/useSearchItems';
import { useSearchItems } from '../api/useSearchItems';
import globalize from '../../../../../lib/globalize';
import Loading from '../../../../../components/loading/LoadingComponent';
import SearchResultsRow from './SearchResultsRow';
import { CardShape } from 'utils/card';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { Section } from '../types';
interface SearchResultsProps {
parentId?: string;
collectionType?: string;
collectionType?: CollectionType;
query?: string;
}
@ -19,9 +21,9 @@ const SearchResults: FC<SearchResultsProps> = ({
collectionType,
query
}) => {
const { isLoading, data } = useSearchItems(parentId, collectionType, query);
const { data, isPending } = useSearchItems(parentId, collectionType, query?.trim());
if (isLoading) return <Loading />;
if (isPending) return <Loading />;
if (!data?.length) {
return (

View file

@ -0,0 +1,11 @@
export const LIVETV_CARD_OPTIONS = {
preferThumb: true,
inheritThumb: false,
showParentTitleOrTitle: true,
showTitle: false,
coverImage: true,
overlayMoreButton: true,
showAirTime: true,
showAirDateTime: true,
showChannelName: true
};

View file

@ -0,0 +1,12 @@
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
export const QUERY_OPTIONS = {
limit: 100,
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.CanDelete,
ItemFields.MediaSourceCount
],
enableTotalRecordCount: false,
imageTypeLimit: 1
};

View file

@ -0,0 +1,20 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
export const SEARCH_SECTIONS_SORT_ORDER: BaseItemKind[] = [
BaseItemKind.Movie,
BaseItemKind.Series,
BaseItemKind.Episode,
BaseItemKind.Person,
BaseItemKind.Playlist,
BaseItemKind.MusicArtist,
BaseItemKind.MusicAlbum,
BaseItemKind.Audio,
BaseItemKind.Video,
BaseItemKind.LiveTvProgram,
BaseItemKind.TvChannel,
BaseItemKind.PhotoAlbum,
BaseItemKind.Photo,
BaseItemKind.AudioBook,
BaseItemKind.Book,
BaseItemKind.BoxSet
];

View file

@ -0,0 +1,10 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { CardOptions } from 'types/cardOptions';
export interface Section {
title: string
items: BaseItemDto[];
sectionType: BaseItemKind;
cardOptions?: CardOptions;
};

View file

@ -0,0 +1,144 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { CardShape } from 'utils/card';
import { Section } from '../types';
import { CardOptions } from 'types/cardOptions';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
import { SEARCH_SECTIONS_SORT_ORDER } from '../constants/sectionSortOrder';
export const isMovies = (collectionType: string) =>
collectionType === CollectionType.Movies;
export const isTVShows = (collectionType: string) =>
collectionType === CollectionType.Tvshows;
export const isMusic = (collectionType: string) =>
collectionType === CollectionType.Music;
export const isLivetv = (collectionType: string) =>
collectionType === CollectionType.Livetv;
export function addSection(
sections: Section[],
title: string,
items: BaseItemDto[] | null | undefined,
cardOptions?: CardOptions
) {
if (items && items?.length > 0 && items[0].Type) {
sections.push({ title, items, sectionType: items[0].Type, cardOptions });
}
}
export function sortSections(sections: Section[]) {
return sections.sort((a, b) => {
const indexA = SEARCH_SECTIONS_SORT_ORDER.indexOf(a.sectionType);
const indexB = SEARCH_SECTIONS_SORT_ORDER.indexOf(b.sectionType);
if (indexA > indexB) {
return 1;
} else if (indexA < indexB) {
return -1;
} else {
return 0;
}
});
}
export function getCardOptionsFromType(type: BaseItemKind) {
switch (type) {
case BaseItemKind.Movie:
case BaseItemKind.Series:
case BaseItemKind.MusicAlbum:
return {
showYear: true
};
case BaseItemKind.Episode:
return {
coverImage: true,
showParentTitle: true
};
case BaseItemKind.MusicArtist:
return {
coverImage: true
};
case BaseItemKind.Audio:
return {
showParentTitle: true,
shape: CardShape.SquareOverflow
};
case BaseItemKind.LiveTvProgram:
return LIVETV_CARD_OPTIONS;
default:
return {};
}
}
export function getTitleFromType(type: BaseItemKind) {
switch (type) {
case BaseItemKind.Movie:
return 'Movies';
case BaseItemKind.Series:
return 'Shows';
case BaseItemKind.Episode:
return 'Episodes';
case BaseItemKind.Playlist:
return 'Playlists';
case BaseItemKind.MusicAlbum:
return 'Albums';
case BaseItemKind.Audio:
return 'Songs';
case BaseItemKind.LiveTvProgram:
return 'Programs';
case BaseItemKind.TvChannel:
return 'Channels';
case BaseItemKind.PhotoAlbum:
return 'HeaderPhotoAlbums';
case BaseItemKind.Photo:
return 'Photos';
case BaseItemKind.AudioBook:
return 'HeaderAudioBooks';
case BaseItemKind.Book:
return 'Books';
case BaseItemKind.BoxSet:
return 'Collections';
default:
return '';
}
}
export function getItemTypesFromCollectionType(collectionType: CollectionType | undefined) {
switch (collectionType) {
case CollectionType.Movies:
return [BaseItemKind.Movie];
case CollectionType.Tvshows:
return [
BaseItemKind.Series,
BaseItemKind.Episode
];
case CollectionType.Music:
return [
BaseItemKind.Playlist,
BaseItemKind.MusicAlbum,
BaseItemKind.Audio
];
case CollectionType.Livetv:
return [];
default:
return [
BaseItemKind.Movie,
BaseItemKind.Series,
BaseItemKind.Episode,
BaseItemKind.Playlist,
BaseItemKind.MusicAlbum,
BaseItemKind.Audio,
BaseItemKind.LiveTvProgram,
BaseItemKind.TvChannel,
BaseItemKind.PhotoAlbum,
BaseItemKind.Photo,
BaseItemKind.AudioBook,
BaseItemKind.Book,
BaseItemKind.BoxSet
];
}
}

View file

@ -7,6 +7,7 @@ import Page from 'components/Page';
import SearchFields from 'apps/stable/features/search/components/SearchFields';
import SearchSuggestions from 'apps/stable/features/search/components/SearchSuggestions';
import SearchResults from 'apps/stable/features/search/components/SearchResults';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
const COLLECTION_TYPE_PARAM = 'collectionType';
const PARENT_ID_PARAM = 'parentId';
@ -15,7 +16,7 @@ const QUERY_PARAM = 'query';
const Search: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
const collectionTypeQuery = searchParams.get(COLLECTION_TYPE_PARAM) || undefined;
const collectionTypeQuery = (searchParams.get(COLLECTION_TYPE_PARAM) || undefined) as CollectionType | undefined;
const urlQuery = searchParams.get(QUERY_PARAM) || '';
const [query, setQuery] = useState(urlQuery);
const prevQuery = usePrevious(query, '');