From a63e80ec4602f89b461d6b4af6325fe4548154bc Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:53:20 +0300 Subject: [PATCH 01/13] Move search components and hooks to features --- .../stable/features/search/api}/useSearchItems.ts | 2 +- .../features/search/api}/useSearchSuggestions.ts | 2 +- .../features/search/components}/SearchFields.tsx | 10 +++++----- .../features/search/components}/SearchResults.tsx | 6 +++--- .../features/search/components}/SearchResultsRow.tsx | 6 +++--- .../features/search/components}/SearchSuggestions.tsx | 8 ++++---- .../features/search/components}/searchfields.scss | 0 src/apps/stable/routes/search.tsx | 6 +++--- src/hooks/searchHook/index.ts | 2 -- 9 files changed, 20 insertions(+), 22 deletions(-) rename src/{hooks/searchHook => apps/stable/features/search/api}/useSearchItems.ts (99%) rename src/{hooks/searchHook => apps/stable/features/search/api}/useSearchSuggestions.ts (97%) rename src/{components/search => apps/stable/features/search/components}/SearchFields.tsx (88%) rename src/{components/search => apps/stable/features/search/components}/SearchResults.tsx (89%) rename src/{components/search => apps/stable/features/search/components}/SearchResultsRow.tsx (89%) rename src/{components/search => apps/stable/features/search/components}/SearchSuggestions.tsx (83%) rename src/{components/search => apps/stable/features/search/components}/searchfields.scss (100%) delete mode 100644 src/hooks/searchHook/index.ts diff --git a/src/hooks/searchHook/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts similarity index 99% rename from src/hooks/searchHook/useSearchItems.ts rename to src/apps/stable/features/search/api/useSearchItems.ts index e720c8ac17..d073fac379 100644 --- a/src/hooks/searchHook/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -14,7 +14,7 @@ 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 { useApi } from '../../../../../hooks/useApi'; import type { CardOptions } from 'types/cardOptions'; import { CardShape } from 'utils/card'; diff --git a/src/hooks/searchHook/useSearchSuggestions.ts b/src/apps/stable/features/search/api/useSearchSuggestions.ts similarity index 97% rename from src/hooks/searchHook/useSearchSuggestions.ts rename to src/apps/stable/features/search/api/useSearchSuggestions.ts index a9b0ce4767..ea6e7b77e0 100644 --- a/src/hooks/searchHook/useSearchSuggestions.ts +++ b/src/apps/stable/features/search/api/useSearchSuggestions.ts @@ -4,7 +4,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { useQuery } from '@tanstack/react-query'; -import { useApi } from '../useApi'; +import { useApi } from 'hooks/useApi'; const fetchGetItems = async ( api?: Api, diff --git a/src/components/search/SearchFields.tsx b/src/apps/stable/features/search/components/SearchFields.tsx similarity index 88% rename from src/components/search/SearchFields.tsx rename to src/apps/stable/features/search/components/SearchFields.tsx index b1cea800c5..cd36dc2386 100644 --- a/src/components/search/SearchFields.tsx +++ b/src/apps/stable/features/search/components/SearchFields.tsx @@ -1,11 +1,11 @@ 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 globalize from '../../lib/globalize'; -import layoutManager from '../layoutManager'; -import browser from '../../scripts/browser'; +import globalize from '../../../../../lib/globalize'; +import layoutManager from '../../../../../components/layoutManager'; +import browser from '../../../../../scripts/browser'; import 'material-design-icons-iconfont'; -import '../../styles/flexstyles.scss'; +import 'styles/flexstyles.scss'; import './searchfields.scss'; interface SearchFieldsProps { diff --git a/src/components/search/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx similarity index 89% rename from src/components/search/SearchResults.tsx rename to src/apps/stable/features/search/components/SearchResults.tsx index 3c7dec3376..24e6a83e4f 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -1,7 +1,7 @@ import React, { type FC } from 'react'; -import { Section, useSearchItems } from 'hooks/searchHook'; -import globalize from '../../lib/globalize'; -import Loading from '../loading/LoadingComponent'; +import { Section, useSearchItems } from '../api/useSearchItems'; +import globalize from '../../../../../lib/globalize'; +import Loading from '../../../../../components/loading/LoadingComponent'; import SearchResultsRow from './SearchResultsRow'; import { CardShape } from 'utils/card'; diff --git a/src/components/search/SearchResultsRow.tsx b/src/apps/stable/features/search/components/SearchResultsRow.tsx similarity index 89% rename from src/components/search/SearchResultsRow.tsx rename to src/apps/stable/features/search/components/SearchResultsRow.tsx index 4fad2a0966..be74c233c9 100644 --- a/src/components/search/SearchResultsRow.tsx +++ b/src/apps/stable/features/search/components/SearchResultsRow.tsx @@ -1,10 +1,10 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; 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 '../../elements/emby-scroller/emby-scroller'; -import '../../elements/emby-itemscontainer/emby-itemscontainer'; +import 'elements/emby-scroller/emby-scroller'; +import 'elements/emby-itemscontainer/emby-itemscontainer'; // There seems to be some compatibility issues here between // React and our legacy web components, so we need to inject diff --git a/src/components/search/SearchSuggestions.tsx b/src/apps/stable/features/search/components/SearchSuggestions.tsx similarity index 83% rename from src/components/search/SearchSuggestions.tsx rename to src/apps/stable/features/search/components/SearchSuggestions.tsx index 809ad29444..d222443fdc 100644 --- a/src/components/search/SearchSuggestions.tsx +++ b/src/apps/stable/features/search/components/SearchSuggestions.tsx @@ -1,12 +1,12 @@ import React, { FunctionComponent } from 'react'; import Loading from 'components/loading/LoadingComponent'; -import { appRouter } from '../router/appRouter'; -import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions'; +import { appRouter } from '../../../../../components/router/appRouter'; +import { useSearchSuggestions } from '../api/useSearchSuggestions'; 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 = { parentId?: string | null; diff --git a/src/components/search/searchfields.scss b/src/apps/stable/features/search/components/searchfields.scss similarity index 100% rename from src/components/search/searchfields.scss rename to src/apps/stable/features/search/components/searchfields.scss diff --git a/src/apps/stable/routes/search.tsx b/src/apps/stable/routes/search.tsx index 290ea60406..4cf0b29b72 100644 --- a/src/apps/stable/routes/search.tsx +++ b/src/apps/stable/routes/search.tsx @@ -4,9 +4,9 @@ import { useDebounceValue } from 'usehooks-ts'; import { usePrevious } from 'hooks/usePrevious'; import globalize from 'lib/globalize'; import Page from 'components/Page'; -import SearchFields from 'components/search/SearchFields'; -import SearchSuggestions from 'components/search/SearchSuggestions'; -import SearchResults from 'components/search/SearchResults'; +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'; const COLLECTION_TYPE_PARAM = 'collectionType'; const PARENT_ID_PARAM = 'parentId'; diff --git a/src/hooks/searchHook/index.ts b/src/hooks/searchHook/index.ts deleted file mode 100644 index 62ba9b422a..0000000000 --- a/src/hooks/searchHook/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useSearchItems'; -export * from './useSearchSuggestions'; From 5b81d4a2fc9edb658a548ed5fd5f6ea03d161d1b Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:18:39 +0300 Subject: [PATCH 02/13] Combine search queries and make them synchronous --- .../features/search/api/fetchItemsByType.ts | 23 + .../features/search/api/useArtistsSearch.ts | 53 ++ .../features/search/api/useLiveTvSearch.ts | 143 +++++ .../features/search/api/usePeopleSearch.ts | 54 ++ .../features/search/api/useSearchItems.ts | 547 ++---------------- .../features/search/api/useVideoSearch.ts | 61 ++ .../search/components/SearchResults.tsx | 10 +- .../search/constants/liveTvCardOptions.ts | 11 + .../features/search/constants/queryOptions.ts | 12 + .../search/constants/sectionSortOrder.ts | 20 + src/apps/stable/features/search/types.ts | 10 + .../stable/features/search/utils/search.ts | 144 +++++ src/apps/stable/routes/search.tsx | 3 +- 13 files changed, 600 insertions(+), 491 deletions(-) create mode 100644 src/apps/stable/features/search/api/fetchItemsByType.ts create mode 100644 src/apps/stable/features/search/api/useArtistsSearch.ts create mode 100644 src/apps/stable/features/search/api/useLiveTvSearch.ts create mode 100644 src/apps/stable/features/search/api/usePeopleSearch.ts create mode 100644 src/apps/stable/features/search/api/useVideoSearch.ts create mode 100644 src/apps/stable/features/search/constants/liveTvCardOptions.ts create mode 100644 src/apps/stable/features/search/constants/queryOptions.ts create mode 100644 src/apps/stable/features/search/constants/sectionSortOrder.ts create mode 100644 src/apps/stable/features/search/types.ts create mode 100644 src/apps/stable/features/search/utils/search.ts diff --git a/src/apps/stable/features/search/api/fetchItemsByType.ts b/src/apps/stable/features/search/api/fetchItemsByType.ts new file mode 100644 index 0000000000..dfe5edaa16 --- /dev/null +++ b/src/apps/stable/features/search/api/fetchItemsByType.ts @@ -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; +}; diff --git a/src/apps/stable/features/search/api/useArtistsSearch.ts b/src/apps/stable/features/search/api/useArtistsSearch.ts new file mode 100644 index 0000000000..6bcaaa2b9f --- /dev/null +++ b/src/apps/stable/features/search/api/useArtistsSearch.ts @@ -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)) + }); +}; diff --git a/src/apps/stable/features/search/api/useLiveTvSearch.ts b/src/apps/stable/features/search/api/useLiveTvSearch.ts new file mode 100644 index 0000000000..6fc015f96e --- /dev/null +++ b/src/apps/stable/features/search/api/useLiveTvSearch.ts @@ -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) + }); +}; diff --git a/src/apps/stable/features/search/api/usePeopleSearch.ts b/src/apps/stable/features/search/api/usePeopleSearch.ts new file mode 100644 index 0000000000..d485e9362c --- /dev/null +++ b/src/apps/stable/features/search/api/usePeopleSearch.ts @@ -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 + }); +}; diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index d073fac379..0b0662b623 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -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 }); }; diff --git a/src/apps/stable/features/search/api/useVideoSearch.ts b/src/apps/stable/features/search/api/useVideoSearch.ts new file mode 100644 index 0000000000..a4b1a6be13 --- /dev/null +++ b/src/apps/stable/features/search/api/useVideoSearch.ts @@ -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 + }); +}; diff --git a/src/apps/stable/features/search/components/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx index 24e6a83e4f..97d23a9c4a 100644 --- a/src/apps/stable/features/search/components/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -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 = ({ collectionType, query }) => { - const { isLoading, data } = useSearchItems(parentId, collectionType, query); + const { data, isPending } = useSearchItems(parentId, collectionType, query?.trim()); - if (isLoading) return ; + if (isPending) return ; if (!data?.length) { return ( diff --git a/src/apps/stable/features/search/constants/liveTvCardOptions.ts b/src/apps/stable/features/search/constants/liveTvCardOptions.ts new file mode 100644 index 0000000000..5b654436d7 --- /dev/null +++ b/src/apps/stable/features/search/constants/liveTvCardOptions.ts @@ -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 +}; diff --git a/src/apps/stable/features/search/constants/queryOptions.ts b/src/apps/stable/features/search/constants/queryOptions.ts new file mode 100644 index 0000000000..0cc88a6fe4 --- /dev/null +++ b/src/apps/stable/features/search/constants/queryOptions.ts @@ -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 +}; diff --git a/src/apps/stable/features/search/constants/sectionSortOrder.ts b/src/apps/stable/features/search/constants/sectionSortOrder.ts new file mode 100644 index 0000000000..bd5e53b81f --- /dev/null +++ b/src/apps/stable/features/search/constants/sectionSortOrder.ts @@ -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 +]; diff --git a/src/apps/stable/features/search/types.ts b/src/apps/stable/features/search/types.ts new file mode 100644 index 0000000000..1544178bc6 --- /dev/null +++ b/src/apps/stable/features/search/types.ts @@ -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; +}; diff --git a/src/apps/stable/features/search/utils/search.ts b/src/apps/stable/features/search/utils/search.ts new file mode 100644 index 0000000000..8ff0458a8a --- /dev/null +++ b/src/apps/stable/features/search/utils/search.ts @@ -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 + ]; + } +} diff --git a/src/apps/stable/routes/search.tsx b/src/apps/stable/routes/search.tsx index 4cf0b29b72..140c88c69a 100644 --- a/src/apps/stable/routes/search.tsx +++ b/src/apps/stable/routes/search.tsx @@ -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, ''); From cd19e9e5e122c9a7ddef27349961523e859d5b86 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:14:04 +0300 Subject: [PATCH 03/13] Cleanup hooks --- .../features/search/api/useArtistsSearch.ts | 22 ++++++++----------- .../features/search/api/usePeopleSearch.ts | 20 +++++++---------- .../features/search/api/useVideoSearch.ts | 22 ++++++++----------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/apps/stable/features/search/api/useArtistsSearch.ts b/src/apps/stable/features/search/api/useArtistsSearch.ts index 6bcaaa2b9f..aa929d18e9 100644 --- a/src/apps/stable/features/search/api/useArtistsSearch.ts +++ b/src/apps/stable/features/search/api/useArtistsSearch.ts @@ -35,19 +35,15 @@ export const useArtistsSearch = ( return useQuery({ queryKey: ['ArtistsSearch', collectionType, parentId, searchTerm], - queryFn: async ({ signal }) => { - const artistsData = await fetchArtists( - api!, - userId!, - { - parentId: parentId, - searchTerm: searchTerm - }, - { signal } - ); - - return artistsData; - }, + queryFn: ({ signal }) => fetchArtists( + api!, + userId!, + { + parentId: parentId, + searchTerm: searchTerm + }, + { signal } + ), enabled: !!api && !!userId && (!collectionType || isMusic(collectionType)) }); }; diff --git a/src/apps/stable/features/search/api/usePeopleSearch.ts b/src/apps/stable/features/search/api/usePeopleSearch.ts index d485e9362c..92f1f0f3d4 100644 --- a/src/apps/stable/features/search/api/usePeopleSearch.ts +++ b/src/apps/stable/features/search/api/usePeopleSearch.ts @@ -37,18 +37,14 @@ export const usePeopleSearch = ( return useQuery({ queryKey: ['PeopleSearch', collectionType, parentId, searchTerm], - queryFn: async ({ signal }) => { - const peopleData = await fetchPeople( - api!, - userId!, - { - searchTerm: searchTerm - }, - { signal } - ); - - return peopleData; - }, + queryFn: ({ signal }) => fetchPeople( + api!, + userId!, + { + searchTerm: searchTerm + }, + { signal } + ), enabled: !!api && !!userId && isPeopleEnabled }); }; diff --git a/src/apps/stable/features/search/api/useVideoSearch.ts b/src/apps/stable/features/search/api/useVideoSearch.ts index a4b1a6be13..d5711b1987 100644 --- a/src/apps/stable/features/search/api/useVideoSearch.ts +++ b/src/apps/stable/features/search/api/useVideoSearch.ts @@ -43,19 +43,15 @@ export const useVideoSearch = ( return useQuery({ queryKey: ['VideoSearch', collectionType, parentId, searchTerm], - queryFn: async ({ signal }) => { - const videosData = await fetchPeople( - api!, - userId!, - { - parentId: parentId, - searchTerm: searchTerm - }, - { signal } - ); - - return videosData; - }, + queryFn: ({ signal }) => fetchPeople( + api!, + userId!, + { + parentId: parentId, + searchTerm: searchTerm + }, + { signal } + ), enabled: !!api && !!userId && !collectionType }); }; From 1fd314213aafed1636d408c0492ac8932b0b4f69 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:44:03 +0300 Subject: [PATCH 04/13] Make LiveTV synchronous --- .../features/search/api/useLiveTvSearch.ts | 69 ++++++++++--------- .../features/search/api/useSearchItems.ts | 8 +-- .../search/constants/sectionSortOrder.ts | 36 +++++----- src/apps/stable/features/search/types.ts | 2 - .../stable/features/search/utils/search.ts | 8 +-- 5 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/apps/stable/features/search/api/useLiveTvSearch.ts b/src/apps/stable/features/search/api/useLiveTvSearch.ts index 6fc015f96e..bb87e0b6b5 100644 --- a/src/apps/stable/features/search/api/useLiveTvSearch.ts +++ b/src/apps/stable/features/search/api/useLiveTvSearch.ts @@ -9,11 +9,11 @@ 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 fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | undefined, signal: AbortSignal) => { const sections: Section[] = []; // Movies row - const moviesData = await fetchItemsByType( + const movies = fetchItemsByType( api, userId, { @@ -22,14 +22,15 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Movies', moviesData.Items, { - ...LIVETV_CARD_OPTIONS, - shape: CardShape.PortraitOverflow + ).then(moviesData => { + addSection(sections, 'Movies', moviesData.Items, { + ...LIVETV_CARD_OPTIONS, + shape: CardShape.PortraitOverflow + }); }); // Episodes row - const episodesData = await fetchItemsByType( + const episodes = fetchItemsByType( api, userId, { @@ -42,13 +43,14 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Episodes', episodesData.Items, { - ...LIVETV_CARD_OPTIONS + ).then(episodesData => { + addSection(sections, 'Episodes', episodesData.Items, { + ...LIVETV_CARD_OPTIONS + }); }); // Sports row - const sportsData = await fetchItemsByType( + const sports = fetchItemsByType( api, userId, { @@ -57,13 +59,14 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Sports', sportsData.Items, { - ...LIVETV_CARD_OPTIONS + ).then(sportsData => { + addSection(sections, 'Sports', sportsData.Items, { + ...LIVETV_CARD_OPTIONS + }); }); // Kids row - const kidsData = await fetchItemsByType( + const kids = fetchItemsByType( api, userId, { @@ -72,13 +75,14 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Kids', kidsData.Items, { - ...LIVETV_CARD_OPTIONS + ).then(kidsData => { + addSection(sections, 'Kids', kidsData.Items, { + ...LIVETV_CARD_OPTIONS + }); }); // News row - const newsData = await fetchItemsByType( + const news = fetchItemsByType( api, userId, { @@ -87,13 +91,14 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'News', newsData.Items, { - ...LIVETV_CARD_OPTIONS + ).then(newsData => { + addSection(sections, 'News', newsData.Items, { + ...LIVETV_CARD_OPTIONS + }); }); // Programs row - const programsData = await fetchItemsByType( + const programs = fetchItemsByType( api, userId, { @@ -106,13 +111,14 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Programs', programsData.Items, { - ...LIVETV_CARD_OPTIONS + ).then(programsData => { + addSection(sections, 'Programs', programsData.Items, { + ...LIVETV_CARD_OPTIONS + }); }); // Channels row - const channelsData = await fetchItemsByType( + const channels = fetchItemsByType( api, userId, { @@ -120,10 +126,11 @@ const fetchLiveTv = async (api: Api, userId: string | undefined, searchTerm: str searchTerm: searchTerm }, { signal } - ); - addSection(sections, 'Channels', channelsData.Items); + ).then(channelsData => { + addSection(sections, 'Channels', channelsData.Items); + }); - return sections; + return Promise.all([ movies, episodes, sports, kids, news, programs, channels ]).then(() => sections); }; export const useLiveTvSearch = ( @@ -136,7 +143,7 @@ export const useLiveTvSearch = ( return useQuery({ queryKey: ['LiveTv', collectionType, parentId, searchTerm], - queryFn: async ({ signal }) => + queryFn: ({ signal }) => fetchLiveTv(api!, userId!, searchTerm, signal), enabled: !!api && !!userId && !!collectionType && !!isLivetv(collectionType) }); diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index 0b0662b623..cb0a815373 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -33,6 +33,10 @@ export const useSearchItems = ( return useQuery({ queryKey: ['SearchItems', collectionType, parentId, searchTerm], queryFn: async ({ signal }) => { + if (liveTvSections) { + return sortSections(liveTvSections); + } + const sections: Section[] = []; addSection(sections, 'Artists', artists?.Items, { @@ -47,10 +51,6 @@ export const useSearchItems = ( showParentTitle: true }); - if (liveTvSections) { - sections.push(...liveTvSections); - } - const itemTypes: BaseItemKind[] = getItemTypesFromCollectionType(collectionType); const searchData = await fetchItemsByType( diff --git a/src/apps/stable/features/search/constants/sectionSortOrder.ts b/src/apps/stable/features/search/constants/sectionSortOrder.ts index bd5e53b81f..86ddb6733f 100644 --- a/src/apps/stable/features/search/constants/sectionSortOrder.ts +++ b/src/apps/stable/features/search/constants/sectionSortOrder.ts @@ -1,20 +1,18 @@ -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 +export const SEARCH_SECTIONS_SORT_ORDER = [ + 'Movies', + 'Shows', + 'Episodes', + 'People', + 'Playlists', + 'Artists', + 'Albums', + 'Songs', + 'HeaderVideos', + 'Programs', + 'Channels', + 'HeaderPhotoAlbums', + 'Photos', + 'HeaderAudioBooks', + 'Books', + 'Collections' ]; diff --git a/src/apps/stable/features/search/types.ts b/src/apps/stable/features/search/types.ts index 1544178bc6..db2aea2cec 100644 --- a/src/apps/stable/features/search/types.ts +++ b/src/apps/stable/features/search/types.ts @@ -1,10 +1,8 @@ -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; }; diff --git a/src/apps/stable/features/search/utils/search.ts b/src/apps/stable/features/search/utils/search.ts index 8ff0458a8a..ec06b099d9 100644 --- a/src/apps/stable/features/search/utils/search.ts +++ b/src/apps/stable/features/search/utils/search.ts @@ -25,15 +25,15 @@ export function addSection( items: BaseItemDto[] | null | undefined, cardOptions?: CardOptions ) { - if (items && items?.length > 0 && items[0].Type) { - sections.push({ title, items, sectionType: items[0].Type, 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.sectionType); - const indexB = SEARCH_SECTIONS_SORT_ORDER.indexOf(b.sectionType); + const indexA = SEARCH_SECTIONS_SORT_ORDER.indexOf(a.title); + const indexB = SEARCH_SECTIONS_SORT_ORDER.indexOf(b.title); if (indexA > indexB) { return 1; From ad01e8669d02982b9030aa7ad1e277533d4f77d5 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:14:35 +0300 Subject: [PATCH 05/13] Add retry global search link button --- .../stable/features/search/api/useSearchItems.ts | 2 +- .../features/search/components/SearchResults.tsx | 16 +++++++++++++++- src/apps/stable/features/search/utils/search.ts | 4 +--- src/strings/en-us.json | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index cb0a815373..f36be74dfd 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -33,7 +33,7 @@ export const useSearchItems = ( return useQuery({ queryKey: ['SearchItems', collectionType, parentId, searchTerm], queryFn: async ({ signal }) => { - if (liveTvSections) { + if (liveTvSections && collectionType && isLivetv(collectionType)) { return sortSections(liveTvSections); } diff --git a/src/apps/stable/features/search/components/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx index 97d23a9c4a..8b76b5cbc0 100644 --- a/src/apps/stable/features/search/components/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -1,4 +1,4 @@ -import React, { type FC } from 'react'; +import React, { useCallback, type FC } from 'react'; import { useSearchItems } from '../api/useSearchItems'; import globalize from '../../../../../lib/globalize'; import Loading from '../../../../../components/loading/LoadingComponent'; @@ -6,6 +6,8 @@ import SearchResultsRow from './SearchResultsRow'; import { CardShape } from 'utils/card'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { Section } from '../types'; +import LinkButton from 'elements/emby-button/LinkButton'; +import { useLocation, useSearchParams } from 'react-router-dom'; interface SearchResultsProps { parentId?: string; @@ -22,6 +24,13 @@ const SearchResults: FC = ({ query }) => { const { data, isPending } = useSearchItems(parentId, collectionType, query?.trim()); + const location = useLocation(); + const [ searchParams ] = useSearchParams(); + + const getUri = useCallback(() => { + searchParams.delete('collectionType'); + return `${location.pathname}?${searchParams.toString()}`; + }, [ searchParams, location.pathname ]); if (isPending) return ; @@ -29,6 +38,11 @@ const SearchResults: FC = ({ return (
{globalize.translate('SearchResultsEmpty', query)} + {collectionType && ( +
+ {globalize.translate('RetryWithGlobalSearch')} +
+ )}
); } diff --git a/src/apps/stable/features/search/utils/search.ts b/src/apps/stable/features/search/utils/search.ts index ec06b099d9..6de5138cd7 100644 --- a/src/apps/stable/features/search/utils/search.ts +++ b/src/apps/stable/features/search/utils/search.ts @@ -110,7 +110,7 @@ export function getTitleFromType(type: BaseItemKind) { export function getItemTypesFromCollectionType(collectionType: CollectionType | undefined) { switch (collectionType) { case CollectionType.Movies: - return [BaseItemKind.Movie]; + return [ BaseItemKind.Movie ]; case CollectionType.Tvshows: return [ BaseItemKind.Series, @@ -122,8 +122,6 @@ export function getItemTypesFromCollectionType(collectionType: CollectionType | BaseItemKind.MusicAlbum, BaseItemKind.Audio ]; - case CollectionType.Livetv: - return []; default: return [ BaseItemKind.Movie, diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 0204b9817d..02b856225c 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1451,6 +1451,7 @@ "ReplaceExistingImages": "Replace existing images", "ReplaceTrickplayImages": "Replace existing trickplay images", "Retry": "Retry", + "RetryWithGlobalSearch": "Retry with global search", "Reset": "Reset", "ResetPassword": "Reset Password", "ResolutionMatchSource": "Match Source", From d9aa7319be0e90bf329d48a712a16626c0e01a4d Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:29:17 +0300 Subject: [PATCH 06/13] Fix bug with initial keystroke resulting in empty search --- .../features/search/api/useVideoSearch.ts | 4 ++-- .../search/components/SearchResults.tsx | 17 ++++++----------- src/apps/stable/routes/search.tsx | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/apps/stable/features/search/api/useVideoSearch.ts b/src/apps/stable/features/search/api/useVideoSearch.ts index d5711b1987..3eafbd46da 100644 --- a/src/apps/stable/features/search/api/useVideoSearch.ts +++ b/src/apps/stable/features/search/api/useVideoSearch.ts @@ -9,7 +9,7 @@ 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 ( +const fetchVideos = async ( api: Api, userId: string, params?: ItemsApiGetItemsRequest, @@ -43,7 +43,7 @@ export const useVideoSearch = ( return useQuery({ queryKey: ['VideoSearch', collectionType, parentId, searchTerm], - queryFn: ({ signal }) => fetchPeople( + queryFn: ({ signal }) => fetchVideos( api!, userId!, { diff --git a/src/apps/stable/features/search/components/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx index 8b76b5cbc0..1f20c15ebb 100644 --- a/src/apps/stable/features/search/components/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, type FC } from 'react'; +import React, { type FC } from 'react'; import { useSearchItems } from '../api/useSearchItems'; import globalize from '../../../../../lib/globalize'; import Loading from '../../../../../components/loading/LoadingComponent'; @@ -6,8 +6,7 @@ import SearchResultsRow from './SearchResultsRow'; import { CardShape } from 'utils/card'; import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { Section } from '../types'; -import LinkButton from 'elements/emby-button/LinkButton'; -import { useLocation, useSearchParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; interface SearchResultsProps { parentId?: string; @@ -24,13 +23,6 @@ const SearchResults: FC = ({ query }) => { const { data, isPending } = useSearchItems(parentId, collectionType, query?.trim()); - const location = useLocation(); - const [ searchParams ] = useSearchParams(); - - const getUri = useCallback(() => { - searchParams.delete('collectionType'); - return `${location.pathname}?${searchParams.toString()}`; - }, [ searchParams, location.pathname ]); if (isPending) return ; @@ -40,7 +32,10 @@ const SearchResults: FC = ({ {globalize.translate('SearchResultsEmpty', query)} {collectionType && (
- {globalize.translate('RetryWithGlobalSearch')} + {globalize.translate('RetryWithGlobalSearch')}
)} diff --git a/src/apps/stable/routes/search.tsx b/src/apps/stable/routes/search.tsx index 140c88c69a..f06ce927d8 100644 --- a/src/apps/stable/routes/search.tsx +++ b/src/apps/stable/routes/search.tsx @@ -51,7 +51,7 @@ const Search: FC = () => { className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage' > - {!query ? ( + {!debouncedQuery ? ( From 2776e660fa2d9716f8bf1f4bd4a61bc41457a4ff Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 6 Mar 2025 18:53:18 +0300 Subject: [PATCH 07/13] Tiny fixup --- .../stable/features/search/api/useLiveTvSearch.ts | 14 +++++++------- .../stable/features/search/api/useSearchItems.ts | 6 ++---- .../features/search/components/SearchFields.tsx | 8 ++++---- .../features/search/components/SearchResults.tsx | 4 ++-- .../search/components/SearchSuggestions.tsx | 10 +++++----- src/strings/en-us.json | 2 +- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/apps/stable/features/search/api/useLiveTvSearch.ts b/src/apps/stable/features/search/api/useLiveTvSearch.ts index bb87e0b6b5..2f5ce822c8 100644 --- a/src/apps/stable/features/search/api/useLiveTvSearch.ts +++ b/src/apps/stable/features/search/api/useLiveTvSearch.ts @@ -17,7 +17,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isMovie: true, searchTerm: searchTerm }, @@ -34,7 +34,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isMovie: false, isSeries: true, isSports: false, @@ -54,7 +54,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isSports: true, searchTerm: searchTerm }, @@ -70,7 +70,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isKids: true, searchTerm: searchTerm }, @@ -86,7 +86,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isNews: true, searchTerm: searchTerm }, @@ -102,7 +102,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.LiveTvProgram], + includeItemTypes: [ BaseItemKind.LiveTvProgram ], isMovie: false, isSeries: false, isSports: false, @@ -122,7 +122,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | api, userId, { - includeItemTypes: [BaseItemKind.TvChannel], + includeItemTypes: [ BaseItemKind.TvChannel ], searchTerm: searchTerm }, { signal } diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index f36be74dfd..4b7ba84114 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -1,7 +1,5 @@ -import type { - BaseItemDto, - BaseItemKind -} from '@jellyfin/sdk/lib/generated-client'; +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'; diff --git a/src/apps/stable/features/search/components/SearchFields.tsx b/src/apps/stable/features/search/components/SearchFields.tsx index cd36dc2386..705144aff5 100644 --- a/src/apps/stable/features/search/components/SearchFields.tsx +++ b/src/apps/stable/features/search/components/SearchFields.tsx @@ -1,9 +1,9 @@ import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react'; -import AlphaPicker from '../../../../../components/alphaPicker/AlphaPickerComponent'; +import AlphaPicker from 'components/alphaPicker/AlphaPickerComponent'; import Input from 'elements/emby-input/Input'; -import globalize from '../../../../../lib/globalize'; -import layoutManager from '../../../../../components/layoutManager'; -import browser from '../../../../../scripts/browser'; +import globalize from 'lib/globalize'; +import layoutManager from 'components/layoutManager'; +import browser from 'scripts/browser'; import 'material-design-icons-iconfont'; import 'styles/flexstyles.scss'; import './searchfields.scss'; diff --git a/src/apps/stable/features/search/components/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx index 1f20c15ebb..d6f73a9ad7 100644 --- a/src/apps/stable/features/search/components/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -1,7 +1,7 @@ import React, { type FC } from 'react'; import { useSearchItems } from '../api/useSearchItems'; -import globalize from '../../../../../lib/globalize'; -import Loading from '../../../../../components/loading/LoadingComponent'; +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'; diff --git a/src/apps/stable/features/search/components/SearchSuggestions.tsx b/src/apps/stable/features/search/components/SearchSuggestions.tsx index d222443fdc..a63514d157 100644 --- a/src/apps/stable/features/search/components/SearchSuggestions.tsx +++ b/src/apps/stable/features/search/components/SearchSuggestions.tsx @@ -1,21 +1,21 @@ import React, { FunctionComponent } from 'react'; import Loading from 'components/loading/LoadingComponent'; -import { appRouter } from '../../../../../components/router/appRouter'; +import { appRouter } from 'components/router/appRouter'; import { useSearchSuggestions } from '../api/useSearchSuggestions'; 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 = { parentId?: string | null; }; const SearchSuggestions: FunctionComponent = ({ parentId }) => { - const { isLoading, data: suggestions } = useSearchSuggestions(parentId || undefined); + const { data: suggestions, isPending } = useSearchSuggestions(parentId || undefined); - if (isLoading) return ; + if (isPending) return ; return (
Date: Thu, 6 Mar 2025 18:58:15 +0300 Subject: [PATCH 08/13] Disable refetch on window refocus for search suggestions --- .../stable/features/search/api/useSearchSuggestions.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/apps/stable/features/search/api/useSearchSuggestions.ts b/src/apps/stable/features/search/api/useSearchSuggestions.ts index ea6e7b77e0..126783927a 100644 --- a/src/apps/stable/features/search/api/useSearchSuggestions.ts +++ b/src/apps/stable/features/search/api/useSearchSuggestions.ts @@ -7,14 +7,11 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'hooks/useApi'; const fetchGetItems = async ( - api?: Api, - userId?: string, + api: Api, + userId: string, parentId?: string, 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( { userId: userId, @@ -43,7 +40,8 @@ export const useSearchSuggestions = (parentId?: string) => { return useQuery({ queryKey: ['SearchSuggestions', { parentId }], queryFn: ({ signal }) => - fetchGetItems(api, userId, parentId, { signal }), + fetchGetItems(api!, userId!, parentId, { signal }), + refetchOnWindowFocus: false, enabled: !!api && !!userId }); }; From f7e09708fc7ce45e39bd2d12136fb7ad3fd79ed6 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:29:04 +0300 Subject: [PATCH 09/13] Refactor query keys --- src/apps/stable/features/search/api/useArtistsSearch.ts | 2 +- src/apps/stable/features/search/api/useLiveTvSearch.ts | 2 +- src/apps/stable/features/search/api/usePeopleSearch.ts | 2 +- src/apps/stable/features/search/api/useSearchItems.ts | 2 +- src/apps/stable/features/search/api/useVideoSearch.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apps/stable/features/search/api/useArtistsSearch.ts b/src/apps/stable/features/search/api/useArtistsSearch.ts index aa929d18e9..def31ee0dc 100644 --- a/src/apps/stable/features/search/api/useArtistsSearch.ts +++ b/src/apps/stable/features/search/api/useArtistsSearch.ts @@ -34,7 +34,7 @@ export const useArtistsSearch = ( const userId = user?.Id; return useQuery({ - queryKey: ['ArtistsSearch', collectionType, parentId, searchTerm], + queryKey: ['Search', 'Artists', collectionType, parentId, searchTerm], queryFn: ({ signal }) => fetchArtists( api!, userId!, diff --git a/src/apps/stable/features/search/api/useLiveTvSearch.ts b/src/apps/stable/features/search/api/useLiveTvSearch.ts index 2f5ce822c8..6cdde3602e 100644 --- a/src/apps/stable/features/search/api/useLiveTvSearch.ts +++ b/src/apps/stable/features/search/api/useLiveTvSearch.ts @@ -142,7 +142,7 @@ export const useLiveTvSearch = ( const userId = user?.Id; return useQuery({ - queryKey: ['LiveTv', collectionType, parentId, searchTerm], + queryKey: ['Search', 'LiveTv', collectionType, parentId, searchTerm], queryFn: ({ signal }) => fetchLiveTv(api!, userId!, searchTerm, signal), enabled: !!api && !!userId && !!collectionType && !!isLivetv(collectionType) diff --git a/src/apps/stable/features/search/api/usePeopleSearch.ts b/src/apps/stable/features/search/api/usePeopleSearch.ts index 92f1f0f3d4..943b611478 100644 --- a/src/apps/stable/features/search/api/usePeopleSearch.ts +++ b/src/apps/stable/features/search/api/usePeopleSearch.ts @@ -36,7 +36,7 @@ export const usePeopleSearch = ( const isPeopleEnabled = (!collectionType || isMovies(collectionType) || isTVShows(collectionType)); return useQuery({ - queryKey: ['PeopleSearch', collectionType, parentId, searchTerm], + queryKey: ['Search', 'People', collectionType, parentId, searchTerm], queryFn: ({ signal }) => fetchPeople( api!, userId!, diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index 4b7ba84114..a799b4fa68 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -29,7 +29,7 @@ export const useSearchItems = ( const isLiveTvEnabled = !isLiveTvPending || !collectionType || !isLivetv(collectionType); return useQuery({ - queryKey: ['SearchItems', collectionType, parentId, searchTerm], + queryKey: ['Search', 'Items', collectionType, parentId, searchTerm], queryFn: async ({ signal }) => { if (liveTvSections && collectionType && isLivetv(collectionType)) { return sortSections(liveTvSections); diff --git a/src/apps/stable/features/search/api/useVideoSearch.ts b/src/apps/stable/features/search/api/useVideoSearch.ts index 3eafbd46da..4ae736c076 100644 --- a/src/apps/stable/features/search/api/useVideoSearch.ts +++ b/src/apps/stable/features/search/api/useVideoSearch.ts @@ -42,7 +42,7 @@ export const useVideoSearch = ( const userId = user?.Id; return useQuery({ - queryKey: ['VideoSearch', collectionType, parentId, searchTerm], + queryKey: ['Search', 'Video', collectionType, parentId, searchTerm], queryFn: ({ signal }) => fetchVideos( api!, userId!, From 69df532c276234c0cec237cbc57442f3818e7e7c Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:11:59 +0300 Subject: [PATCH 10/13] Fix invalid class names --- src/apps/stable/features/search/components/SearchResults.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/stable/features/search/components/SearchResults.tsx b/src/apps/stable/features/search/components/SearchResults.tsx index d6f73a9ad7..47ce8f030c 100644 --- a/src/apps/stable/features/search/components/SearchResults.tsx +++ b/src/apps/stable/features/search/components/SearchResults.tsx @@ -62,7 +62,7 @@ const SearchResults: FC = ({ }; return ( -
+
{data.map((section, index) => renderSection(section, index))}
); From cdd330a01f58c92dbd45a77bb06b3014673c8379 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:18:39 +0300 Subject: [PATCH 11/13] Separate LiveTvProgram to a standalone query --- .../features/search/api/useProgramsSearch.ts | 50 +++++++++++++++++++ .../features/search/api/useSearchItems.ts | 10 +++- .../stable/features/search/utils/search.ts | 1 - 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/apps/stable/features/search/api/useProgramsSearch.ts diff --git a/src/apps/stable/features/search/api/useProgramsSearch.ts b/src/apps/stable/features/search/api/useProgramsSearch.ts new file mode 100644 index 0000000000..560c2e4682 --- /dev/null +++ b/src/apps/stable/features/search/api/useProgramsSearch.ts @@ -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: parentId, + searchTerm: searchTerm + }, + { signal } + ), + enabled: !!api && !!userId && !collectionType + }); +}; diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index a799b4fa68..0f7b44ad18 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -10,6 +10,8 @@ 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, @@ -19,6 +21,7 @@ export const useSearchItems = ( 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; @@ -26,6 +29,7 @@ export const useSearchItems = ( 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({ @@ -41,6 +45,10 @@ export const useSearchItems = ( coverImage: true }); + addSection(sections, 'Programs', programs?.Items, { + ...LIVETV_CARD_OPTIONS + }); + addSection(sections, 'People', people?.Items, { coverImage: true }); @@ -77,6 +85,6 @@ export const useSearchItems = ( return sortSections(sections); }, - enabled: !!api && !!userId && !!isArtistsEnabled && !!isPeopleEnabled && !!isVideosEnabled && !!isLiveTvEnabled + enabled: !!api && !!userId && !!isArtistsEnabled && !!isPeopleEnabled && !!isVideosEnabled && !!isLiveTvEnabled && !!isProgramsEnabled }); }; diff --git a/src/apps/stable/features/search/utils/search.ts b/src/apps/stable/features/search/utils/search.ts index 6de5138cd7..441b0a531d 100644 --- a/src/apps/stable/features/search/utils/search.ts +++ b/src/apps/stable/features/search/utils/search.ts @@ -130,7 +130,6 @@ export function getItemTypesFromCollectionType(collectionType: CollectionType | BaseItemKind.Playlist, BaseItemKind.MusicAlbum, BaseItemKind.Audio, - BaseItemKind.LiveTvProgram, BaseItemKind.TvChannel, BaseItemKind.PhotoAlbum, BaseItemKind.Photo, From 1a33b510ce2355c6e1117fc170f2ea66ab2c14f6 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:32:16 +0300 Subject: [PATCH 12/13] Organize long enabled expression --- src/apps/stable/features/search/api/useSearchItems.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index 0f7b44ad18..e16bdd67f7 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -85,6 +85,14 @@ export const useSearchItems = ( return sortSections(sections); }, - enabled: !!api && !!userId && !!isArtistsEnabled && !!isPeopleEnabled && !!isVideosEnabled && !!isLiveTvEnabled && !!isProgramsEnabled + enabled: ( + !!api + && !!userId + && !!isArtistsEnabled + && !!isPeopleEnabled + && !!isVideosEnabled + && !!isLiveTvEnabled + && !!isProgramsEnabled + ) }); }; From 34f5be944080ecc6075301021ab0b233ddae7dda Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:07:51 +0300 Subject: [PATCH 13/13] Use shorthand syntax --- .../stable/features/search/api/fetchItemsByType.ts | 2 +- .../stable/features/search/api/useArtistsSearch.ts | 6 +++--- .../stable/features/search/api/useLiveTvSearch.ts | 14 +++++++------- .../stable/features/search/api/usePeopleSearch.ts | 4 ++-- .../features/search/api/useProgramsSearch.ts | 4 ++-- .../stable/features/search/api/useSearchItems.ts | 4 ++-- .../features/search/api/useSearchSuggestions.ts | 4 ++-- .../stable/features/search/api/useVideoSearch.ts | 6 +++--- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/apps/stable/features/search/api/fetchItemsByType.ts b/src/apps/stable/features/search/api/fetchItemsByType.ts index dfe5edaa16..ec52347dbf 100644 --- a/src/apps/stable/features/search/api/fetchItemsByType.ts +++ b/src/apps/stable/features/search/api/fetchItemsByType.ts @@ -13,7 +13,7 @@ export const fetchItemsByType = async ( const response = await getItemsApi(api).getItems( { ...QUERY_OPTIONS, - userId: userId, + userId, recursive: true, ...params }, diff --git a/src/apps/stable/features/search/api/useArtistsSearch.ts b/src/apps/stable/features/search/api/useArtistsSearch.ts index def31ee0dc..4be8c70b57 100644 --- a/src/apps/stable/features/search/api/useArtistsSearch.ts +++ b/src/apps/stable/features/search/api/useArtistsSearch.ts @@ -17,7 +17,7 @@ const fetchArtists = async ( const response = await getArtistsApi(api).getArtists( { ...QUERY_OPTIONS, - userId: userId, + userId, ...params }, options @@ -39,8 +39,8 @@ export const useArtistsSearch = ( api!, userId!, { - parentId: parentId, - searchTerm: searchTerm + parentId, + searchTerm }, { signal } ), diff --git a/src/apps/stable/features/search/api/useLiveTvSearch.ts b/src/apps/stable/features/search/api/useLiveTvSearch.ts index 6cdde3602e..e4fef3386e 100644 --- a/src/apps/stable/features/search/api/useLiveTvSearch.ts +++ b/src/apps/stable/features/search/api/useLiveTvSearch.ts @@ -19,7 +19,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | { includeItemTypes: [ BaseItemKind.LiveTvProgram ], isMovie: true, - searchTerm: searchTerm + searchTerm }, { signal } ).then(moviesData => { @@ -40,7 +40,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | isSports: false, isKids: false, isNews: false, - searchTerm: searchTerm + searchTerm }, { signal } ).then(episodesData => { @@ -56,7 +56,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | { includeItemTypes: [ BaseItemKind.LiveTvProgram ], isSports: true, - searchTerm: searchTerm + searchTerm }, { signal } ).then(sportsData => { @@ -72,7 +72,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | { includeItemTypes: [ BaseItemKind.LiveTvProgram ], isKids: true, - searchTerm: searchTerm + searchTerm }, { signal } ).then(kidsData => { @@ -88,7 +88,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | { includeItemTypes: [ BaseItemKind.LiveTvProgram ], isNews: true, - searchTerm: searchTerm + searchTerm }, { signal } ).then(newsData => { @@ -108,7 +108,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | isSports: false, isKids: false, isNews: false, - searchTerm: searchTerm + searchTerm }, { signal } ).then(programsData => { @@ -123,7 +123,7 @@ const fetchLiveTv = (api: Api, userId: string | undefined, searchTerm: string | userId, { includeItemTypes: [ BaseItemKind.TvChannel ], - searchTerm: searchTerm + searchTerm }, { signal } ).then(channelsData => { diff --git a/src/apps/stable/features/search/api/usePeopleSearch.ts b/src/apps/stable/features/search/api/usePeopleSearch.ts index 943b611478..3dd1a74a59 100644 --- a/src/apps/stable/features/search/api/usePeopleSearch.ts +++ b/src/apps/stable/features/search/api/usePeopleSearch.ts @@ -17,7 +17,7 @@ const fetchPeople = async ( const response = await getPersonsApi(api).getPersons( { ...QUERY_OPTIONS, - userId: userId, + userId, ...params }, options @@ -41,7 +41,7 @@ export const usePeopleSearch = ( api!, userId!, { - searchTerm: searchTerm + searchTerm }, { signal } ), diff --git a/src/apps/stable/features/search/api/useProgramsSearch.ts b/src/apps/stable/features/search/api/useProgramsSearch.ts index 560c2e4682..23d91381b5 100644 --- a/src/apps/stable/features/search/api/useProgramsSearch.ts +++ b/src/apps/stable/features/search/api/useProgramsSearch.ts @@ -40,8 +40,8 @@ export const useProgramsSearch = ( api!, userId!, { - parentId: parentId, - searchTerm: searchTerm + parentId, + searchTerm }, { signal } ), diff --git a/src/apps/stable/features/search/api/useSearchItems.ts b/src/apps/stable/features/search/api/useSearchItems.ts index e16bdd67f7..b528290d87 100644 --- a/src/apps/stable/features/search/api/useSearchItems.ts +++ b/src/apps/stable/features/search/api/useSearchItems.ts @@ -64,8 +64,8 @@ export const useSearchItems = ( userId, { includeItemTypes: itemTypes, - parentId: parentId, - searchTerm: searchTerm, + parentId, + searchTerm, limit: 800 }, { signal } diff --git a/src/apps/stable/features/search/api/useSearchSuggestions.ts b/src/apps/stable/features/search/api/useSearchSuggestions.ts index 126783927a..af079fab11 100644 --- a/src/apps/stable/features/search/api/useSearchSuggestions.ts +++ b/src/apps/stable/features/search/api/useSearchSuggestions.ts @@ -14,7 +14,7 @@ const fetchGetItems = async ( ) => { const response = await getItemsApi(api).getItems( { - userId: userId, + userId, sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random], includeItemTypes: [ BaseItemKind.Movie, @@ -25,7 +25,7 @@ const fetchGetItems = async ( recursive: true, imageTypeLimit: 0, enableImages: false, - parentId: parentId, + parentId, enableTotalRecordCount: false }, options diff --git a/src/apps/stable/features/search/api/useVideoSearch.ts b/src/apps/stable/features/search/api/useVideoSearch.ts index 4ae736c076..06eaaf25e7 100644 --- a/src/apps/stable/features/search/api/useVideoSearch.ts +++ b/src/apps/stable/features/search/api/useVideoSearch.ts @@ -18,7 +18,7 @@ const fetchVideos = async ( const response = await getItemsApi(api).getItems( { ...QUERY_OPTIONS, - userId: userId, + userId, recursive: true, mediaTypes: [MediaType.Video], excludeItemTypes: [ @@ -47,8 +47,8 @@ export const useVideoSearch = ( api!, userId!, { - parentId: parentId, - searchTerm: searchTerm + parentId, + searchTerm }, { signal } ),