feat(trending): add filter options (#2137)

Signed-off-by: Florian Hoech <code@florians-web.de>
This commit is contained in:
bogo22
2026-03-05 11:24:55 +01:00
committed by GitHub
parent 3152f727ef
commit 4ce0db1959
5 changed files with 129 additions and 27 deletions

View File

@@ -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

View File

@@ -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<TmdbSearchMovieResponse> => {
try {
const data = await this.get<TmdbSearchMovieResponse>(
@@ -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<TmdbSearchTvResponse> => {
try {
const data = await this.get<TmdbSearchTvResponse>(
@@ -748,6 +753,7 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
{
params: {
page,
language,
},
}
);

View File

@@ -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(
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 && 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
)
)
),
med.tmdbId === result.id && (type ? med.mediaType === type : true)
);
return mapper(result, selectedMedia);
}),
});
} catch (e) {
logger.debug('Something went wrong retrieving trending items', {

View File

@@ -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<MediaType>('all');
const [currentTimeWindow, setCurrentTimeWindow] = useState<TimeWindow>('day');
const {
isLoadingInitialData,
isEmpty,
@@ -26,7 +37,8 @@ const Trending = () => {
fetchMore,
error,
} = useDiscover<MovieResult | TvResult | PersonResult>(
'/api/v1/discover/trending'
'/api/v1/discover/trending',
{ mediaType: currentMediaType, timeWindow: currentTimeWindow }
);
if (error) {
@@ -36,8 +48,53 @@ const Trending = () => {
return (
<>
<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>
<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>
<ListView
items={titles}

View File

@@ -124,6 +124,8 @@
"components.Discover.resetwarning": "Reset all sliders to default. This will also delete any custom sliders!",
"components.Discover.stopediting": "Stop Editing",
"components.Discover.studios": "Studios",
"components.Discover.timeWindowDay": "Daily",
"components.Discover.timeWindowWeek": "Weekly",
"components.Discover.tmdbmoviegenre": "TMDB Movie Genre",
"components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword",
"components.Discover.tmdbmoviestreamingservices": "TMDB Movie Streaming Services",