From 75011610e57f03098c8be9375d0c9ba1e3647e9b Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:23:37 -0500 Subject: [PATCH] feat: add language-filtered Discover pages (#1111) --- overseerr-api.yml | 94 +++++++++ server/api/themoviedb/index.ts | 8 +- server/routes/discover.ts | 183 ++++++++++++++---- .../Discover/DiscoverMovieLanguage/index.tsx | 71 +++++++ .../Discover/DiscoverTvLanguage/index.tsx | 71 +++++++ src/components/MovieDetails/index.tsx | 20 +- src/components/TvDetails/index.tsx | 20 +- src/i18n/locale/en.json | 2 + .../movies/language/[language]/index.tsx | 9 + .../discover/tv/language/[language]/index.tsx | 9 + 10 files changed, 430 insertions(+), 57 deletions(-) create mode 100644 src/components/Discover/DiscoverMovieLanguage/index.tsx create mode 100644 src/components/Discover/DiscoverTvLanguage/index.tsx create mode 100644 src/pages/discover/movies/language/[language]/index.tsx create mode 100644 src/pages/discover/tv/language/[language]/index.tsx diff --git a/overseerr-api.yml b/overseerr-api.yml index 0de788e0..7c28dcfd 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3332,6 +3332,53 @@ paths: type: array items: $ref: '#/components/schemas/MovieResult' + /discover/movies/language/{language}: + get: + summary: Discover movies by original language + description: Returns a list of movies based on the provided ISO 639-1 language code in a JSON object. + tags: + - search + parameters: + - in: path + name: language + required: true + schema: + type: string + example: en + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + language: + $ref: '#/components/schemas/SpokenLanguage' + results: + type: array + items: + $ref: '#/components/schemas/MovieResult' /discover/movies/studio/{studioId}: get: summary: Discover movies by studio @@ -3467,6 +3514,53 @@ paths: type: array items: $ref: '#/components/schemas/TvResult' + /discover/tv/language/{language}: + get: + summary: Discover TV shows by original language + description: Returns a list of TV shows based on the provided ISO 639-1 language code in a JSON object. + tags: + - search + parameters: + - in: path + name: language + required: true + schema: + type: string + example: en + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + - in: query + name: language + schema: + type: string + example: en + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + language: + $ref: '#/components/schemas/SpokenLanguage' + results: + type: array + items: + $ref: '#/components/schemas/TvResult' /discover/tv/genre/{genreId}: get: summary: Discover TV shows by genre diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 6a059520..e98ebb7e 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -34,6 +34,7 @@ interface DiscoverMovieOptions { language?: string; primaryReleaseDateGte?: string; primaryReleaseDateLte?: string; + originalLanguage?: string; genre?: number; studio?: number; sortBy?: @@ -59,6 +60,7 @@ interface DiscoverTvOptions { firstAirDateGte?: string; firstAirDateLte?: string; includeEmptyReleaseDate?: boolean; + originalLanguage?: string; genre?: number; network?: number; sortBy?: @@ -368,6 +370,7 @@ class TheMovieDb extends ExternalAPI { language = 'en', primaryReleaseDateGte, primaryReleaseDateLte, + originalLanguage, genre, studio, }: DiscoverMovieOptions = {}): Promise => { @@ -379,7 +382,7 @@ class TheMovieDb extends ExternalAPI { include_adult: includeAdult, language, region: this.region, - with_original_language: this.originalLanguage, + with_original_language: originalLanguage ?? this.originalLanguage, 'primary_release_date.gte': primaryReleaseDateGte, 'primary_release_date.lte': primaryReleaseDateLte, with_genres: genre, @@ -400,6 +403,7 @@ class TheMovieDb extends ExternalAPI { firstAirDateGte, firstAirDateLte, includeEmptyReleaseDate = false, + originalLanguage, genre, network, }: DiscoverTvOptions = {}): Promise => { @@ -412,7 +416,7 @@ class TheMovieDb extends ExternalAPI { region: this.region, 'first_air_date.gte': firstAirDateGte, 'first_air_date.lte': firstAirDateLte, - with_original_language: this.originalLanguage, + with_original_language: originalLanguage ?? this.originalLanguage, include_null_first_air_dates: includeEmptyReleaseDate, with_genres: genre, with_networks: network, diff --git a/server/routes/discover.ts b/server/routes/discover.ts index d2ffaf2c..c46048ae 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -63,23 +63,25 @@ discoverRoutes.get('/movies', async (req, res) => { }); }); -discoverRoutes.get<{ genreId: string }>( - '/movies/genre/:genreId', - async (req, res) => { +discoverRoutes.get<{ language: string }>( + '/movies/language/:language', + async (req, res, next) => { const tmdb = createTmdbWithRegionLanaguage(req.user); - const genres = await tmdb.getMovieGenres({ - language: req.query.language as string, - }); + const languages = await tmdb.getLanguages(); - const genre = genres.find( - (genre) => genre.id === Number(req.params.genreId) + const language = languages.find( + (lang) => lang.iso_639_1 === req.params.language ); + if (!language) { + return next({ status: 404, message: 'Unable to retrieve language' }); + } + const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page), language: req.query.language as string, - genre: Number(req.params.genreId), + originalLanguage: req.params.language, }); const media = await Media.getRelatedMedia( @@ -90,7 +92,7 @@ discoverRoutes.get<{ genreId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - genre, + language, results: data.results.map((result) => mapMovieResult( result, @@ -104,17 +106,27 @@ discoverRoutes.get<{ genreId: string }>( } ); -discoverRoutes.get<{ studioId: string }>( - '/movies/studio/:studioId', - async (req, res) => { - const tmdb = new TheMovieDb(); +discoverRoutes.get<{ genreId: string }>( + '/movies/genre/:genreId', + async (req, res, next) => { + const tmdb = createTmdbWithRegionLanaguage(req.user); - const studio = await tmdb.getStudio(Number(req.params.studioId)); + const genres = await tmdb.getMovieGenres({ + language: req.query.language as string, + }); + + const genre = genres.find( + (genre) => genre.id === Number(req.params.genreId) + ); + + if (!genre) { + return next({ status: 404, message: 'Unable to retrieve genre' }); + } const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page), language: req.query.language as string, - studio: Number(req.params.studioId), + genre: Number(req.params.genreId), }); const media = await Media.getRelatedMedia( @@ -125,7 +137,7 @@ discoverRoutes.get<{ studioId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - studio: mapProductionCompany(studio), + genre, results: data.results.map((result) => mapMovieResult( result, @@ -139,6 +151,45 @@ discoverRoutes.get<{ studioId: string }>( } ); +discoverRoutes.get<{ studioId: string }>( + '/movies/studio/:studioId', + async (req, res, next) => { + const tmdb = new TheMovieDb(); + + try { + const studio = await tmdb.getStudio(Number(req.params.studioId)); + + const data = await tmdb.getDiscoverMovies({ + page: Number(req.query.page), + language: req.query.language as string, + studio: Number(req.params.studioId), + }); + + const media = await Media.getRelatedMedia( + data.results.map((result) => result.id) + ); + + return res.status(200).json({ + page: data.page, + totalPages: data.total_pages, + totalResults: data.total_results, + studio: mapProductionCompany(studio), + results: data.results.map((result) => + mapMovieResult( + result, + media.find( + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.MOVIE + ) + ) + ), + }); + } catch (e) { + return next({ status: 404, message: 'Unable to retrieve studio' }); + } + } +); + discoverRoutes.get('/movies/upcoming', async (req, res) => { const tmdb = createTmdbWithRegionLanaguage(req.user); @@ -202,23 +253,25 @@ discoverRoutes.get('/tv', async (req, res) => { }); }); -discoverRoutes.get<{ genreId: string }>( - '/tv/genre/:genreId', - async (req, res) => { +discoverRoutes.get<{ language: string }>( + '/tv/language/:language', + async (req, res, next) => { const tmdb = createTmdbWithRegionLanaguage(req.user); - const genres = await tmdb.getTvGenres({ - language: req.query.language as string, - }); + const languages = await tmdb.getLanguages(); - const genre = genres.find( - (genre) => genre.id === Number(req.params.genreId) + const language = languages.find( + (lang) => lang.iso_639_1 === req.params.language ); + if (!language) { + return next({ status: 404, message: 'Unable to retrieve language' }); + } + const data = await tmdb.getDiscoverTv({ page: Number(req.query.page), language: req.query.language as string, - genre: Number(req.params.genreId), + originalLanguage: req.params.language, }); const media = await Media.getRelatedMedia( @@ -229,7 +282,7 @@ discoverRoutes.get<{ genreId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - genre, + language, results: data.results.map((result) => mapTvResult( result, @@ -242,17 +295,27 @@ discoverRoutes.get<{ genreId: string }>( } ); -discoverRoutes.get<{ networkId: string }>( - '/tv/network/:networkId', - async (req, res) => { - const tmdb = new TheMovieDb(); +discoverRoutes.get<{ genreId: string }>( + '/tv/genre/:genreId', + async (req, res, next) => { + const tmdb = createTmdbWithRegionLanaguage(req.user); - const network = await tmdb.getNetwork(Number(req.params.networkId)); + const genres = await tmdb.getTvGenres({ + language: req.query.language as string, + }); + + const genre = genres.find( + (genre) => genre.id === Number(req.params.genreId) + ); + + if (!genre) { + return next({ status: 404, message: 'Unable to retrieve genre' }); + } const data = await tmdb.getDiscoverTv({ page: Number(req.query.page), language: req.query.language as string, - network: Number(req.params.networkId), + genre: Number(req.params.genreId), }); const media = await Media.getRelatedMedia( @@ -263,7 +326,7 @@ discoverRoutes.get<{ networkId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - network: mapNetwork(network), + genre, results: data.results.map((result) => mapTvResult( result, @@ -276,6 +339,45 @@ discoverRoutes.get<{ networkId: string }>( } ); +discoverRoutes.get<{ networkId: string }>( + '/tv/network/:networkId', + async (req, res, next) => { + const tmdb = new TheMovieDb(); + + try { + const network = await tmdb.getNetwork(Number(req.params.networkId)); + + const data = await tmdb.getDiscoverTv({ + page: Number(req.query.page), + language: req.query.language as string, + network: Number(req.params.networkId), + }); + + const media = await Media.getRelatedMedia( + data.results.map((result) => result.id) + ); + + return res.status(200).json({ + page: data.page, + totalPages: data.total_pages, + totalResults: data.total_results, + network: mapNetwork(network), + results: data.results.map((result) => + mapTvResult( + result, + media.find( + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.TV + ) + ) + ), + }); + } catch (e) { + return next({ status: 404, message: 'Unable to retrieve network' }); + } + } +); + discoverRoutes.get('/tv/upcoming', async (req, res) => { const tmdb = createTmdbWithRegionLanaguage(req.user); @@ -331,15 +433,18 @@ discoverRoutes.get('/trending', async (req, res) => { ? mapMovieResult( result, media.find( - (req) => - req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.MOVIE ) ) : isPerson(result) ? mapPersonResult(result) : mapTvResult( result, - media.find((req) => req.tmdbId === result.id && MediaType.TV) + media.find( + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.TV + ) ) ), }); @@ -368,8 +473,8 @@ discoverRoutes.get<{ keywordId: string }>( mapMovieResult( result, media.find( - (req) => - req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.MOVIE ) ) ), diff --git a/src/components/Discover/DiscoverMovieLanguage/index.tsx b/src/components/Discover/DiscoverMovieLanguage/index.tsx new file mode 100644 index 00000000..b1e19d05 --- /dev/null +++ b/src/components/Discover/DiscoverMovieLanguage/index.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import type { MovieResult } from '../../../../server/models/Search'; +import ListView from '../../Common/ListView'; +import { defineMessages, useIntl } from 'react-intl'; +import Header from '../../Common/Header'; +import PageTitle from '../../Common/PageTitle'; +import { useRouter } from 'next/router'; +import globalMessages from '../../../i18n/globalMessages'; +import useDiscover from '../../../hooks/useDiscover'; +import Error from '../../../pages/_error'; + +const messages = defineMessages({ + languageMovies: '{language} Movies', +}); + +const DiscoverMovieLanguage: React.FC = () => { + const router = useRouter(); + const intl = useIntl(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + } = useDiscover< + MovieResult, + { + originalLanguage: { + iso_639_1: string; + english_name: string; + name: string; + }; + } + >(`/api/v1/discover/movies/language/${router.query.language}`); + + if (error) { + return ; + } + + const title = isLoadingInitialData + ? intl.formatMessage(globalMessages.loading) + : intl.formatMessage(messages.languageMovies, { + language: intl.formatDisplayName(router.query.language as string, { + type: 'language', + fallback: 'none', + }), + }); + + return ( + <> + +
+
{title}
+
+ 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverMovieLanguage; diff --git a/src/components/Discover/DiscoverTvLanguage/index.tsx b/src/components/Discover/DiscoverTvLanguage/index.tsx new file mode 100644 index 00000000..ed0873f9 --- /dev/null +++ b/src/components/Discover/DiscoverTvLanguage/index.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import type { TvResult } from '../../../../server/models/Search'; +import ListView from '../../Common/ListView'; +import { defineMessages, useIntl } from 'react-intl'; +import Header from '../../Common/Header'; +import PageTitle from '../../Common/PageTitle'; +import { useRouter } from 'next/router'; +import globalMessages from '../../../i18n/globalMessages'; +import useDiscover from '../../../hooks/useDiscover'; +import Error from '../../../pages/_error'; + +const messages = defineMessages({ + languageSeries: '{language} Series', +}); + +const DiscoverTvLanguage: React.FC = () => { + const router = useRouter(); + const intl = useIntl(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + } = useDiscover< + TvResult, + { + originalLanguage: { + iso_639_1: string; + english_name: string; + name: string; + }; + } + >(`/api/v1/discover/tv/language/${router.query.language}`); + + if (error) { + return ; + } + + const title = isLoadingInitialData + ? intl.formatMessage(globalMessages.loading) + : intl.formatMessage(messages.languageSeries, { + language: intl.formatDisplayName(router.query.language as string, { + type: 'language', + fallback: 'none', + }), + }); + + return ( + <> + +
+
{title}
+
+ 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverTvLanguage; diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 9d898815..d76dfaa6 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -651,19 +651,23 @@ const MovieDetails: React.FC = ({ movie }) => { )} - {data.spokenLanguages.some( - (lng) => lng.iso_639_1 === data.originalLanguage - ) && ( + {data.originalLanguage && (
{intl.formatMessage(messages.originallanguage)} - { - data.spokenLanguages.find( - (lng) => lng.iso_639_1 === data.originalLanguage - )?.name - } + + + {intl.formatDisplayName(data.originalLanguage, { + type: 'language', + fallback: 'none', + }) ?? + data.spokenLanguages.find( + (lng) => lng.iso_639_1 === data.originalLanguage + )?.name} + +
)} diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 930f6a6a..6ae24e0e 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -675,19 +675,23 @@ const TvDetails: React.FC = ({ tv }) => { {data.status} - {data.spokenLanguages.some( - (lng) => lng.iso_639_1 === data.originalLanguage - ) && ( + {data.originalLanguage && (
{intl.formatMessage(messages.originallanguage)} - { - data.spokenLanguages.find( - (lng) => lng.iso_639_1 === data.originalLanguage - )?.name - } + + + {intl.formatDisplayName(data.originalLanguage, { + type: 'language', + fallback: 'none', + }) ?? + data.spokenLanguages.find( + (lng) => lng.iso_639_1 === data.originalLanguage + )?.name} + +
)} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 319f76ae..5ff9f8cd 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -15,9 +15,11 @@ "components.CollectionDetails.requestswillbecreated4k": "The following titles will have 4K requests created for them:", "components.Common.ListView.noresults": "No results.", "components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Movies", + "components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Movies", "components.Discover.DiscoverNetwork.networkSeries": "{network} Series", "components.Discover.DiscoverStudio.studioMovies": "{studio} Movies", "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series", + "components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series", "components.Discover.NetworkSlider.networks": "Networks", "components.Discover.StudioSlider.studios": "Studios", "components.Discover.discover": "Discover", diff --git a/src/pages/discover/movies/language/[language]/index.tsx b/src/pages/discover/movies/language/[language]/index.tsx new file mode 100644 index 00000000..a1fba4fe --- /dev/null +++ b/src/pages/discover/movies/language/[language]/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { NextPage } from 'next'; +import DiscoverMovieLanguage from '../../../../../components/Discover/DiscoverMovieLanguage'; + +const DiscoverMovieLanguagePage: NextPage = () => { + return ; +}; + +export default DiscoverMovieLanguagePage; diff --git a/src/pages/discover/tv/language/[language]/index.tsx b/src/pages/discover/tv/language/[language]/index.tsx new file mode 100644 index 00000000..7d81b6c4 --- /dev/null +++ b/src/pages/discover/tv/language/[language]/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { NextPage } from 'next'; +import DiscoverTvLanguage from '../../../../../components/Discover/DiscoverTvLanguage'; + +const DiscoverTvLanguagePage: NextPage = () => { + return ; +}; + +export default DiscoverTvLanguagePage;