import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'; import type { ApiClient } from 'jellyfin-apiclient'; import classNames from 'classnames'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import globalize from '../../scripts/globalize'; import ServerConnections from '../ServerConnections'; import SearchResultsRow from './SearchResultsRow'; import Loading from '../loading/LoadingComponent'; type SearchResultsProps = { serverId?: string; parentId?: string | null; collectionType?: string | null; query?: string; }; const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({ ...result, Items: result.Items || [] }); const isMovies = (collectionType: string) => collectionType === 'movies'; const isMusic = (collectionType: string) => collectionType === 'music'; const isTVShows = (collectionType: string) => collectionType === 'tvshows' || collectionType === 'tv'; /* * React component to display search result rows for global search and non-live tv library search */ const SearchResults: FunctionComponent = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => { const [ movies, setMovies ] = useState([]); const [ shows, setShows ] = useState([]); const [ episodes, setEpisodes ] = useState([]); const [ videos, setVideos ] = useState([]); const [ programs, setPrograms ] = useState([]); const [ channels, setChannels ] = useState([]); const [ playlists, setPlaylists ] = useState([]); const [ artists, setArtists ] = useState([]); const [ albums, setAlbums ] = useState([]); const [ songs, setSongs ] = useState([]); const [ photoAlbums, setPhotoAlbums ] = useState([]); const [ photos, setPhotos ] = useState([]); const [ audioBooks, setAudioBooks ] = useState([]); const [ books, setBooks ] = useState([]); const [ people, setPeople ] = useState([]); const [ collections, setCollections ] = useState([]); const [isLoading, setIsLoading] = useState(false); const getDefaultParameters = useCallback(() => ({ ParentId: parentId, searchTerm: query, Limit: 100, Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount', Recursive: true, EnableTotalRecordCount: false, ImageTypeLimit: 1, IncludePeople: false, IncludeMedia: false, IncludeGenres: false, IncludeStudios: false, IncludeArtists: false }), [parentId, query]); const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => ( apiClient?.getArtists( apiClient.getCurrentUserId(), { ...getDefaultParameters(), IncludeArtists: true, ...params } ).then(ensureNonNullItems) ), [getDefaultParameters]); const fetchItems = useCallback((apiClient: ApiClient, params = {}) => ( apiClient?.getItems( apiClient.getCurrentUserId(), { ...getDefaultParameters(), IncludeMedia: true, ...params } ).then(ensureNonNullItems) ), [getDefaultParameters]); const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => ( apiClient?.getPeople( apiClient.getCurrentUserId(), { ...getDefaultParameters(), IncludePeople: true, ...params } ).then(ensureNonNullItems) ), [getDefaultParameters]); useEffect(() => { // Reset state setMovies([]); setShows([]); setEpisodes([]); setVideos([]); setPrograms([]); setChannels([]); setPlaylists([]); setArtists([]); setAlbums([]); setSongs([]); setPhotoAlbums([]); setPhotos([]); setAudioBooks([]); setBooks([]); setPeople([]); setCollections([]); if (!query) { setIsLoading(false); return; } setIsLoading(true); const apiClient = ServerConnections.getApiClient(serverId); const fetchPromises = []; // Movie libraries if (!collectionType || isMovies(collectionType)) { fetchPromises.push( // Movies row fetchItems(apiClient, { IncludeItemTypes: 'Movie' }) .then(result => setMovies(result.Items)) .catch(() => setMovies([])) ); } // TV Show libraries if (!collectionType || isTVShows(collectionType)) { fetchPromises.push( // Shows row fetchItems(apiClient, { IncludeItemTypes: 'Series' }) .then(result => setShows(result.Items)) .catch(() => setShows([])), // Episodes row fetchItems(apiClient, { IncludeItemTypes: 'Episode' }) .then(result => setEpisodes(result.Items)) .catch(() => setEpisodes([])) ); } // People are included for Movies and TV Shows if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) { fetchPromises.push( // People row fetchPeople(apiClient) .then(result => setPeople(result.Items)) .catch(() => setPeople([])) ); } // Music libraries if (!collectionType || isMusic(collectionType)) { fetchPromises.push( // Playlists row fetchItems(apiClient, { IncludeItemTypes: 'Playlist' }) .then(results => setPlaylists(results.Items)) .catch(() => setPlaylists([])), // Artists row fetchArtists(apiClient) .then(result => setArtists(result.Items)) .catch(() => setArtists([])), // Albums row fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' }) .then(result => setAlbums(result.Items)) .catch(() => setAlbums([])), // Songs row fetchItems(apiClient, { IncludeItemTypes: 'Audio' }) .then(result => setSongs(result.Items)) .catch(() => setSongs([])) ); } // Other libraries do not support in-library search currently if (!collectionType) { fetchPromises.push( // Videos row fetchItems(apiClient, { MediaTypes: 'Video', ExcludeItemTypes: 'Movie,Episode,TvChannel' }) .then(result => setVideos(result.Items)) .catch(() => setVideos([])), // Programs row fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' }) .then(result => setPrograms(result.Items)) .catch(() => setPrograms([])), // Channels row fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' }) .then(result => setChannels(result.Items)) .catch(() => setChannels([])), // Photo Albums row fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' }) .then(result => setPhotoAlbums(result.Items)) .catch(() => setPhotoAlbums([])), // Photos row fetchItems(apiClient, { IncludeItemTypes: 'Photo' }) .then(result => setPhotos(result.Items)) .catch(() => setPhotos([])), // Audio Books row fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' }) .then(result => setAudioBooks(result.Items)) .catch(() => setAudioBooks([])), // Books row fetchItems(apiClient, { IncludeItemTypes: 'Book' }) .then(result => setBooks(result.Items)) .catch(() => setBooks([])), // Collections row fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' }) .then(result => setCollections(result.Items)) .catch(() => setCollections([])) ); } Promise.all(fetchPromises) .then(() => { setIsLoading(false); // Set loading to false when all fetch calls are done }) .catch((error) => { console.error('An error occurred while fetching data:', error); setIsLoading(false); // Set loading to false even if an error occurs }); }, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]); const allEmpty = [movies, shows, episodes, videos, programs, channels, playlists, artists, albums, songs, photoAlbums, photos, audioBooks, books, people, collections].every(arr => arr.length === 0); return (
{isLoading ? ( ) : ( <> {allEmpty && !isLoading && (
{globalize.translate('SearchResultsEmpty', query)}
)} )}
); }; export default SearchResults;