feat(trending): add filter options (#2137)
Signed-off-by: Florian Hoech <code@florians-web.de>
This commit is contained in:
@@ -5964,6 +5964,23 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: en
|
example: en
|
||||||
|
- in: query
|
||||||
|
name: mediaType
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- all
|
||||||
|
- movie
|
||||||
|
- tv
|
||||||
|
default: all
|
||||||
|
- in: query
|
||||||
|
name: timeWindow
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- day
|
||||||
|
- week
|
||||||
|
default: day
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Results
|
description: Results
|
||||||
|
|||||||
@@ -715,9 +715,11 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
public getMovieTrending = async ({
|
public getMovieTrending = async ({
|
||||||
page = 1,
|
page = 1,
|
||||||
timeWindow = 'day',
|
timeWindow = 'day',
|
||||||
|
language = this.locale,
|
||||||
}: {
|
}: {
|
||||||
page?: number;
|
page?: number;
|
||||||
timeWindow?: 'day' | 'week';
|
timeWindow?: 'day' | 'week';
|
||||||
|
language?: string;
|
||||||
} = {}): Promise<TmdbSearchMovieResponse> => {
|
} = {}): Promise<TmdbSearchMovieResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
@@ -725,6 +727,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
|
language,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -738,9 +741,11 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
public getTvTrending = async ({
|
public getTvTrending = async ({
|
||||||
page = 1,
|
page = 1,
|
||||||
timeWindow = 'day',
|
timeWindow = 'day',
|
||||||
|
language = this.locale,
|
||||||
}: {
|
}: {
|
||||||
page?: number;
|
page?: number;
|
||||||
timeWindow?: 'day' | 'week';
|
timeWindow?: 'day' | 'week';
|
||||||
|
language?: string;
|
||||||
} = {}): Promise<TmdbSearchTvResponse> => {
|
} = {}): Promise<TmdbSearchTvResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchTvResponse>(
|
const data = await this.get<TmdbSearchTvResponse>(
|
||||||
@@ -748,6 +753,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
|
language,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -673,10 +673,41 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
const tmdb = createTmdbWithRegionLanguage(req.user);
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await tmdb.getAllTrending({
|
const mediaType = (req.query.mediaType as 'all' | 'movie' | 'tv') ?? 'all';
|
||||||
page: Number(req.query.page),
|
const timeWindow =
|
||||||
language: (req.query.language as string) ?? req.locale,
|
(req.query.timeWindow as 'day' | 'week') === 'week' ? 'week' : 'day';
|
||||||
});
|
const language = (req.query.language as string) ?? req.locale;
|
||||||
|
const page = Number(req.query.page);
|
||||||
|
|
||||||
|
const trendingFetchers = {
|
||||||
|
movie: async () => ({
|
||||||
|
data: await tmdb.getMovieTrending({ page, language, timeWindow }),
|
||||||
|
mapper: mapMovieResult,
|
||||||
|
type: MediaType.MOVIE,
|
||||||
|
}),
|
||||||
|
tv: async () => ({
|
||||||
|
data: await tmdb.getTvTrending({ page, language, timeWindow }),
|
||||||
|
mapper: mapTvResult,
|
||||||
|
type: MediaType.TV,
|
||||||
|
}),
|
||||||
|
all: async () => ({
|
||||||
|
data: await tmdb.getAllTrending({ page, language, timeWindow }),
|
||||||
|
mapper: (result: any, media?: Media) => {
|
||||||
|
if (isMovie(result)) {
|
||||||
|
return mapMovieResult(result, media);
|
||||||
|
} else if (isPerson(result)) {
|
||||||
|
return mapPersonResult(result);
|
||||||
|
} else if (isCollection(result)) {
|
||||||
|
return mapCollectionResult(result);
|
||||||
|
} else {
|
||||||
|
return mapTvResult(result, media);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: null,
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const { data, mapper, type } = await trendingFetchers[mediaType]();
|
||||||
|
|
||||||
const media = await Media.getRelatedMedia(
|
const media = await Media.getRelatedMedia(
|
||||||
req.user,
|
req.user,
|
||||||
@@ -687,27 +718,16 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
page: data.page,
|
page: data.page,
|
||||||
totalPages: data.total_pages,
|
totalPages: data.total_pages,
|
||||||
totalResults: data.total_results,
|
totalResults: data.total_results,
|
||||||
results: data.results.map((result) =>
|
results: data.results.map((result) => {
|
||||||
isMovie(result)
|
// - If "type" is set (case: "movie" or "tv"), the mediaType must also match.
|
||||||
? mapMovieResult(
|
// - If "type" is not set (case: "all"), only filter by tmdbId.
|
||||||
result,
|
const selectedMedia = media.find(
|
||||||
media.find(
|
(med) =>
|
||||||
(med) =>
|
med.tmdbId === result.id && (type ? med.mediaType === type : true)
|
||||||
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
);
|
||||||
)
|
|
||||||
)
|
return mapper(result, selectedMedia);
|
||||||
: isPerson(result)
|
}),
|
||||||
? mapPersonResult(result)
|
|
||||||
: isCollection(result)
|
|
||||||
? mapCollectionResult(result)
|
|
||||||
: mapTvResult(
|
|
||||||
result,
|
|
||||||
media.find(
|
|
||||||
(med) =>
|
|
||||||
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Something went wrong retrieving trending items', {
|
logger.debug('Something went wrong retrieving trending items', {
|
||||||
|
|||||||
@@ -2,21 +2,32 @@ import Header from '@app/components/Common/Header';
|
|||||||
import ListView from '@app/components/Common/ListView';
|
import ListView from '@app/components/Common/ListView';
|
||||||
import PageTitle from '@app/components/Common/PageTitle';
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
import useDiscover from '@app/hooks/useDiscover';
|
import useDiscover from '@app/hooks/useDiscover';
|
||||||
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import Error from '@app/pages/_error';
|
import Error from '@app/pages/_error';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
|
import { CircleStackIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||||
import type {
|
import type {
|
||||||
MovieResult,
|
MovieResult,
|
||||||
PersonResult,
|
PersonResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
} from '@server/models/Search';
|
} from '@server/models/Search';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages('components.Discover', {
|
const messages = defineMessages('components.Discover', {
|
||||||
trending: 'Trending',
|
trending: 'Trending',
|
||||||
|
timeWindowDay: 'Daily',
|
||||||
|
timeWindowWeek: 'Weekly',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type MediaType = 'all' | 'movie' | 'tv';
|
||||||
|
|
||||||
|
type TimeWindow = 'day' | 'week';
|
||||||
|
|
||||||
const Trending = () => {
|
const Trending = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [currentMediaType, setCurrentMediaType] = useState<MediaType>('all');
|
||||||
|
const [currentTimeWindow, setCurrentTimeWindow] = useState<TimeWindow>('day');
|
||||||
const {
|
const {
|
||||||
isLoadingInitialData,
|
isLoadingInitialData,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
@@ -26,7 +37,8 @@ const Trending = () => {
|
|||||||
fetchMore,
|
fetchMore,
|
||||||
error,
|
error,
|
||||||
} = useDiscover<MovieResult | TvResult | PersonResult>(
|
} = useDiscover<MovieResult | TvResult | PersonResult>(
|
||||||
'/api/v1/discover/trending'
|
'/api/v1/discover/trending',
|
||||||
|
{ mediaType: currentMediaType, timeWindow: currentTimeWindow }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -36,8 +48,53 @@ const Trending = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={intl.formatMessage(messages.trending)} />
|
<PageTitle title={intl.formatMessage(messages.trending)} />
|
||||||
<div className="mb-5 mt-1">
|
<div className="mb-5 mt-1 flex flex-col justify-between lg:flex-row lg:items-end">
|
||||||
<Header>{intl.formatMessage(messages.trending)}</Header>
|
<Header>{intl.formatMessage(messages.trending)}</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-sm text-gray-100">
|
||||||
|
<CircleStackIcon className="h-6 w-6" />
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
id="mediaType"
|
||||||
|
name="mediaType"
|
||||||
|
onChange={(e) => setCurrentMediaType(e.target.value as MediaType)}
|
||||||
|
value={currentMediaType}
|
||||||
|
className="rounded-r-only"
|
||||||
|
>
|
||||||
|
<option value="all">
|
||||||
|
{intl.formatMessage(globalMessages.all)}
|
||||||
|
</option>
|
||||||
|
<option value="movie">
|
||||||
|
{intl.formatMessage(globalMessages.movies)}
|
||||||
|
</option>
|
||||||
|
<option value="tv">
|
||||||
|
{intl.formatMessage(globalMessages.tvshows)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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-sm text-gray-100">
|
||||||
|
<FunnelIcon className="h-6 w-6" />
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
id="timeWindow"
|
||||||
|
name="timeWindow"
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentTimeWindow(e.target.value as TimeWindow)
|
||||||
|
}
|
||||||
|
value={currentTimeWindow}
|
||||||
|
className="rounded-r-only"
|
||||||
|
>
|
||||||
|
<option value="day">
|
||||||
|
{intl.formatMessage(messages.timeWindowDay)}
|
||||||
|
</option>
|
||||||
|
<option value="week">
|
||||||
|
{intl.formatMessage(messages.timeWindowWeek)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
items={titles}
|
items={titles}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
"components.Discover.resetwarning": "Reset all sliders to default. This will also delete any custom sliders!",
|
"components.Discover.resetwarning": "Reset all sliders to default. This will also delete any custom sliders!",
|
||||||
"components.Discover.stopediting": "Stop Editing",
|
"components.Discover.stopediting": "Stop Editing",
|
||||||
"components.Discover.studios": "Studios",
|
"components.Discover.studios": "Studios",
|
||||||
|
"components.Discover.timeWindowDay": "Daily",
|
||||||
|
"components.Discover.timeWindowWeek": "Weekly",
|
||||||
"components.Discover.tmdbmoviegenre": "TMDB Movie Genre",
|
"components.Discover.tmdbmoviegenre": "TMDB Movie Genre",
|
||||||
"components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword",
|
"components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword",
|
||||||
"components.Discover.tmdbmoviestreamingservices": "TMDB Movie Streaming Services",
|
"components.Discover.tmdbmoviestreamingservices": "TMDB Movie Streaming Services",
|
||||||
|
|||||||
Reference in New Issue
Block a user