diff --git a/seerr-api.yml b/seerr-api.yml index aaaf30bf..99ef16cc 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -5964,6 +5964,23 @@ paths: schema: type: string 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: '200': description: Results diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index e7621a4a..666c7a25 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -715,9 +715,11 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { public getMovieTrending = async ({ page = 1, timeWindow = 'day', + language = this.locale, }: { page?: number; timeWindow?: 'day' | 'week'; + language?: string; } = {}): Promise => { try { const data = await this.get( @@ -725,6 +727,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { { params: { page, + language, }, } ); @@ -738,9 +741,11 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { public getTvTrending = async ({ page = 1, timeWindow = 'day', + language = this.locale, }: { page?: number; timeWindow?: 'day' | 'week'; + language?: string; } = {}): Promise => { try { const data = await this.get( @@ -748,6 +753,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider { { params: { page, + language, }, } ); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index b40ba996..d467976e 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -673,10 +673,41 @@ discoverRoutes.get('/trending', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); try { - const data = await tmdb.getAllTrending({ - page: Number(req.query.page), - language: (req.query.language as string) ?? req.locale, - }); + const mediaType = (req.query.mediaType as 'all' | 'movie' | 'tv') ?? 'all'; + const timeWindow = + (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( req.user, @@ -687,27 +718,16 @@ discoverRoutes.get('/trending', async (req, res, next) => { page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - results: data.results.map((result) => - isMovie(result) - ? mapMovieResult( - result, - media.find( - (med) => - med.tmdbId === result.id && med.mediaType === MediaType.MOVIE - ) - ) - : isPerson(result) - ? mapPersonResult(result) - : isCollection(result) - ? mapCollectionResult(result) - : mapTvResult( - result, - media.find( - (med) => - med.tmdbId === result.id && med.mediaType === MediaType.TV - ) - ) - ), + results: data.results.map((result) => { + // - If "type" is set (case: "movie" or "tv"), the mediaType must also match. + // - If "type" is not set (case: "all"), only filter by tmdbId. + const selectedMedia = media.find( + (med) => + med.tmdbId === result.id && (type ? med.mediaType === type : true) + ); + + return mapper(result, selectedMedia); + }), }); } catch (e) { logger.debug('Something went wrong retrieving trending items', { diff --git a/src/components/Discover/Trending.tsx b/src/components/Discover/Trending.tsx index 36169288..d8942cab 100644 --- a/src/components/Discover/Trending.tsx +++ b/src/components/Discover/Trending.tsx @@ -2,21 +2,32 @@ import Header from '@app/components/Common/Header'; import ListView from '@app/components/Common/ListView'; import PageTitle from '@app/components/Common/PageTitle'; import useDiscover from '@app/hooks/useDiscover'; +import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; +import { CircleStackIcon, FunnelIcon } from '@heroicons/react/24/solid'; import type { MovieResult, PersonResult, TvResult, } from '@server/models/Search'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; const messages = defineMessages('components.Discover', { trending: 'Trending', + timeWindowDay: 'Daily', + timeWindowWeek: 'Weekly', }); +type MediaType = 'all' | 'movie' | 'tv'; + +type TimeWindow = 'day' | 'week'; + const Trending = () => { const intl = useIntl(); + const [currentMediaType, setCurrentMediaType] = useState('all'); + const [currentTimeWindow, setCurrentTimeWindow] = useState('day'); const { isLoadingInitialData, isEmpty, @@ -26,7 +37,8 @@ const Trending = () => { fetchMore, error, } = useDiscover( - '/api/v1/discover/trending' + '/api/v1/discover/trending', + { mediaType: currentMediaType, timeWindow: currentTimeWindow } ); if (error) { @@ -36,8 +48,53 @@ const Trending = () => { return ( <> -
+
{intl.formatMessage(messages.trending)}
+
+
+ + + + +
+
+ + + + +
+