From cfd1bc253557d6e19725743b8aa9a2fa33bbe760 Mon Sep 17 00:00:00 2001 From: Joaquin Olivero <66050823+JoaquinOlivero@users.noreply.github.com> Date: Mon, 19 Aug 2024 15:37:04 -0300 Subject: [PATCH] feat: adds status filter for tv shows (#796) re #605 Co-authored-by: JoaquinOlivero --- overseerr-api.yml | 5 ++ server/api/themoviedb/index.ts | 3 + server/routes/discover.ts | 2 + .../Discover/FilterSlideover/index.tsx | 19 +++++ src/components/Discover/constants.ts | 5 ++ src/components/Selector/index.tsx | 76 +++++++++++++++++++ src/i18n/locale/en.json | 8 ++ 7 files changed, 118 insertions(+) diff --git a/overseerr-api.yml b/overseerr-api.yml index 3cb42284c..8f916708c 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -4862,6 +4862,11 @@ paths: schema: type: string example: 8|9 + - in: query + name: status + schema: + type: string + example: 3|4 responses: '200': description: Results diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 922ff90f4..6f13ec08a 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -95,6 +95,7 @@ interface DiscoverTvOptions { sortBy?: SortOptions; watchRegion?: string; watchProviders?: string; + withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5 } class TheMovieDb extends ExternalAPI { @@ -523,6 +524,7 @@ class TheMovieDb extends ExternalAPI { voteCountLte, watchProviders, watchRegion, + withStatus, }: DiscoverTvOptions = {}): Promise => { try { const defaultFutureDate = new Date( @@ -570,6 +572,7 @@ class TheMovieDb extends ExternalAPI { 'vote_count.lte': voteCountLte || '', with_watch_providers: watchProviders || '', watch_region: watchRegion || '', + with_status: withStatus || '', }); return data; diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 9590d32b3..55a844ad2 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -71,6 +71,7 @@ const QueryFilterOptions = z.object({ network: z.coerce.string().optional(), watchProviders: z.coerce.string().optional(), watchRegion: z.coerce.string().optional(), + status: z.coerce.string().optional(), }); export type FilterOptions = z.infer; @@ -385,6 +386,7 @@ discoverRoutes.get('/tv', async (req, res, next) => { voteCountLte: query.voteCountLte, watchProviders: query.watchProviders, watchRegion: query.watchRegion, + withStatus: query.status, }); const media = await Media.getRelatedMedia( diff --git a/src/components/Discover/FilterSlideover/index.tsx b/src/components/Discover/FilterSlideover/index.tsx index d7029fb21..7df6d55ab 100644 --- a/src/components/Discover/FilterSlideover/index.tsx +++ b/src/components/Discover/FilterSlideover/index.tsx @@ -8,6 +8,7 @@ import { CompanySelector, GenreSelector, KeywordSelector, + StatusSelector, WatchProviderSelector, } from '@app/components/Selector'; import useSettings from '@app/hooks/useSettings'; @@ -40,6 +41,7 @@ const messages = defineMessages('components.Discover.FilterSlideover', { runtime: 'Runtime', streamingservices: 'Streaming Services', voteCount: 'Number of votes between {minValue} and {maxValue}', + status: 'Status', }); type FilterSlideoverProps = { @@ -150,6 +152,23 @@ const FilterSlideover = ({ updateQueryParams('genre', value?.map((v) => v.value).join(',')); }} /> + {type === 'tv' && ( + <> + + {intl.formatMessage(messages.status)} + + { + updateQueryParams( + 'status', + value?.map((v) => v.value).join('|') + ); + }} + /> + + )} {intl.formatMessage(messages.keywords)} diff --git a/src/components/Discover/constants.ts b/src/components/Discover/constants.ts index 42d0dab05..c123c9272 100644 --- a/src/components/Discover/constants.ts +++ b/src/components/Discover/constants.ts @@ -108,6 +108,7 @@ export const QueryFilterOptions = z.object({ voteCountGte: z.string().optional(), watchRegion: z.string().optional(), watchProviders: z.string().optional(), + status: z.string().optional(), }); export type FilterOptions = z.infer; @@ -147,6 +148,10 @@ export const prepareFilterValues = ( filterValues.genre = values.genre; } + if (values.status) { + filterValues.status = values.status; + } + if (values.keywords) { filterValues.keywords = values.keywords; } diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index ba40c991d..a371b7f98 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -33,6 +33,13 @@ const messages = defineMessages('components.Selector', { nooptions: 'No results.', showmore: 'Show More', showless: 'Show Less', + searchStatus: 'Select status...', + returningSeries: 'Returning Series', + planned: 'Planned', + inProduction: 'In Production', + ended: 'Ended', + canceled: 'Canceled', + pilot: 'Pilot', }); type SingleVal = { @@ -204,6 +211,75 @@ export const GenreSelector = ({ ); }; +export const StatusSelector = ({ + isMulti, + defaultValue, + onChange, +}: BaseSelectorMultiProps | BaseSelectorSingleProps) => { + const intl = useIntl(); + const [defaultDataValue, setDefaultDataValue] = useState< + { label: string; value: number }[] | null + >(null); + + const options = useMemo( + () => [ + { name: intl.formatMessage(messages.returningSeries), id: 0 }, + { name: intl.formatMessage(messages.planned), id: 1 }, + { name: intl.formatMessage(messages.inProduction), id: 2 }, + { name: intl.formatMessage(messages.ended), id: 3 }, + { name: intl.formatMessage(messages.canceled), id: 4 }, + { name: intl.formatMessage(messages.pilot), id: 5 }, + ], + [intl] + ); + + useEffect(() => { + const loadDefaultStatus = async (): Promise => { + if (!defaultValue) { + return; + } + const statuses = defaultValue.split('|'); + + const statusData = options + .filter((opt) => statuses.find((s) => Number(s) === opt.id)) + .map((o) => ({ + label: o.name, + value: o.id, + })); + + setDefaultDataValue(statusData); + }; + + loadDefaultStatus(); + }, [defaultValue, options]); + + const loadStatusOptions = async () => { + return options + .map((result) => ({ + label: result.name, + value: result.id, + })) + .filter(({ label }) => label.toLowerCase()); + }; + + return ( + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onChange(value as any); + }} + /> + ); +}; + export const KeywordSelector = ({ isMulti, defaultValue, diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 5c9aa6fe2..0c07003f0 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -73,6 +73,7 @@ "components.Discover.FilterSlideover.releaseDate": "Release Date", "components.Discover.FilterSlideover.runtime": "Runtime", "components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime", + "components.Discover.FilterSlideover.status": "Status", "components.Discover.FilterSlideover.streamingservices": "Streaming Services", "components.Discover.FilterSlideover.studio": "Studio", "components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score", @@ -555,9 +556,16 @@ "components.ResetPassword.validationpasswordrequired": "You must provide a password", "components.Search.search": "Search", "components.Search.searchresults": "Search Results", + "components.Selector.canceled": "Canceled", + "components.Selector.ended": "Ended", + "components.Selector.inProduction": "In Production", "components.Selector.nooptions": "No results.", + "components.Selector.pilot": "Pilot", + "components.Selector.planned": "Planned", + "components.Selector.returningSeries": "Returning Series", "components.Selector.searchGenres": "Select genres…", "components.Selector.searchKeywords": "Search keywords…", + "components.Selector.searchStatus": "Select status...", "components.Selector.searchStudios": "Search studios…", "components.Selector.showless": "Show Less", "components.Selector.showmore": "Show More",