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
10 changes: 10 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5714,6 +5714,11 @@ paths:
schema:
type: string
example: 8|9
- in: query
name: excludeWatchProviders
schema:
type: string
example: 8|9
- in: query
name: certification
schema:
Expand Down Expand Up @@ -6040,6 +6045,11 @@ paths:
schema:
type: string
example: 8|9
- in: query
name: excludeWatchProviders
schema:
type: string
example: 8|9
- in: query
name: status
schema:
Expand Down
6 changes: 6 additions & 0 deletions server/api/themoviedb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ interface DiscoverMovieOptions {
sortBy?: MovieSortOptions;
watchRegion?: string;
watchProviders?: string;
excludeWatchProviders?: string;
certification?: string;
certificationGte?: string;
certificationLte?: string;
Expand All @@ -131,6 +132,7 @@ interface DiscoverTvOptions {
sortBy?: TvSortOptions;
watchRegion?: string;
watchProviders?: string;
excludeWatchProviders?: string;
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
certification?: string;
certificationGte?: string;
Expand Down Expand Up @@ -697,6 +699,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
voteCountGte,
voteCountLte,
watchProviders,
excludeWatchProviders,
watchRegion,
certification,
certificationGte,
Expand Down Expand Up @@ -750,6 +753,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
'vote_count.lte': voteCountLte,
watch_region: watchRegion,
with_watch_providers: watchProviders,
without_watch_providers: excludeWatchProviders,
certification: certification,
'certification.gte': certificationGte,
'certification.lte': certificationLte,
Expand Down Expand Up @@ -784,6 +788,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
voteCountGte,
voteCountLte,
watchProviders,
excludeWatchProviders,
watchRegion,
withStatus,
certification,
Expand Down Expand Up @@ -836,6 +841,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
'vote_count.gte': voteCountGte,
'vote_count.lte': voteCountLte,
with_watch_providers: watchProviders,
without_watch_providers: excludeWatchProviders,
watch_region: watchRegion,
with_status: withStatus,
certification: certification,
Expand Down
3 changes: 3 additions & 0 deletions server/routes/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const QueryFilterOptions = z.object({
voteCountLte: z.coerce.string().optional(),
network: z.coerce.string().optional(),
watchProviders: z.coerce.string().optional(),
excludeWatchProviders: z.coerce.string().optional(),
watchRegion: z.coerce.string().optional(),
status: z.coerce.string().optional(),
certification: z.coerce.string().optional(),
Expand Down Expand Up @@ -132,6 +133,7 @@ discoverRoutes.get('/movies', async (req, res, next) => {
voteCountGte: query.voteCountGte,
voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
excludeWatchProviders: query.excludeWatchProviders,
watchRegion: query.watchRegion,
certification: query.certification,
certificationGte: query.certificationGte,
Expand Down Expand Up @@ -444,6 +446,7 @@ discoverRoutes.get('/tv', async (req, res, next) => {
voteCountGte: query.voteCountGte,
voteCountLte: query.voteCountLte,
watchProviders: query.watchProviders,
excludeWatchProviders: query.excludeWatchProviders,
watchRegion: query.watchRegion,
withStatus: query.status,
certification: query.certification,
Expand Down
40 changes: 32 additions & 8 deletions src/components/Discover/CreateSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,16 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
type: DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES,
title: intl.formatMessage(sliderTitles.tmdbmoviestreamingservices),
dataUrl: '/api/v1/discover/movies',
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
params:
'watchRegion=$regionValue&watchProviders=$providersValue&excludeWatchProviders=$excludeProvidersValue',
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
},
{
type: DiscoverSliderType.TMDB_TV_STREAMING_SERVICES,
title: intl.formatMessage(sliderTitles.tmdbtvstreamingservices),
dataUrl: '/api/v1/discover/tv',
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
params:
'watchRegion=$regionValue&watchProviders=$providersValue&excludeWatchProviders=$excludeProvidersValue',
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
},
];
Expand Down Expand Up @@ -444,11 +446,20 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
activeProviders={
slider?.data
?.split(',')[1]
.split('|')
?.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers) => {
setFieldValue('data', `${region},${providers.join('|')}`);
excludeProviders={
slider?.data
?.split(',')[2]
?.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers, excludes) => {
setFieldValue(
'data',
`${region},${providers.join('|')},${excludes.join('|')}`
);
}}
/>
);
Expand All @@ -461,11 +472,20 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
activeProviders={
slider?.data
?.split(',')[1]
.split('|')
?.split('|')
.map((v) => Number(v)) ?? []
}
excludeProviders={
slider?.data
?.split(',')[2]
?.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers) => {
setFieldValue('data', `${region},${providers.join('|')}`);
onChange={(region, providers, excludes) => {
setFieldValue(
'data',
`${region},${providers.join('|')},${excludes.join('|')}`
);
}}
/>
);
Expand Down Expand Up @@ -555,6 +575,10 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
'$providersValue',
encodeURIExtraParams(values?.data.split(',')[1])
)
.replace(
'$excludeProvidersValue',
encodeURIExtraParams(values?.data.split(',')[2])
)
: activeOption.params?.replace(
'$value',
encodeURIExtraParams(values.data)
Expand Down
11 changes: 9 additions & 2 deletions src/components/Discover/FilterSlideover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,23 @@ const FilterSlideover = ({
currentFilters.watchProviders?.split('|').map((v) => Number(v)) ??
[]
}
onChange={(region, providers) => {
if (providers.length) {
excludeProviders={
currentFilters.excludeWatchProviders
?.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers, excludes) => {
if (providers.length || excludes.length) {
batchUpdateQueryParams({
watchRegion: region,
watchProviders: providers.join('|'),
excludeWatchProviders: excludes.join('|'),
});
} else {
batchUpdateQueryParams({
watchRegion: undefined,
watchProviders: undefined,
excludeWatchProviders: undefined,
});
}
}}
Expand Down
8 changes: 7 additions & 1 deletion src/components/Discover/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const QueryFilterOptions = z.object({
voteCountGte: z.string().optional(),
watchRegion: z.string().optional(),
watchProviders: z.string().optional(),
excludeWatchProviders: z.string().optional(),
status: z.string().optional(),
certification: z.string().optional(),
certificationGte: z.string().optional(),
Expand Down Expand Up @@ -198,6 +199,10 @@ export const prepareFilterValues = (
filterValues.watchProviders = values.watchProviders;
}

if (values.excludeWatchProviders) {
filterValues.excludeWatchProviders = values.excludeWatchProviders;
}

if (values.watchRegion) {
filterValues.watchRegion = values.watchRegion;
}
Expand Down Expand Up @@ -251,9 +256,10 @@ export const countActiveFilters = (filterValues: FilterOptions): number => {
delete clonedFilters.withRuntimeLte;
}

if (clonedFilters.watchProviders) {
if (clonedFilters.watchProviders || clonedFilters.excludeWatchProviders) {
totalCount += 1;
delete clonedFilters.watchProviders;
delete clonedFilters.excludeWatchProviders;
delete clonedFilters.watchRegion;
}

Expand Down
16 changes: 12 additions & 4 deletions src/components/Discover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,14 @@ const Discover = () => {
url="/api/v1/discover/movies"
extraParams={`watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
}&watchProviders=${
slider.data?.split(',')[1]
}&excludeWatchProviders=${slider.data?.split(',')[2] ?? ''}`}
linkUrl={`/discover/movies?watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
}&watchProviders=${
slider.data?.split(',')[1]
}&excludeWatchProviders=${slider.data?.split(',')[2] ?? ''}`}
/>
);
break;
Expand All @@ -391,10 +395,14 @@ const Discover = () => {
url="/api/v1/discover/tv"
extraParams={`watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
}&watchProviders=${
slider.data?.split(',')[1]
}&excludeWatchProviders=${slider.data?.split(',')[2] ?? ''}`}
linkUrl={`/discover/tv?watchRegion=${
slider.data?.split(',')[0]
}&watchProviders=${slider.data?.split(',')[1]}`}
}&watchProviders=${
slider.data?.split(',')[1]
}&excludeWatchProviders=${slider.data?.split(',')[2] ?? ''}`}
/>
);
break;
Expand Down
42 changes: 35 additions & 7 deletions src/components/Selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import useSettings from '@app/hooks/useSettings';
import defineMessages from '@app/utils/defineMessages';
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/20/solid';
import { CheckCircleIcon } from '@heroicons/react/24/solid';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
import type {
TmdbCompanySearchResponse,
TmdbGenre,
Expand Down Expand Up @@ -373,14 +373,20 @@ type WatchProviderSelectorProps = {
type: 'movie' | 'tv';
region?: string;
activeProviders?: number[];
onChange: (region: string, value: number[]) => void;
excludeProviders?: number[];
onChange: (
region: string,
activeValue: number[],
excludeValue: number[]
) => void;
};

export const WatchProviderSelector = ({
type,
onChange,
region,
activeProviders,
excludeProviders,
}: WatchProviderSelectorProps) => {
const intl = useIntl();
const { currentSettings } = useSettings();
Expand All @@ -395,17 +401,20 @@ export const WatchProviderSelector = ({
const [activeProvider, setActiveProvider] = useState<number[]>(
activeProviders ?? []
);
const [excludeProvider, setExcludeProvider] = useState<number[]>(
excludeProviders ?? []
);
const { data, isLoading } = useSWR<WatchProviderDetails[]>(
`/api/v1/watchproviders/${
type === 'movie' ? 'movies' : 'tv'
}?watchRegion=${watchRegion}`
);

useEffect(() => {
onChange(watchRegion, activeProvider);
onChange(watchRegion, activeProvider, excludeProvider);
// removed onChange as a dependency as we only need to call it when the value(s) change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeProvider, watchRegion]);
}, [activeProvider, excludeProvider, watchRegion]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const orderedData = useMemo(() => {
if (!data) {
Expand All @@ -416,8 +425,11 @@ export const WatchProviderSelector = ({
}, [data]);

const toggleProvider = (id: number) => {
if (activeProvider.includes(id)) {
if (excludeProvider.includes(id)) {
setExcludeProvider(excludeProvider.filter((p) => p !== id));
} else if (activeProvider.includes(id)) {
setActiveProvider(activeProvider.filter((p) => p !== id));
setExcludeProvider([...excludeProvider, id]);
} else {
setActiveProvider([...activeProvider, id]);
}
Expand Down Expand Up @@ -447,6 +459,7 @@ export const WatchProviderSelector = ({
<div className="provider-icons grid gap-2">
{initialProviders.map((provider) => {
const isActive = activeProvider.includes(provider.id);
const isExclude = excludeProvider.includes(provider.id);
return (
<Tooltip
content={provider.name}
Expand All @@ -456,7 +469,9 @@ export const WatchProviderSelector = ({
className={`provider-container relative w-full cursor-pointer rounded-lg ring-1 ${
isActive
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
: isExclude
? 'bg-gray-600 ring-red-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
}`}
onClick={() => toggleProvider(provider.id)}
onKeyDown={(e) => {
Expand All @@ -481,6 +496,11 @@ export const WatchProviderSelector = ({
<CheckCircleIcon className="h-6 w-6" />
</div>
)}
{isExclude && (
<div className="pointer-events-none absolute -left-1 -top-1 flex items-center justify-center text-indigo-100 opacity-90">
<XCircleIcon className="h-6 w-6" />
</div>
)}
</div>
</Tooltip>
);
Expand All @@ -490,6 +510,7 @@ export const WatchProviderSelector = ({
<div className="provider-icons relative top-2 grid gap-2">
{otherProviders.map((provider) => {
const isActive = activeProvider.includes(provider.id);
const isExclude = excludeProvider.includes(provider.id);
return (
<Tooltip
content={provider.name}
Expand All @@ -499,7 +520,9 @@ export const WatchProviderSelector = ({
className={`provider-container relative w-full cursor-pointer rounded-lg ring-1 transition ${
isActive
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
: isExclude
? 'bg-gray-600 ring-red-500 hover:bg-gray-500'
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
}`}
onClick={() => toggleProvider(provider.id)}
onKeyDown={(e) => {
Expand All @@ -524,6 +547,11 @@ export const WatchProviderSelector = ({
<CheckCircleIcon className="h-6 w-6" />
</div>
)}
{isExclude && (
<div className="pointer-events-none absolute -left-1 -top-1 flex items-center justify-center text-indigo-100 opacity-90">
<XCircleIcon className="h-6 w-6" />
</div>
)}
</div>
</Tooltip>
);
Expand Down