jellyfish-web/src/components/search/SearchResults.tsx
2024-01-11 02:24:16 -05:00

347 lines
14 KiB
TypeScript

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<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
const [ videos, setVideos ] = useState<BaseItemDto[]>([]);
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
const [ playlists, setPlaylists ] = useState<BaseItemDto[]>([]);
const [ artists, setArtists ] = useState<BaseItemDto[]>([]);
const [ albums, setAlbums ] = useState<BaseItemDto[]>([]);
const [ songs, setSongs ] = useState<BaseItemDto[]>([]);
const [ photoAlbums, setPhotoAlbums ] = useState<BaseItemDto[]>([]);
const [ photos, setPhotos ] = useState<BaseItemDto[]>([]);
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
const [isLoading, setIsLoading] = useState(false);
const getDefaultParameters = useCallback(() => ({
ParentId: parentId,
searchTerm: query,
Limit: 100,
Fields: 'PrimaryImageAspectRatio,CanDelete,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 (
<div
className={classNames(
'searchResults',
'padded-bottom-page',
'padded-top',
{ 'hide': !query || collectionType === 'livetv' }
)}
>
{isLoading ? (
<Loading />
) : (
<>
<SearchResultsRow
title={globalize.translate('Movies')}
items={movies}
cardOptions={{ showYear: true }}
/>
<SearchResultsRow
title={globalize.translate('Shows')}
items={shows}
cardOptions={{ showYear: true }}
/>
<SearchResultsRow
title={globalize.translate('Episodes')}
items={episodes}
cardOptions={{
coverImage: true,
showParentTitle: true
}}
/>
<SearchResultsRow
title={globalize.translate('HeaderVideos')}
items={videos}
cardOptions={{ showParentTitle: true }}
/>
<SearchResultsRow
title={globalize.translate('Programs')}
items={programs}
cardOptions={{
preferThumb: true,
inheritThumb: false,
showParentTitleOrTitle: true,
showTitle: false,
coverImage: true,
overlayMoreButton: true,
showAirTime: true,
showAirDateTime: true,
showChannelName: true
}}
/>
<SearchResultsRow
title={globalize.translate('Channels')}
items={channels}
cardOptions={{ shape: 'square' }}
/>
<SearchResultsRow
title={globalize.translate('Playlists')}
items={playlists}
/>
<SearchResultsRow
title={globalize.translate('Artists')}
items={artists}
cardOptions={{ coverImage: true }}
/>
<SearchResultsRow
title={globalize.translate('Albums')}
items={albums}
cardOptions={{ showParentTitle: true }}
/>
<SearchResultsRow
title={globalize.translate('Songs')}
items={songs}
cardOptions={{ showParentTitle: true }}
/>
<SearchResultsRow
title={globalize.translate('HeaderPhotoAlbums')}
items={photoAlbums}
/>
<SearchResultsRow
title={globalize.translate('Photos')}
items={photos}
/>
<SearchResultsRow
title={globalize.translate('HeaderAudioBooks')}
items={audioBooks}
/>
<SearchResultsRow
title={globalize.translate('Books')}
items={books}
/>
<SearchResultsRow
title={globalize.translate('Collections')}
items={collections}
/>
<SearchResultsRow
title={globalize.translate('People')}
items={people}
cardOptions={{ coverImage: true }}
/>
{allEmpty && query && !isLoading && (
<div className='sorry-text'>{globalize.translate('SearchResultsEmpty', query)}</div>
)}
</>
)}
</div>
);
};
export default SearchResults;