diff --git a/overseerr-api.yml b/overseerr-api.yml index b4117f760e..c6502b3f6a 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3228,6 +3228,11 @@ paths: schema: type: number example: 10751 + - in: query + name: studio + schema: + type: number + example: 2 responses: '200': description: Results @@ -3311,6 +3316,11 @@ paths: schema: type: number example: 18 + - in: query + name: network + schema: + type: number + example: 1 responses: '200': description: Results @@ -4371,6 +4381,46 @@ paths: name: type: string example: English + /studio/{studioId}: + get: + summary: Get movie studio details + description: Returns movie studio details in a JSON object. + tags: + - tmdb + parameters: + - in: path + name: studioId + required: true + schema: + type: number + example: 2 + responses: + '200': + description: Movie studio details + content: + application/json: + schema: + $ref: '#/components/schemas/ProductionCompany' + /network/{networkId}: + get: + summary: Get TV network details + description: Returns TV network details in a JSON object. + tags: + - tmdb + parameters: + - in: path + name: networkId + required: true + schema: + type: number + example: 1 + responses: + '200': + description: TV network details + content: + application/json: + schema: + $ref: '#/components/schemas/ProductionCompany' /genres/movie: get: summary: Get list of official TMDb movie genres diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index 797325c599..3f7b309d40 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -3,11 +3,13 @@ import cacheManager from '../../lib/cache'; import ExternalAPI from '../externalapi'; import { TmdbCollection, + TmdbStudio, TmdbExternalIdResponse, TmdbGenre, TmdbGenresResult, TmdbLanguage, TmdbMovieDetails, + TmdbNetwork, TmdbPersonCombinedCredits, TmdbPersonDetail, TmdbRegion, @@ -33,6 +35,7 @@ interface DiscoverMovieOptions { primaryReleaseDateGte?: string; primaryReleaseDateLte?: string; genre?: number; + studio?: number; sortBy?: | 'popularity.asc' | 'popularity.desc' @@ -57,6 +60,7 @@ interface DiscoverTvOptions { firstAirDateLte?: string; includeEmptyReleaseDate?: boolean; genre?: number; + network?: number; sortBy?: | 'popularity.asc' | 'popularity.desc' @@ -365,6 +369,7 @@ class TheMovieDb extends ExternalAPI { primaryReleaseDateGte, primaryReleaseDateLte, genre, + studio, }: DiscoverMovieOptions = {}): Promise => { try { const data = await this.get('/discover/movie', { @@ -379,6 +384,7 @@ class TheMovieDb extends ExternalAPI { 'primary_release_date.gte': primaryReleaseDateGte, 'primary_release_date.lte': primaryReleaseDateLte, with_genres: genre, + with_companies: studio, }, }); @@ -396,6 +402,7 @@ class TheMovieDb extends ExternalAPI { firstAirDateLte, includeEmptyReleaseDate = false, genre, + network, }: DiscoverTvOptions = {}): Promise => { try { const data = await this.get('/discover/tv', { @@ -409,6 +416,7 @@ class TheMovieDb extends ExternalAPI { with_original_language: this.originalLanguage, include_null_first_air_dates: includeEmptyReleaseDate, with_genres: genre, + with_networks: network, }, }); @@ -668,6 +676,26 @@ class TheMovieDb extends ExternalAPI { } } + public async getStudio(studioId: number): Promise { + try { + const data = await this.get(`/studio/${studioId}`); + + return data; + } catch (e) { + throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`); + } + } + + public async getNetwork(networkId: number): Promise { + try { + const data = await this.get(`/network/${networkId}`); + + return data; + } catch (e) { + throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`); + } + } + public async getMovieGenres(): Promise { try { const data = await this.get( diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts index 5e79fc0241..51ae3f270d 100644 --- a/server/api/themoviedb/interfaces.ts +++ b/server/api/themoviedb/interfaces.ts @@ -390,3 +390,23 @@ export interface TmdbGenre { id: number; name: string; } + +export interface TmdbStudio { + id: number; + name: string; + description?: string; + headquarters?: string; + homepage?: string; + logo_path?: string; + origin_country?: string; + parent_company?: TmdbStudio; +} + +export interface TmdbNetwork { + id: number; + name: string; + headquarters?: string; + homepage?: string; + logo_path?: string; + origin_country?: string; +} diff --git a/server/routes/discover.ts b/server/routes/discover.ts index ed31017042..4879b4b3b6 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -39,6 +39,7 @@ discoverRoutes.get('/movies', async (req, res) => { page: Number(req.query.page), language: req.query.language as string, genre: req.query.genre ? Number(req.query.genre) : undefined, + studio: req.query.studio ? Number(req.query.studio) : undefined, }); const media = await Media.getRelatedMedia( @@ -101,6 +102,7 @@ discoverRoutes.get('/tv', async (req, res) => { page: Number(req.query.page), language: req.query.language as string, genre: req.query.genre ? Number(req.query.genre) : undefined, + network: req.query.network ? Number(req.query.network) : undefined, }); const media = await Media.getRelatedMedia( diff --git a/server/routes/index.ts b/server/routes/index.ts index b19b5ff3e7..b4a4162466 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -74,6 +74,22 @@ router.get('/languages', isAuthenticated(), async (req, res) => { return res.status(200).json(languages); }); +router.get<{ id: string }>('/studio/:id', async (req, res) => { + const tmdb = new TheMovieDb(); + + const studio = await tmdb.getStudio(Number(req.params.id)); + + return res.status(200).json(studio); +}); + +router.get<{ id: string }>('/network/:id', async (req, res) => { + const tmdb = new TheMovieDb(); + + const network = await tmdb.getNetwork(Number(req.params.id)); + + return res.status(200).json(network); +}); + router.get('/genres/movie', isAuthenticated(), async (req, res) => { const tmdb = new TheMovieDb(); diff --git a/src/components/Discover/DiscoverMovies.tsx b/src/components/Discover/DiscoverMovies.tsx index 249e9a4765..fb96b740f9 100644 --- a/src/components/Discover/DiscoverMovies.tsx +++ b/src/components/Discover/DiscoverMovies.tsx @@ -9,11 +9,15 @@ import useSettings from '../../hooks/useSettings'; import { MediaStatus } from '../../../server/constants/media'; import PageTitle from '../Common/PageTitle'; import { useRouter } from 'next/router'; -import { TmdbGenre } from '../../../server/api/themoviedb/interfaces'; +import { + TmdbStudio, + TmdbGenre, +} from '../../../server/api/themoviedb/interfaces'; const messages = defineMessages({ discovermovies: 'Popular Movies', genreMovies: '{genre} Movies', + studioMovies: '{studio} Movies', }); interface SearchResult { @@ -32,6 +36,10 @@ const DiscoverMovies: React.FC = () => { const { data: genres } = useSWR('/api/v1/genres/movie'); const genre = genres?.find((g) => g.id === Number(router.query.genreId)); + const { data: studio } = useSWR( + `/api/v1/studio/${router.query.studioId}` + ); + const { data, error, size, setSize } = useSWRInfinite( (pageIndex: number, previousPageData: SearchResult | null) => { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { @@ -40,7 +48,7 @@ const DiscoverMovies: React.FC = () => { return `/api/v1/discover/movies?page=${pageIndex + 1}&language=${locale}${ genre ? `&genre=${genre.id}` : '' - }`; + }${studio ? `&studio=${studio.id}` : ''}`; }, { initialSize: 3, @@ -80,6 +88,8 @@ const DiscoverMovies: React.FC = () => { const title = genre ? intl.formatMessage(messages.genreMovies, { genre: genre.name }) + : studio + ? intl.formatMessage(messages.studioMovies, { studio: studio.name }) : intl.formatMessage(messages.discovermovies); return ( diff --git a/src/components/Discover/DiscoverTv.tsx b/src/components/Discover/DiscoverTv.tsx index 7d7e3eafa7..05243dfa33 100644 --- a/src/components/Discover/DiscoverTv.tsx +++ b/src/components/Discover/DiscoverTv.tsx @@ -9,11 +9,15 @@ import useSettings from '../../hooks/useSettings'; import { MediaStatus } from '../../../server/constants/media'; import PageTitle from '../Common/PageTitle'; import { useRouter } from 'next/router'; -import { TmdbGenre } from '../../../server/api/themoviedb/interfaces'; +import { + TmdbGenre, + TmdbNetwork, +} from '../../../server/api/themoviedb/interfaces'; const messages = defineMessages({ discovertv: 'Popular Series', genreSeries: '{genre} Series', + networkSeries: '{network} Series', }); interface SearchResult { @@ -32,6 +36,10 @@ const DiscoverTv: React.FC = () => { const { data: genres } = useSWR('/api/v1/genres/tv'); const genre = genres?.find((g) => g.id === Number(router.query.genreId)); + const { data: network } = useSWR( + `/api/v1/network/${router.query.networkId}` + ); + const { data, error, size, setSize } = useSWRInfinite( (pageIndex: number, previousPageData: SearchResult | null) => { if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { @@ -40,7 +48,7 @@ const DiscoverTv: React.FC = () => { return `/api/v1/discover/tv?page=${pageIndex + 1}&language=${locale}${ genre ? `&genre=${genre.id}` : '' - }`; + }${network ? `&network=${network.id}` : ''}`; }, { initialSize: 3, @@ -79,6 +87,8 @@ const DiscoverTv: React.FC = () => { const title = genre ? intl.formatMessage(messages.genreSeries, { genre: genre.name }) + : network + ? intl.formatMessage(messages.networkSeries, { network: network.name }) : intl.formatMessage(messages.discovertv); return ( diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 652fdef9b5..afedec9573 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -672,7 +672,13 @@ const MovieDetails: React.FC = ({ movie }) => { {intl.formatMessage(messages.studio)} - {data.productionCompanies[0]?.name} + + + {data.productionCompanies[0].name} + + )} diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index d46bbdbbf2..b80d7688ad 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -686,7 +686,20 @@ const TvDetails: React.FC = ({ tv }) => { {intl.formatMessage(messages.network)} - {data.networks.map((n) => n.name).join(', ')} + {data.networks + .map((n) => ( + + {n.name} + + )) + .reduce((prev, curr) => ( + <> + {prev}, {curr} + + ))} )} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index d2e08f828f..b6d0f6a11d 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -19,11 +19,13 @@ "components.Discover.discovertv": "Popular Series", "components.Discover.genreMovies": "{genre} Movies", "components.Discover.genreSeries": "{genre} Series", + "components.Discover.networkSeries": "{network} Series", "components.Discover.nopending": "No Pending Requests", "components.Discover.popularmovies": "Popular Movies", "components.Discover.populartv": "Popular Series", "components.Discover.recentlyAdded": "Recently Added", "components.Discover.recentrequests": "Recent Requests", + "components.Discover.studioMovies": "{studio} Movies", "components.Discover.trending": "Trending", "components.Discover.upcoming": "Upcoming Movies", "components.Discover.upcomingmovies": "Upcoming Movies", diff --git a/src/pages/discover/movies/studio/[studioId]/index.tsx b/src/pages/discover/movies/studio/[studioId]/index.tsx new file mode 100644 index 0000000000..e1371e60d8 --- /dev/null +++ b/src/pages/discover/movies/studio/[studioId]/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { NextPage } from 'next'; +import DiscoverMovies from '../../../../../components/Discover/DiscoverMovies'; + +const DiscoverMoviesStudioPage: NextPage = () => { + return ; +}; + +export default DiscoverMoviesStudioPage; diff --git a/src/pages/discover/tv/network/[networkId]/index.tsx b/src/pages/discover/tv/network/[networkId]/index.tsx new file mode 100644 index 0000000000..b30f537742 --- /dev/null +++ b/src/pages/discover/tv/network/[networkId]/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { NextPage } from 'next'; +import DiscoverTv from '../../../../../components/Discover/DiscoverTv'; + +const DiscoverTvNetworkPage: NextPage = () => { + return ; +}; + +export default DiscoverTvNetworkPage;