mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6593 from viown/search-enhancements
Search Enhancements
This commit is contained in:
commit
fcf344cea3
22 changed files with 713 additions and 546 deletions
23
src/apps/stable/features/search/api/fetchItemsByType.ts
Normal file
23
src/apps/stable/features/search/api/fetchItemsByType.ts
Normal 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,
|
||||||
|
recursive: true,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
49
src/apps/stable/features/search/api/useArtistsSearch.ts
Normal file
49
src/apps/stable/features/search/api/useArtistsSearch.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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,
|
||||||
|
...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: ['Search', 'Artists', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: ({ signal }) => fetchArtists(
|
||||||
|
api!,
|
||||||
|
userId!,
|
||||||
|
{
|
||||||
|
parentId,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
enabled: !!api && !!userId && (!collectionType || isMusic(collectionType))
|
||||||
|
});
|
||||||
|
};
|
150
src/apps/stable/features/search/api/useLiveTvSearch.ts
Normal file
150
src/apps/stable/features/search/api/useLiveTvSearch.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
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 = (api: Api, userId: string | undefined, searchTerm: string | undefined, signal: AbortSignal) => {
|
||||||
|
const sections: Section[] = [];
|
||||||
|
|
||||||
|
// Movies row
|
||||||
|
const movies = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isMovie: true,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(moviesData => {
|
||||||
|
addSection(sections, 'Movies', moviesData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS,
|
||||||
|
shape: CardShape.PortraitOverflow
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Episodes row
|
||||||
|
const episodes = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isMovie: false,
|
||||||
|
isSeries: true,
|
||||||
|
isSports: false,
|
||||||
|
isKids: false,
|
||||||
|
isNews: false,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(episodesData => {
|
||||||
|
addSection(sections, 'Episodes', episodesData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sports row
|
||||||
|
const sports = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isSports: true,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(sportsData => {
|
||||||
|
addSection(sections, 'Sports', sportsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kids row
|
||||||
|
const kids = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isKids: true,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(kidsData => {
|
||||||
|
addSection(sections, 'Kids', kidsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// News row
|
||||||
|
const news = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isNews: true,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(newsData => {
|
||||||
|
addSection(sections, 'News', newsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Programs row
|
||||||
|
const programs = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.LiveTvProgram ],
|
||||||
|
isMovie: false,
|
||||||
|
isSeries: false,
|
||||||
|
isSports: false,
|
||||||
|
isKids: false,
|
||||||
|
isNews: false,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(programsData => {
|
||||||
|
addSection(sections, 'Programs', programsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Channels row
|
||||||
|
const channels = fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [ BaseItemKind.TvChannel ],
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
).then(channelsData => {
|
||||||
|
addSection(sections, 'Channels', channelsData.Items);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all([ movies, episodes, sports, kids, news, programs, channels ]).then(() => sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLiveTvSearch = (
|
||||||
|
parentId?: string,
|
||||||
|
collectionType?: CollectionType,
|
||||||
|
searchTerm?: string
|
||||||
|
) => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const userId = user?.Id;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['Search', 'LiveTv', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchLiveTv(api!, userId!, searchTerm, signal),
|
||||||
|
enabled: !!api && !!userId && !!collectionType && !!isLivetv(collectionType)
|
||||||
|
});
|
||||||
|
};
|
50
src/apps/stable/features/search/api/usePeopleSearch.ts
Normal file
50
src/apps/stable/features/search/api/usePeopleSearch.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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,
|
||||||
|
...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: ['Search', 'People', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: ({ signal }) => fetchPeople(
|
||||||
|
api!,
|
||||||
|
userId!,
|
||||||
|
{
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
enabled: !!api && !!userId && isPeopleEnabled
|
||||||
|
});
|
||||||
|
};
|
50
src/apps/stable/features/search/api/useProgramsSearch.ts
Normal file
50
src/apps/stable/features/search/api/useProgramsSearch.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
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 { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { ItemsApiGetItemsRequest } from '@jellyfin/sdk/lib/generated-client/api/items-api';
|
||||||
|
import { fetchItemsByType } from './fetchItemsByType';
|
||||||
|
|
||||||
|
const fetchPrograms = async (
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params?: ItemsApiGetItemsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const response = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProgramsSearch = (
|
||||||
|
parentId?: string,
|
||||||
|
collectionType?: CollectionType,
|
||||||
|
searchTerm?: string
|
||||||
|
) => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const userId = user?.Id;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['Search', 'Programs', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: ({ signal }) => fetchPrograms(
|
||||||
|
api!,
|
||||||
|
userId!,
|
||||||
|
{
|
||||||
|
parentId,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
enabled: !!api && !!userId && !collectionType
|
||||||
|
});
|
||||||
|
};
|
98
src/apps/stable/features/search/api/useSearchItems.ts
Normal file
98
src/apps/stable/features/search/api/useSearchItems.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useApi } from '../../../../../hooks/useApi';
|
||||||
|
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';
|
||||||
|
import { useProgramsSearch } from './useProgramsSearch';
|
||||||
|
import { LIVETV_CARD_OPTIONS } from '../constants/liveTvCardOptions';
|
||||||
|
|
||||||
|
export const useSearchItems = (
|
||||||
|
parentId?: 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: programs, isPending: isProgramsPending } = useProgramsSearch(parentId, collectionType, searchTerm);
|
||||||
|
const { data: liveTvSections, isPending: isLiveTvPending } = useLiveTvSearch(parentId, collectionType, searchTerm);
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const userId = user?.Id;
|
||||||
|
|
||||||
|
const isArtistsEnabled = !isArtistsPending || (collectionType && !isMusic(collectionType));
|
||||||
|
const isPeopleEnabled = !isPeoplePending || (collectionType && !isMovies(collectionType) && !isTVShows(collectionType));
|
||||||
|
const isVideosEnabled = !isVideosPending || collectionType;
|
||||||
|
const isProgramsEnabled = !isProgramsPending || collectionType;
|
||||||
|
const isLiveTvEnabled = !isLiveTvPending || !collectionType || !isLivetv(collectionType);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['Search', 'Items', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
if (liveTvSections && collectionType && isLivetv(collectionType)) {
|
||||||
|
return sortSections(liveTvSections);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections: Section[] = [];
|
||||||
|
|
||||||
|
addSection(sections, 'Artists', artists?.Items, {
|
||||||
|
coverImage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
addSection(sections, 'Programs', programs?.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
addSection(sections, 'People', people?.Items, {
|
||||||
|
coverImage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
addSection(sections, 'HeaderVideos', videos?.Items, {
|
||||||
|
showParentTitle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemTypes: BaseItemKind[] = getItemTypesFromCollectionType(collectionType);
|
||||||
|
|
||||||
|
const searchData = await fetchItemsByType(
|
||||||
|
api!,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: itemTypes,
|
||||||
|
parentId,
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortSections(sections);
|
||||||
|
},
|
||||||
|
enabled: (
|
||||||
|
!!api
|
||||||
|
&& !!userId
|
||||||
|
&& !!isArtistsEnabled
|
||||||
|
&& !!isPeopleEnabled
|
||||||
|
&& !!isVideosEnabled
|
||||||
|
&& !!isLiveTvEnabled
|
||||||
|
&& !!isProgramsEnabled
|
||||||
|
)
|
||||||
|
});
|
||||||
|
};
|
|
@ -4,20 +4,17 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
|
|
||||||
const fetchGetItems = async (
|
const fetchGetItems = async (
|
||||||
api?: Api,
|
api: Api,
|
||||||
userId?: string,
|
userId: string,
|
||||||
parentId?: string,
|
parentId?: string,
|
||||||
options?: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
) => {
|
) => {
|
||||||
if (!api) throw new Error('No API instance available');
|
|
||||||
if (!userId) throw new Error('No User ID provided');
|
|
||||||
|
|
||||||
const response = await getItemsApi(api).getItems(
|
const response = await getItemsApi(api).getItems(
|
||||||
{
|
{
|
||||||
userId: userId,
|
userId,
|
||||||
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
||||||
includeItemTypes: [
|
includeItemTypes: [
|
||||||
BaseItemKind.Movie,
|
BaseItemKind.Movie,
|
||||||
|
@ -28,7 +25,7 @@ const fetchGetItems = async (
|
||||||
recursive: true,
|
recursive: true,
|
||||||
imageTypeLimit: 0,
|
imageTypeLimit: 0,
|
||||||
enableImages: false,
|
enableImages: false,
|
||||||
parentId: parentId,
|
parentId,
|
||||||
enableTotalRecordCount: false
|
enableTotalRecordCount: false
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
|
@ -43,7 +40,8 @@ export const useSearchSuggestions = (parentId?: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['SearchSuggestions', { parentId }],
|
queryKey: ['SearchSuggestions', { parentId }],
|
||||||
queryFn: ({ signal }) =>
|
queryFn: ({ signal }) =>
|
||||||
fetchGetItems(api, userId, parentId, { signal }),
|
fetchGetItems(api!, userId!, parentId, { signal }),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
enabled: !!api && !!userId
|
enabled: !!api && !!userId
|
||||||
});
|
});
|
||||||
};
|
};
|
57
src/apps/stable/features/search/api/useVideoSearch.ts
Normal file
57
src/apps/stable/features/search/api/useVideoSearch.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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 fetchVideos = async (
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params?: ItemsApiGetItemsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const response = await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
...QUERY_OPTIONS,
|
||||||
|
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: ['Search', 'Video', collectionType, parentId, searchTerm],
|
||||||
|
queryFn: ({ signal }) => fetchVideos(
|
||||||
|
api!,
|
||||||
|
userId!,
|
||||||
|
{
|
||||||
|
parentId,
|
||||||
|
searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
),
|
||||||
|
enabled: !!api && !!userId && !collectionType
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
|
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
|
||||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
import AlphaPicker from 'components/alphaPicker/AlphaPickerComponent';
|
||||||
import Input from 'elements/emby-input/Input';
|
import Input from 'elements/emby-input/Input';
|
||||||
import globalize from '../../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from 'components/layoutManager';
|
||||||
import browser from '../../scripts/browser';
|
import browser from 'scripts/browser';
|
||||||
import 'material-design-icons-iconfont';
|
import 'material-design-icons-iconfont';
|
||||||
import '../../styles/flexstyles.scss';
|
import 'styles/flexstyles.scss';
|
||||||
import './searchfields.scss';
|
import './searchfields.scss';
|
||||||
|
|
||||||
interface SearchFieldsProps {
|
interface SearchFieldsProps {
|
|
@ -1,13 +1,16 @@
|
||||||
import React, { type FC } from 'react';
|
import React, { type FC } from 'react';
|
||||||
import { Section, useSearchItems } from 'hooks/searchHook';
|
import { useSearchItems } from '../api/useSearchItems';
|
||||||
import globalize from '../../lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Loading from '../loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import SearchResultsRow from './SearchResultsRow';
|
import SearchResultsRow from './SearchResultsRow';
|
||||||
import { CardShape } from 'utils/card';
|
import { CardShape } from 'utils/card';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { Section } from '../types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
interface SearchResultsProps {
|
interface SearchResultsProps {
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
collectionType?: string;
|
collectionType?: CollectionType;
|
||||||
query?: string;
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,14 +22,22 @@ const SearchResults: FC<SearchResultsProps> = ({
|
||||||
collectionType,
|
collectionType,
|
||||||
query
|
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) {
|
if (!data?.length) {
|
||||||
return (
|
return (
|
||||||
<div className='noItemsMessage centerMessage'>
|
<div className='noItemsMessage centerMessage'>
|
||||||
{globalize.translate('SearchResultsEmpty', query)}
|
{globalize.translate('SearchResultsEmpty', query)}
|
||||||
|
{collectionType && (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
className='emby-button'
|
||||||
|
to={`/search.html?query=${encodeURIComponent(query || '')}`}
|
||||||
|
>{globalize.translate('RetryWithGlobalSearch')}</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +62,7 @@ const SearchResults: FC<SearchResultsProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'searchResults, padded-top, padded-bottom-page'}>
|
<div className={'searchResults padded-top padded-bottom-page'}>
|
||||||
{data.map((section, index) => renderSection(section, index))}
|
{data.map((section, index) => renderSection(section, index))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
|
@ -1,10 +1,10 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { type FC, useEffect, useRef } from 'react';
|
import React, { type FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||||
import type { CardOptions } from 'types/cardOptions';
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
import '../../elements/emby-scroller/emby-scroller';
|
import 'elements/emby-scroller/emby-scroller';
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
// There seems to be some compatibility issues here between
|
// There seems to be some compatibility issues here between
|
||||||
// React and our legacy web components, so we need to inject
|
// React and our legacy web components, so we need to inject
|
|
@ -1,21 +1,21 @@
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import { appRouter } from '../router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions';
|
import { useSearchSuggestions } from '../api/useSearchSuggestions';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import LinkButton from '../../elements/emby-button/LinkButton';
|
import LinkButton from 'elements/emby-button/LinkButton';
|
||||||
|
|
||||||
import '../../elements/emby-button/emby-button';
|
import 'elements/emby-button/emby-button';
|
||||||
|
|
||||||
type SearchSuggestionsProps = {
|
type SearchSuggestionsProps = {
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => {
|
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => {
|
||||||
const { isLoading, data: suggestions } = useSearchSuggestions(parentId || undefined);
|
const { data: suggestions, isPending } = useSearchSuggestions(parentId || undefined);
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
if (isPending) return <Loading />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
|
@ -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
|
||||||
|
};
|
12
src/apps/stable/features/search/constants/queryOptions.ts
Normal file
12
src/apps/stable/features/search/constants/queryOptions.ts
Normal 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
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const SEARCH_SECTIONS_SORT_ORDER = [
|
||||||
|
'Movies',
|
||||||
|
'Shows',
|
||||||
|
'Episodes',
|
||||||
|
'People',
|
||||||
|
'Playlists',
|
||||||
|
'Artists',
|
||||||
|
'Albums',
|
||||||
|
'Songs',
|
||||||
|
'HeaderVideos',
|
||||||
|
'Programs',
|
||||||
|
'Channels',
|
||||||
|
'HeaderPhotoAlbums',
|
||||||
|
'Photos',
|
||||||
|
'HeaderAudioBooks',
|
||||||
|
'Books',
|
||||||
|
'Collections'
|
||||||
|
];
|
8
src/apps/stable/features/search/types.ts
Normal file
8
src/apps/stable/features/search/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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[];
|
||||||
|
cardOptions?: CardOptions;
|
||||||
|
};
|
141
src/apps/stable/features/search/utils/search.ts
Normal file
141
src/apps/stable/features/search/utils/search.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
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) {
|
||||||
|
sections.push({ title, items, cardOptions });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortSections(sections: Section[]) {
|
||||||
|
return sections.sort((a, b) => {
|
||||||
|
const indexA = SEARCH_SECTIONS_SORT_ORDER.indexOf(a.title);
|
||||||
|
const indexB = SEARCH_SECTIONS_SORT_ORDER.indexOf(b.title);
|
||||||
|
|
||||||
|
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
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.Playlist,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.TvChannel,
|
||||||
|
BaseItemKind.PhotoAlbum,
|
||||||
|
BaseItemKind.Photo,
|
||||||
|
BaseItemKind.AudioBook,
|
||||||
|
BaseItemKind.Book,
|
||||||
|
BaseItemKind.BoxSet
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,10 @@ import { useDebounceValue } from 'usehooks-ts';
|
||||||
import { usePrevious } from 'hooks/usePrevious';
|
import { usePrevious } from 'hooks/usePrevious';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Page from 'components/Page';
|
import Page from 'components/Page';
|
||||||
import SearchFields from 'components/search/SearchFields';
|
import SearchFields from 'apps/stable/features/search/components/SearchFields';
|
||||||
import SearchSuggestions from 'components/search/SearchSuggestions';
|
import SearchSuggestions from 'apps/stable/features/search/components/SearchSuggestions';
|
||||||
import SearchResults from 'components/search/SearchResults';
|
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 COLLECTION_TYPE_PARAM = 'collectionType';
|
||||||
const PARENT_ID_PARAM = 'parentId';
|
const PARENT_ID_PARAM = 'parentId';
|
||||||
|
@ -15,7 +16,7 @@ const QUERY_PARAM = 'query';
|
||||||
const Search: FC = () => {
|
const Search: FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
|
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 urlQuery = searchParams.get(QUERY_PARAM) || '';
|
||||||
const [query, setQuery] = useState(urlQuery);
|
const [query, setQuery] = useState(urlQuery);
|
||||||
const prevQuery = usePrevious(query, '');
|
const prevQuery = usePrevious(query, '');
|
||||||
|
@ -50,7 +51,7 @@ const Search: FC = () => {
|
||||||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||||
>
|
>
|
||||||
<SearchFields query={query} onSearch={setQuery} />
|
<SearchFields query={query} onSearch={setQuery} />
|
||||||
{!query ? (
|
{!debouncedQuery ? (
|
||||||
<SearchSuggestions
|
<SearchSuggestions
|
||||||
parentId={parentIdQuery}
|
parentId={parentIdQuery}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './useSearchItems';
|
|
||||||
export * from './useSearchSuggestions';
|
|
|
@ -1,509 +0,0 @@
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
|
||||||
import type { Api } from '@jellyfin/sdk';
|
|
||||||
import type {
|
|
||||||
ArtistsApiGetArtistsRequest,
|
|
||||||
BaseItemDto,
|
|
||||||
ItemsApiGetItemsRequest,
|
|
||||||
PersonsApiGetPersonsRequest
|
|
||||||
} 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 '../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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSearchItems = (
|
|
||||||
parentId?: string,
|
|
||||||
collectionType?: string,
|
|
||||||
searchTerm?: string
|
|
||||||
) => {
|
|
||||||
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 sections: Section[] = [];
|
|
||||||
|
|
||||||
const addSection = (
|
|
||||||
title: string,
|
|
||||||
items: BaseItemDto[] | null | undefined,
|
|
||||||
cardOptions?: CardOptions
|
|
||||||
) => {
|
|
||||||
if (items && items?.length > 0) {
|
|
||||||
sections.push({ title, items, cardOptions });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
enabled: !!api && !!userId
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1451,6 +1451,7 @@
|
||||||
"ReplaceExistingImages": "Replace existing images",
|
"ReplaceExistingImages": "Replace existing images",
|
||||||
"ReplaceTrickplayImages": "Replace existing trickplay images",
|
"ReplaceTrickplayImages": "Replace existing trickplay images",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
|
"RetryWithGlobalSearch": "Retry with a global search",
|
||||||
"Reset": "Reset",
|
"Reset": "Reset",
|
||||||
"ResetPassword": "Reset Password",
|
"ResetPassword": "Reset Password",
|
||||||
"ResolutionMatchSource": "Match Source",
|
"ResolutionMatchSource": "Match Source",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue