Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4756,6 +4756,71 @@ paths:
schema:
type: string
example: en
- in: query
name: genre
schema:
type: string
example: 18
- in: query
name: keywords
schema:
type: string
example: 1,2
- in: query
name: sortBy
schema:
type: string
example: popularity.desc
- in: query
name: firstAirDateGte
schema:
type: string
example: 2022-01-01
- in: query
name: firstAirDateLte
schema:
type: string
example: 2023-01-01
- in: query
name: withRuntimeGte
schema:
type: number
example: 60
- in: query
name: withRuntimeLte
schema:
type: number
example: 120
- in: query
name: voteAverageGte
schema:
type: number
example: 7
- in: query
name: voteAverageLte
schema:
type: number
example: 10
- in: query
name: voteCountGte
schema:
type: number
example: 7
- in: query
name: voteCountLte
schema:
type: number
example: 10
- in: query
name: watchRegion
schema:
type: string
example: US
- in: query
name: watchProviders
schema:
type: string
example: 8|9
responses:
'200':
description: Results
Expand All @@ -4775,6 +4840,10 @@ paths:
example: 200
network:
$ref: '#/components/schemas/Network'
keywords:
type: array
items:
$ref: '#/components/schemas/Keyword'
results:
type: array
items:
Expand Down
22 changes: 19 additions & 3 deletions server/api/themoviedb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export type SortOptions =
| 'vote_count.asc'
| 'vote_count.desc'
| 'first_air_date.asc'
| 'first_air_date.desc';
| 'first_air_date.desc'
| 'name.asc'
| 'name.desc';

