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

View File

@@ -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,
}, },
} }
); );

View File

@@ -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', {

View File

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

View File

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