mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix: handle search results with collections (#3393)
* feat: handle search collection * Update server/utils/typeHelpers.ts Co-authored-by: Danshil Kokil Mungur <danshil.mungur@gmail.com> * fix: modified title card to show collection instead of movies --------- Co-authored-by: Danshil Kokil Mungur <danshil.mungur@gmail.com> Co-authored-by: Brandon <cohbrandon@gmail.com>
This commit is contained in:
@@ -28,6 +28,18 @@ export interface TmdbTvResult extends TmdbMediaResult {
|
|||||||
first_air_date: string;
|
first_air_date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TmdbCollectionResult {
|
||||||
|
id: number;
|
||||||
|
media_type: 'collection';
|
||||||
|
title: string;
|
||||||
|
original_title: string;
|
||||||
|
adult: boolean;
|
||||||
|
poster_path?: string;
|
||||||
|
backdrop_path?: string;
|
||||||
|
overview: string;
|
||||||
|
original_language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TmdbPersonResult {
|
export interface TmdbPersonResult {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -45,7 +57,12 @@ interface TmdbPaginatedResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
results: (
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
TmdbCollectionResult,
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbPersonDetails,
|
TmdbPersonDetails,
|
||||||
@@ -9,7 +10,7 @@ import type {
|
|||||||
import { MediaType as MainMediaType } from '@server/constants/media';
|
import { MediaType as MainMediaType } from '@server/constants/media';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
|
|
||||||
export type MediaType = 'tv' | 'movie' | 'person';
|
export type MediaType = 'tv' | 'movie' | 'person' | 'collection';
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -43,6 +44,18 @@ export interface TvResult extends SearchResult {
|
|||||||
firstAirDate: string;
|
firstAirDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CollectionResult {
|
||||||
|
id: number;
|
||||||
|
mediaType: 'collection';
|
||||||
|
title: string;
|
||||||
|
originalTitle: string;
|
||||||
|
adult: boolean;
|
||||||
|
posterPath?: string;
|
||||||
|
backdropPath?: string;
|
||||||
|
overview: string;
|
||||||
|
originalLanguage: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PersonResult {
|
export interface PersonResult {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -53,7 +66,7 @@ export interface PersonResult {
|
|||||||
knownFor: (MovieResult | TvResult)[];
|
knownFor: (MovieResult | TvResult)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Results = MovieResult | TvResult | PersonResult;
|
export type Results = MovieResult | TvResult | PersonResult | CollectionResult;
|
||||||
|
|
||||||
export const mapMovieResult = (
|
export const mapMovieResult = (
|
||||||
movieResult: TmdbMovieResult,
|
movieResult: TmdbMovieResult,
|
||||||
@@ -99,6 +112,20 @@ export const mapTvResult = (
|
|||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapCollectionResult = (
|
||||||
|
collectionResult: TmdbCollectionResult
|
||||||
|
): CollectionResult => ({
|
||||||
|
id: collectionResult.id,
|
||||||
|
mediaType: collectionResult.media_type || 'collection',
|
||||||
|
adult: collectionResult.adult,
|
||||||
|
originalLanguage: collectionResult.original_language,
|
||||||
|
originalTitle: collectionResult.original_title,
|
||||||
|
title: collectionResult.title,
|
||||||
|
overview: collectionResult.overview,
|
||||||
|
backdropPath: collectionResult.backdrop_path,
|
||||||
|
posterPath: collectionResult.poster_path,
|
||||||
|
});
|
||||||
|
|
||||||
export const mapPersonResult = (
|
export const mapPersonResult = (
|
||||||
personResult: TmdbPersonResult
|
personResult: TmdbPersonResult
|
||||||
): PersonResult => ({
|
): PersonResult => ({
|
||||||
@@ -118,7 +145,12 @@ export const mapPersonResult = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mapSearchResults = (
|
export const mapSearchResults = (
|
||||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
|
results: (
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
)[],
|
||||||
media?: Media[]
|
media?: Media[]
|
||||||
): Results[] =>
|
): Results[] =>
|
||||||
results.map((result) => {
|
results.map((result) => {
|
||||||
@@ -139,6 +171,8 @@ export const mapSearchResults = (
|
|||||||
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
|
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
case 'collection':
|
||||||
|
return mapCollectionResult(result);
|
||||||
default:
|
default:
|
||||||
return mapPersonResult(result);
|
return mapPersonResult(result);
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,13 @@ import { getSettings } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapProductionCompany } from '@server/models/Movie';
|
import { mapProductionCompany } from '@server/models/Movie';
|
||||||
import {
|
import {
|
||||||
|
mapCollectionResult,
|
||||||
mapMovieResult,
|
mapMovieResult,
|
||||||
mapPersonResult,
|
mapPersonResult,
|
||||||
mapTvResult,
|
mapTvResult,
|
||||||
} from '@server/models/Search';
|
} from '@server/models/Search';
|
||||||
import { mapNetwork } from '@server/models/Tv';
|
import { mapNetwork } from '@server/models/Tv';
|
||||||
import { isMovie, isPerson } from '@server/utils/typeHelpers';
|
import { isCollection, isMovie, isPerson } from '@server/utils/typeHelpers';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -647,6 +648,8 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
)
|
)
|
||||||
: isPerson(result)
|
: isPerson(result)
|
||||||
? mapPersonResult(result)
|
? mapPersonResult(result)
|
||||||
|
: isCollection(result)
|
||||||
|
? mapCollectionResult(result)
|
||||||
: mapTvResult(
|
: mapTvResult(
|
||||||
result,
|
result,
|
||||||
media.find(
|
media.find(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
TmdbCollectionResult,
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbPersonDetails,
|
TmdbPersonDetails,
|
||||||
@@ -8,17 +9,35 @@ import type {
|
|||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/themoviedb/interfaces';
|
||||||
|
|
||||||
export const isMovie = (
|
export const isMovie = (
|
||||||
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
movie:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
): movie is TmdbMovieResult => {
|
): movie is TmdbMovieResult => {
|
||||||
return (movie as TmdbMovieResult).title !== undefined;
|
return (movie as TmdbMovieResult).title !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPerson = (
|
export const isPerson = (
|
||||||
person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
person:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
): person is TmdbPersonResult => {
|
): person is TmdbPersonResult => {
|
||||||
return (person as TmdbPersonResult).known_for !== undefined;
|
return (person as TmdbPersonResult).known_for !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isCollection = (
|
||||||
|
collection:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
): collection is TmdbCollectionResult => {
|
||||||
|
return (collection as TmdbCollectionResult).media_type === 'collection';
|
||||||
|
};
|
||||||
|
|
||||||
export const isMovieDetails = (
|
export const isMovieDetails = (
|
||||||
movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
|
movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
|
||||||
): movie is TmdbMovieDetails => {
|
): movie is TmdbMovieDetails => {
|
||||||
|
@@ -5,6 +5,7 @@ import useVerticalScroll from '@app/hooks/useVerticalScroll';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
import type {
|
import type {
|
||||||
|
CollectionResult,
|
||||||
MovieResult,
|
MovieResult,
|
||||||
PersonResult,
|
PersonResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
@@ -12,7 +13,7 @@ import type {
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
type ListViewProps = {
|
type ListViewProps = {
|
||||||
items?: (TvResult | MovieResult | PersonResult)[];
|
items?: (TvResult | MovieResult | PersonResult | CollectionResult)[];
|
||||||
plexItems?: WatchlistItem[];
|
plexItems?: WatchlistItem[];
|
||||||
isEmpty?: boolean;
|
isEmpty?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -90,6 +91,18 @@ const ListView = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'collection':
|
||||||
|
titleCard = (
|
||||||
|
<TitleCard
|
||||||
|
id={title.id}
|
||||||
|
image={title.posterPath}
|
||||||
|
summary={title.overview}
|
||||||
|
title={title.title}
|
||||||
|
mediaType={title.mediaType}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
case 'person':
|
case 'person':
|
||||||
titleCard = (
|
titleCard = (
|
||||||
<PersonCard
|
<PersonCard
|
||||||
|
@@ -23,7 +23,7 @@ interface TitleCardProps {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
year?: string;
|
year?: string;
|
||||||
title: string;
|
title: string;
|
||||||
userScore: number;
|
userScore?: number;
|
||||||
mediaType: MediaType;
|
mediaType: MediaType;
|
||||||
status?: MediaStatus;
|
status?: MediaStatus;
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
@@ -73,7 +73,9 @@ const TitleCard = ({
|
|||||||
const showRequestButton = hasPermission(
|
const showRequestButton = hasPermission(
|
||||||
[
|
[
|
||||||
Permission.REQUEST,
|
Permission.REQUEST,
|
||||||
mediaType === 'movie' ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV,
|
mediaType === 'movie' || mediaType === 'collection'
|
||||||
|
? Permission.REQUEST_MOVIE
|
||||||
|
: Permission.REQUEST_TV,
|
||||||
],
|
],
|
||||||
{ type: 'or' }
|
{ type: 'or' }
|
||||||
);
|
);
|
||||||
@@ -86,7 +88,13 @@ const TitleCard = ({
|
|||||||
<RequestModal
|
<RequestModal
|
||||||
tmdbId={id}
|
tmdbId={id}
|
||||||
show={showRequestModal}
|
show={showRequestModal}
|
||||||
type={mediaType === 'movie' ? 'movie' : 'tv'}
|
type={
|
||||||
|
mediaType === 'movie'
|
||||||
|
? 'movie'
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? 'collection'
|
||||||
|
: 'tv'
|
||||||
|
}
|
||||||
onComplete={requestComplete}
|
onComplete={requestComplete}
|
||||||
onUpdating={requestUpdating}
|
onUpdating={requestUpdating}
|
||||||
onCancel={closeModal}
|
onCancel={closeModal}
|
||||||
@@ -130,7 +138,7 @@ const TitleCard = ({
|
|||||||
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
|
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
|
||||||
mediaType === 'movie'
|
mediaType === 'movie' || mediaType === 'collection'
|
||||||
? 'border-blue-500 bg-blue-600'
|
? 'border-blue-500 bg-blue-600'
|
||||||
: 'border-purple-600 bg-purple-600'
|
: 'border-purple-600 bg-purple-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -138,6 +146,8 @@ const TitleCard = ({
|
|||||||
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
||||||
{mediaType === 'movie'
|
{mediaType === 'movie'
|
||||||
? intl.formatMessage(globalMessages.movie)
|
? intl.formatMessage(globalMessages.movie)
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? intl.formatMessage(globalMessages.collection)
|
||||||
: intl.formatMessage(globalMessages.tvshow)}
|
: intl.formatMessage(globalMessages.tvshow)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +187,15 @@ const TitleCard = ({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden rounded-xl">
|
<div className="absolute inset-0 overflow-hidden rounded-xl">
|
||||||
<Link href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}>
|
<Link
|
||||||
|
href={
|
||||||
|
mediaType === 'movie'
|
||||||
|
? `/movie/${id}`
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? `/collection/${id}`
|
||||||
|
: `/tv/${id}`
|
||||||
|
}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left"
|
className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left"
|
||||||
style={{
|
style={{
|
||||||
|
@@ -16,6 +16,7 @@ const globalMessages = defineMessages({
|
|||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
movie: 'Movie',
|
movie: 'Movie',
|
||||||
movies: 'Movies',
|
movies: 'Movies',
|
||||||
|
collection: 'Collection',
|
||||||
tvshow: 'Series',
|
tvshow: 'Series',
|
||||||
tvshows: 'Series',
|
tvshows: 'Series',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
Reference in New Issue
Block a user