interface DiscoverMovieOptions {
page?: number;
Expand Down Expand Up @@ -512,7 +514,14 @@ class TheMovieDb extends ExternalAPI {
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
'vote_average.lte': voteAverageLte,
'vote_count.gte': voteCountGte,
// Require minimum votes when filtering or sorting by rating to exclude unreliable ratings
'vote_count.gte':
voteCountGte ??
(sortBy?.startsWith('vote_average')
? '50'
: voteAverageGte || voteAverageLte
? '1'
: undefined),
'vote_count.lte': voteCountLte,
watch_region: watchRegion,
with_watch_providers: watchProviders,
Expand Down Expand Up @@ -586,7 +595,14 @@ class TheMovieDb extends ExternalAPI {
'with_runtime.lte': withRuntimeLte,
'vote_average.gte': voteAverageGte,
'vote_average.lte': voteAverageLte,
'vote_count.gte': voteCountGte,
// Require minimum votes when filtering or sorting by rating to exclude unreliable ratings
'vote_count.gte':
voteCountGte ??
(sortBy?.startsWith('vote_average')
? '50'
: voteAverageGte || voteAverageLte
? '1'
: undefined),
'vote_count.lte': voteCountLte,
with_watch_providers: watchProviders,
watch_region: watchRegion,
Expand Down
40 changes: 37 additions & 3 deletions server/routes/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,26 +537,60 @@ discoverRoutes.get<{ genreId: string }>(
discoverRoutes.get<{ networkId: string }>(
'/tv/network/:networkId',
async (req, res, next) => {
const tmdb = new TheMovieDb();
const tmdb = createTmdbWithRegionLanguage(req.user);

try {
const query = QueryFilterOptions.parse(req.query);
const keywords = query.keywords;
const network = await tmdb.getNetwork(Number(req.params.networkId));

const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: (req.query.language as string) ?? req.locale,
page: Number(query.page) || 1,
sortBy: query.sortBy as SortOptions,
language: req.locale ?? query.language,
genre: query.genre,
network: Number(req.params.networkId),
firstAirDateLte: query.firstAirDateLte
? new Date(query.firstAirDateLte).toISOString().split('T')[0]
: undefined,
firstAirDateGte: query.firstAirDateGte
? new Date(query.firstAirDateGte).toISOString().split('T')[0]
: undefined,
originalLanguage: query.language,
keywords,
withRuntimeGte: query.withRuntimeGte,
withRuntimeLte: query.withRuntimeLte,
voteAverageGte: query.voteAverageGte,
voteAverageLte: query.voteAverageLte,
voteCountGte: query.voteCountGte,
voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
watchRegion: query.watchRegion,
});

const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);

let keywordData: TmdbKeyword[] = [];
if (keywords) {
const splitKeywords = keywords.split(',');

keywordData = await Promise.all(
splitKeywords.map(async (keywordId) => {
return await tmdb.getKeywordDetails({
keywordId: Number(keywordId),
});
})
);
}

return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
network: mapNetwork(network),
keywords: keywordData,
results: data.results.map((result) =>
mapTvResult(
result,
Expand Down
109 changes: 104 additions & 5 deletions src/components/Discover/DiscoverNetwork/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
import Button from '@app/components/Common/Button';
import Header from '@app/components/Common/Header';
import ListView from '@app/components/Common/ListView';
import PageTitle from '@app/components/Common/PageTitle';
import type { FilterOptions } from '@app/components/Discover/constants';
import {
countActiveFilters,
prepareFilterValues,
} from '@app/components/Discover/constants';
import FilterSlideover from '@app/components/Discover/FilterSlideover';
import useDiscover from '@app/hooks/useDiscover';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
import type { TvNetwork } from '@server/models/common';
import type { TvResult } from '@server/models/Search';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';

const messages = defineMessages({
networkSeries: '{network} Series',
activefilters:
'{count, plural, one {# Active Filter} other {# Active Filters}}',
sortPopularityAsc: 'Popularity Ascending',
sortPopularityDesc: 'Popularity Descending',
sortFirstAirDateAsc: 'First Air Date Ascending',
sortFirstAirDateDesc: 'First Air Date Descending',
sortTmdbRatingAsc: 'TMDB Rating Ascending',
sortTmdbRatingDesc: 'TMDB Rating Descending',
sortTitleAsc: 'Title (A-Z) Ascending',
sortTitleDesc: 'Title (Z-A) Descending',
});

const SortOptions: Record<string, TMDBSortOptions> = {
PopularityAsc: 'popularity.asc',
PopularityDesc: 'popularity.desc',
FirstAirDateAsc: 'first_air_date.asc',
FirstAirDateDesc: 'first_air_date.desc',
TmdbRatingAsc: 'vote_average.asc',
TmdbRatingDesc: 'vote_average.desc',
TitleAsc: 'name.asc',
TitleDesc: 'name.desc',
} as const;

const DiscoverTvNetwork = () => {
const router = useRouter();
const intl = useIntl();
const [showFilters, setShowFilters] = useState(false);
const preparedFilters = prepareFilterValues(router.query);
const updateQueryParams = useUpdateQueryParams({});

const { data: network } = useSWR<TvNetwork>(
router.query.networkId ? `/api/v1/network/${router.query.networkId}` : null
);

const {
isLoadingInitialData,
Expand All @@ -26,8 +66,11 @@ const DiscoverTvNetwork = () => {
fetchMore,
error,
firstResultData,
} = useDiscover<TvResult, { network: TvNetwork }>(
`/api/v1/discover/tv/network/${router.query.networkId}`
} = useDiscover<TvResult, { network: TvNetwork }, FilterOptions>(
`/api/v1/discover/tv/network/${router.query.networkId}`,
{
...preparedFilters,
}
);

if (error) {
Expand All @@ -43,10 +86,10 @@ const DiscoverTvNetwork = () => {
return (
<>
<PageTitle title={title} />
<div className="mt-1 mb-5">
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
<Header>
{firstResultData?.network.logoPath ? (
<div className="mb-6 flex justify-center">
<div className="flex justify-center">
<img
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
alt={firstResultData.network.name}
Expand All @@ -57,14 +100,70 @@ const DiscoverTvNetwork = () => {
title
)}
</Header>
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
<BarsArrowDownIcon className="h-6 w-6" />
</span>
<select
id="sortBy"
name="sortBy"
className="rounded-r-only"
value={preparedFilters.sortBy}
onChange={(e) => updateQueryParams('sortBy', e.target.value)}
>
<option value={SortOptions.PopularityDesc}>
{intl.formatMessage(messages.sortPopularityDesc)}
</option>
<option value={SortOptions.PopularityAsc}>
{intl.formatMessage(messages.sortPopularityAsc)}
</option>
<option value={SortOptions.FirstAirDateDesc}>
{intl.formatMessage(messages.sortFirstAirDateDesc)}
</option>
<option value={SortOptions.FirstAirDateAsc}>
{intl.formatMessage(messages.sortFirstAirDateAsc)}
</option>
<option value={SortOptions.TmdbRatingDesc}>
{intl.formatMessage(messages.sortTmdbRatingDesc)}
</option>
<option value={SortOptions.TmdbRatingAsc}>
{intl.formatMessage(messages.sortTmdbRatingAsc)}
</option>
<option value={SortOptions.TitleAsc}>
{intl.formatMessage(messages.sortTitleAsc)}
</option>
<option value={SortOptions.TitleDesc}>
{intl.formatMessage(messages.sortTitleDesc)}
</option>
</select>
</div>
<FilterSlideover
type="tv"
currentFilters={preparedFilters}
onClose={() => setShowFilters(false)}
show={showFilters}
lockedNetwork={network}
/>
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
<Button onClick={() => setShowFilters(true)} className="w-full">
<FunnelIcon />
<span>
{intl.formatMessage(messages.activefilters, {
count: countActiveFilters(preparedFilters) + 1,
})}
</span>
</Button>
</div>
</div>
</div>
<ListView
items={titles}
isEmpty={isEmpty}
isReachingEnd={isReachingEnd}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Discover/DiscoverTv/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const SortOptions: Record<string, TMDBSortOptions> = {
FirstAirDateDesc: 'first_air_date.desc',
TmdbRatingAsc: 'vote_average.asc',
TmdbRatingDesc: 'vote_average.desc',
TitleAsc: 'original_title.asc',
TitleDesc: 'original_title.desc',
TitleAsc: 'name.asc',
TitleDesc: 'name.desc',
} as const;

const DiscoverTv = () => {
Expand Down
Loading