refactor: move genre/studio/network calls into their own endpoints

this commit also adds a `useDiscover` hook to help with creating discover pages with less repeating
code
pull/1093/head
sct 4 years ago
parent ed0a7fbdf5
commit 63c122e5e0

@ -574,6 +574,19 @@ components:
type: string
name:
type: string
Network:
type: object
properties:
id:
type: number
example: 1
logoPath:
type: string
nullable: true
originCountry:
type: string
name:
type: string
RelatedVideo:
type: object
properties:
@ -3227,12 +3240,12 @@ paths:
name: genre
schema:
type: number
example: 10751
example: 18
- in: query
name: studio
schema:
type: number
example: 2
example: 1
responses:
'200':
description: Results
@ -3254,6 +3267,100 @@ paths:
type: array
items:
$ref: '#/components/schemas/MovieResult'
/discover/movies/genre/{genreId}:
get:
summary: Discover movies by genre
description: Returns a list of movies based on the provided genre ID in a JSON object.
tags:
- search
parameters:
- in: path
name: genreId
required: true
schema:
type: string
example: '1'
- 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
genre:
$ref: '#/components/schemas/Genre'
results:
type: array
items:
$ref: '#/components/schemas/MovieResult'
/discover/movies/studio/{studioId}:
get:
summary: Discover movies by studio
description: Returns a list of movies based on the provided studio ID in a JSON object.
tags:
- search
parameters:
- in: path
name: studioId
required: true
schema:
type: string
example: '1'
- 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
studio:
$ref: '#/components/schemas/ProductionCompany'
results:
type: array
items:
$ref: '#/components/schemas/MovieResult'
/discover/movies/upcoming:
get:
summary: Upcoming movies
@ -3342,6 +3449,100 @@ paths:
type: array
items:
$ref: '#/components/schemas/TvResult'
/discover/tv/genre/{genreId}:
get:
summary: Discover TV shows by genre
description: Returns a list of TV shows based on the provided genre ID in a JSON object.
tags:
- search
parameters:
- in: path
name: genreId
required: true
schema:
type: string
example: '1'
- 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
genre:
$ref: '#/components/schemas/Genre'
results:
type: array
items:
$ref: '#/components/schemas/TvResult'
/discover/tv/network/{networkId}:
get:
summary: Discover TV shows by network
description: Returns a list of TV shows based on the provided network ID in a JSON object.
tags:
- search
parameters:
- in: path
name: networkId
required: true
schema:
type: string
example: '1'
- 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
network:
$ref: '#/components/schemas/Network'
results:
type: array
items:
$ref: '#/components/schemas/TvResult'
/discover/tv/upcoming:
get:
summary: Discover Upcoming TV shows

@ -3,7 +3,6 @@ import cacheManager from '../../lib/cache';
import ExternalAPI from '../externalapi';
import {
TmdbCollection,
TmdbStudio,
TmdbExternalIdResponse,
TmdbGenre,
TmdbGenresResult,
@ -19,6 +18,7 @@ import {
TmdbSeasonWithEpisodes,
TmdbTvDetails,
TmdbUpcomingMoviesResponse,
TmdbProductionCompany,
} from './interfaces';
interface SearchOptions {
@ -675,9 +675,11 @@ class TheMovieDb extends ExternalAPI {
}
}
public async getStudio(studioId: number): Promise<TmdbStudio> {
public async getStudio(studioId: number): Promise<TmdbProductionCompany> {
try {
const data = await this.get<TmdbStudio>(`/company/${studioId}`);
const data = await this.get<TmdbProductionCompany>(
`/company/${studioId}`
);
return data;
} catch (e) {
@ -695,11 +697,19 @@ class TheMovieDb extends ExternalAPI {
}
}
public async getMovieGenres(): Promise<TmdbGenre[]> {
public async getMovieGenres({
language = 'en',
}: {
language?: string;
} = {}): Promise<TmdbGenre[]> {
try {
const data = await this.get<TmdbGenresResult>(
'/genre/movie/list',
{},
{
params: {
language,
},
},
86400 // 24 hours
);
@ -711,11 +721,19 @@ class TheMovieDb extends ExternalAPI {
}
}
public async getTvGenres(): Promise<TmdbGenre[]> {
public async getTvGenres({
language = 'en',
}: {
language?: string;
} = {}): Promise<TmdbGenre[]> {
try {
const data = await this.get<TmdbGenresResult>(
'/genre/tv/list',
{},
{
params: {
language,
},
},
86400 // 24 hours
);

@ -109,6 +109,16 @@ export interface TmdbExternalIds {
twitter_id?: string;
}
export interface TmdbProductionCompany {
id: number;
logo_path?: string;
name: string;
origin_country: string;
homepage?: string;
headquarters?: string;
description?: string;
}
export interface TmdbMovieDetails {
id: number;
imdb_id?: string;
@ -125,12 +135,7 @@ export interface TmdbMovieDetails {
original_title: string;
overview?: string;
popularity: number;
production_companies: {
id: number;
name: string;
logo_path?: string;
origin_country: string;
}[];
production_companies: TmdbProductionCompany[];
production_countries: {
iso_3166_1: string;
name: string;
@ -227,12 +232,7 @@ export interface TmdbTvDetails {
last_episode_to_air?: TmdbTvEpisodeResult;
name: string;
next_episode_to_air?: TmdbTvEpisodeResult;
networks: {
id: number;
name: string;
logo_path: string;
origin_country: string;
}[];
networks: TmdbNetwork[];
number_of_episodes: number;
number_of_seasons: number;
origin_country: string[];
@ -391,17 +391,6 @@ export interface TmdbGenre {
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;

@ -1,6 +1,7 @@
import type {
TmdbMovieDetails,
TmdbMovieReleaseResult,
TmdbProductionCompany,
} from '../api/themoviedb/interfaces';
import {
ProductionCompany,
@ -79,6 +80,18 @@ export interface MovieDetails {
plexUrl?: string;
}
export const mapProductionCompany = (
company: TmdbProductionCompany
): ProductionCompany => ({
id: company.id,
name: company.name,
originCountry: company.origin_country,
description: company.description,
headquarters: company.headquarters,
homepage: company.homepage,
logoPath: company.logo_path,
});
export const mapMovieDetails = (
movie: TmdbMovieDetails,
media?: Media
@ -91,12 +104,7 @@ export const mapMovieDetails = (
originalLanguage: movie.original_language,
originalTitle: movie.original_title,
popularity: movie.popularity,
productionCompanies: movie.production_companies.map((company) => ({
id: company.id,
logoPath: company.logo_path,
originCountry: company.origin_country,
name: company.name,
})),
productionCompanies: movie.production_companies.map(mapProductionCompany),
productionCountries: movie.production_countries,
releaseDate: movie.release_date,
releases: movie.release_dates,

@ -9,6 +9,7 @@ import {
mapExternalIds,
Keyword,
mapVideos,
TvNetwork,
} from './common';
import type {
TmdbTvEpisodeResult,
@ -16,6 +17,7 @@ import type {
TmdbTvDetails,
TmdbSeasonWithEpisodes,
TmdbTvRatingResult,
TmdbNetwork,
} from '../api/themoviedb/interfaces';
import type Media from '../entity/Media';
import { Video } from './Movie';
@ -77,7 +79,7 @@ export interface TvDetails {
lastEpisodeToAir?: Episode;
name: string;
nextEpisodeToAir?: Episode;
networks: ProductionCompany[];
networks: TvNetwork[];
numberOfEpisodes: number;
numberOfSeasons: number;
originCountry: string[];
@ -139,6 +141,15 @@ export const mapSeasonWithEpisodes = (
posterPath: season.poster_path,
});
export const mapNetwork = (network: TmdbNetwork): TvNetwork => ({
id: network.id,
name: network.name,
originCountry: network.origin_country,
headquarters: network.headquarters,
homepage: network.homepage,
logoPath: network.logo_path,
});
export const mapTvDetails = (
show: TmdbTvDetails,
media?: Media
@ -157,12 +168,7 @@ export const mapTvDetails = (
languages: show.languages,
lastAirDate: show.last_air_date,
name: show.name,
networks: show.networks.map((network) => ({
id: network.id,
name: network.name,
originCountry: network.origin_country,
logoPath: network.logo_path,
})),
networks: show.networks.map(mapNetwork),
numberOfEpisodes: show.number_of_episodes,
numberOfSeasons: show.number_of_seasons,
originCountry: show.origin_country,

@ -14,6 +14,18 @@ export interface ProductionCompany {
logoPath?: string;
originCountry: string;
name: string;
description?: string;
headquarters?: string;
homepage?: string;
}
export interface TvNetwork {
id: number;
logoPath?: string;
originCountry?: string;
name: string;
headquarters?: string;
homepage?: string;
}
export interface Keyword {

@ -6,6 +6,8 @@ import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import { User } from '../entity/User';
import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv';
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
const settings = getSettings();
@ -61,6 +63,82 @@ discoverRoutes.get('/movies', async (req, res) => {
});
});
discoverRoutes.get<{ genreId: string }>(
'/movies/genre/:genreId',
async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
});
const genre = genres.find(
(genre) => genre.id === Number(req.params.genreId)
);
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
genre: Number(req.params.genreId),
});
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,
genre,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
}
);
discoverRoutes.get<{ studioId: string }>(
'/movies/studio/:studioId',
async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);
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(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
}
);
discoverRoutes.get('/movies/upcoming', async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);
@ -124,6 +202,80 @@ discoverRoutes.get('/tv', async (req, res) => {
});
});
discoverRoutes.get<{ genreId: string }>(
'/tv/genre/:genreId',
async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
});
const genre = genres.find(
(genre) => genre.id === Number(req.params.genreId)
);
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
genre: Number(req.params.genreId),
});
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,
genre,
results: data.results.map((result) =>
mapTvResult(
result,
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
)
)
),
});
}
);
discoverRoutes.get<{ networkId: string }>(
'/tv/network/:networkId',
async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);
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
)
)
),
});
}
);
discoverRoutes.get('/tv/upcoming', async (req, res) => {
const tmdb = createTmdbWithRegionLanaguage(req.user);

@ -17,6 +17,8 @@ import { getAppVersion, getCommitTag } from '../utils/appVersion';
import serviceRoutes from './service';
import { appDataStatus, appDataPath } from '../utils/appDataVolume';
import TheMovieDb from '../api/themoviedb';
import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv';
const router = Router();
@ -79,7 +81,7 @@ router.get<{ id: string }>('/studio/:id', async (req, res) => {
const studio = await tmdb.getStudio(Number(req.params.id));
return res.status(200).json(studio);
return res.status(200).json(mapProductionCompany(studio));
});
router.get<{ id: string }>('/network/:id', async (req, res) => {
@ -87,7 +89,7 @@ router.get<{ id: string }>('/network/:id', async (req, res) => {
const network = await tmdb.getNetwork(Number(req.params.id));
return res.status(200).json(network);
return res.status(200).json(mapNetwork(network));
});
router.get('/genres/movie', isAuthenticated(), async (req, res) => {

@ -0,0 +1,62 @@
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({
genreMovies: '{genre} Movies',
});
const DiscoverMovieGenre: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
firstResultData,
} = useDiscover<MovieResult, { genre: { id: number; name: string } }>(
`/api/v1/discover/movies/genre/${router.query.genreId}`
);
if (error) {
return <Error statusCode={500} />;
}
const title = isLoadingInitialData
? intl.formatMessage(globalMessages.loading)
: intl.formatMessage(messages.genreMovies, {
genre: firstResultData?.genre.name,
});
return (
<>
<PageTitle title={title} />
<div className="mt-1 mb-5">
<Header>{title}</Header>
</div>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default DiscoverMovieGenre;

@ -1,18 +1,11 @@
import React, { useContext } from 'react';
import useSWR, { useSWRInfinite } from 'swr';
import React from 'react';
import type { MovieResult } from '../../../server/models/Search';
import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
import { useRouter } from 'next/router';
import {
TmdbStudio,
TmdbGenre,
} from '../../../server/api/themoviedb/interfaces';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
const messages = defineMessages({
discovermovies: 'Popular Movies',
@ -20,77 +13,24 @@ const messages = defineMessages({
studioMovies: '{studio} Movies',
});
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const DiscoverMovies: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data: genres } = useSWR<TmdbGenre[]>('/api/v1/genres/movie');
const genre = genres?.find((g) => g.id === Number(router.query.genreId));
const { data: studio } = useSWR<TmdbStudio>(
`/api/v1/studio/${router.query.studioId}`
);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/movies?page=${pageIndex + 1}&language=${locale}${
genre ? `&genre=${genre.id}` : ''
}${studio ? `&studio=${studio.id}` : ''}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
} = useDiscover<MovieResult>('/api/v1/discover/movies');
if (error) {
return <div>{error}</div>;
}
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as MovieResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
return <Error statusCode={500} />;
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
const title = genre
? intl.formatMessage(messages.genreMovies, { genre: genre.name })
: studio
? intl.formatMessage(messages.studioMovies, { studio: studio.name })
: intl.formatMessage(messages.discovermovies);
const title = intl.formatMessage(messages.discovermovies);
return (
<>

@ -0,0 +1,75 @@
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';
import { TvNetwork } from '../../../../server/models/common';
const messages = defineMessages({
networkSeries: '{network} Series',
});
const DiscoverTvNetwork: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
firstResultData,
} = useDiscover<TvResult, { network: TvNetwork }>(
`/api/v1/discover/tv/network/${router.query.networkId}`
);
if (error) {
return <Error statusCode={500} />;
}
const title = isLoadingInitialData
? intl.formatMessage(globalMessages.loading)
: intl.formatMessage(messages.networkSeries, {
network: firstResultData?.network.name,
});
return (
<>
<PageTitle title={title} />
<div className="mt-1 mb-5">
<Header>
{firstResultData?.network.logoPath ? (
<div className="flex justify-center mb-6">
<img
src={`//image.tmdb.org/t/p/w780_filter(negate,000,666)/${firstResultData?.network.logoPath}`}
alt=""
className="text-white max-h-24 sm:max-h-32"
/>
</div>
) : (
title
)}
</Header>
</div>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default DiscoverTvNetwork;

@ -0,0 +1,75 @@
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';
import { ProductionCompany } from '../../../../server/models/common';
const messages = defineMessages({
studioMovies: '{studio} Movies',
});
const DiscoverMovieStudio: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
firstResultData,
} = useDiscover<MovieResult, { studio: ProductionCompany }>(
`/api/v1/discover/movies/studio/${router.query.studioId}`
);
if (error) {
return <Error statusCode={500} />;
}
const title = isLoadingInitialData
? intl.formatMessage(globalMessages.loading)
: intl.formatMessage(messages.studioMovies, {
studio: firstResultData?.studio.name,
});
return (
<>
<PageTitle title={title} />
<div className="mt-1 mb-5">
<Header>
{firstResultData?.studio.logoPath ? (
<div className="flex justify-center mb-6">
<img
src={`//image.tmdb.org/t/p/w780_filter(negate,000,666)/${firstResultData?.studio.logoPath}`}
alt=""
className="text-white max-h-24 sm:max-h-32"
/>
</div>
) : (
title
)}
</Header>
</div>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default DiscoverMovieStudio;

@ -1,18 +1,11 @@
import React, { useContext } from 'react';
import useSWR, { useSWRInfinite } from 'swr';
import React from 'react';
import type { TvResult } from '../../../server/models/Search';
import ListView from '../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
import { useRouter } from 'next/router';
import {
TmdbGenre,
TmdbNetwork,
} from '../../../server/api/themoviedb/interfaces';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
const messages = defineMessages({
discovertv: 'Popular Series',
@ -20,76 +13,24 @@ const messages = defineMessages({
networkSeries: '{network} Series',
});
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: TvResult[];
}
const DiscoverTv: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data: genres } = useSWR<TmdbGenre[]>('/api/v1/genres/tv');
const genre = genres?.find((g) => g.id === Number(router.query.genreId));
const { data: network } = useSWR<TmdbNetwork>(
`/api/v1/network/${router.query.networkId}`
);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/tv?page=${pageIndex + 1}&language=${locale}${
genre ? `&genre=${genre.id}` : ''
}${network ? `&network=${network.id}` : ''}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
} = useDiscover<TvResult>('/api/v1/discover/tv');
if (error) {
return <div>{error}</div>;
}
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as TvResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
return <Error statusCode={500} />;
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
const title = genre
? intl.formatMessage(messages.genreSeries, { genre: genre.name })
: network
? intl.formatMessage(messages.networkSeries, { network: network.name })
: intl.formatMessage(messages.discovertv);
const title = intl.formatMessage(messages.discovertv);
return (
<>

@ -0,0 +1,62 @@
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({
genreSeries: '{genre} Series',
});
const DiscoverTvGenre: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
firstResultData,
} = useDiscover<TvResult, { genre: { id: number; name: string } }>(
`/api/v1/discover/tv/genre/${router.query.genreId}`
);
if (error) {
return <Error statusCode={500} />;
}
const title = isLoadingInitialData
? intl.formatMessage(globalMessages.loading)
: intl.formatMessage(messages.genreSeries, {
genre: firstResultData?.genre.name,
});
return (
<>
<PageTitle title={title} />
<div className="mt-1 mb-5">
<Header>{title}</Header>
</div>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default DiscoverTvGenre;

@ -1,74 +1,33 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import React from 'react';
import type { TvResult } from '../../../server/models/Search';
import ListView from '../Common/ListView';
import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
const messages = defineMessages({
upcomingtv: 'Upcoming Series',
});
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: TvResult[];
}
const DiscoverTvUpcoming: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/tv/upcoming?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
} = useDiscover<TvResult>('/api/v1/discover/tv/upcoming');
if (error) {
return <div>{error}</div>;
return <Error statusCode={500} />;
}
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as TvResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<PageTitle title={intl.formatMessage(messages.upcomingtv)} />

@ -1,79 +1,38 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import React from 'react';
import type {
MovieResult,
TvResult,
PersonResult,
} from '../../../server/models/Search';
import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
const messages = defineMessages({
trending: 'Trending',
});
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: (MovieResult | TvResult | PersonResult)[];
}
const Trending: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/trending?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
} = useDiscover<MovieResult | TvResult | PersonResult>(
'/api/v1/discover/trending'
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
if (error) {
return <div>{error}</div>;
return <Error statusCode={500} />;
}
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as (MovieResult | TvResult | PersonResult)[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<PageTitle title={intl.formatMessage(messages.trending)} />

@ -1,74 +1,33 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import React from 'react';
import type { MovieResult } from '../../../server/models/Search';
import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import useSettings from '../../hooks/useSettings';
import { MediaStatus } from '../../../server/constants/media';
import PageTitle from '../Common/PageTitle';
import useDiscover from '../../hooks/useDiscover';
import Error from '../../pages/_error';
const messages = defineMessages({
upcomingmovies: 'Upcoming Movies',
});
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const UpcomingMovies: React.FC = () => {
const intl = useIntl();
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/movies/upcoming?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
const {
isLoadingInitialData,
isEmpty,
isLoadingMore,
isReachingEnd,
titles,
fetchMore,
error,
} = useDiscover<MovieResult>('/api/v1/discover/movies/upcoming');
if (error) {
return <div>{error}</div>;
return <Error statusCode={500} />;
}
let titles = (data ?? []).reduce(
(a, v) => [...a, ...v.results],
[] as MovieResult[]
);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<PageTitle title={intl.formatMessage(messages.upcomingmovies)} />

@ -0,0 +1,99 @@
import { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import { MediaStatus } from '../../server/constants/media';
import { LanguageContext } from '../context/LanguageContext';
import useSettings from './useSettings';
export interface BaseSearchResult<T> {
page: number;
totalResults: number;
totalPages: number;
results: T[];
}
interface BaseMedia {
mediaType: string;
mediaInfo?: {
status: MediaStatus;
};
}
interface DiscoverResult<T, S> {
isLoadingInitialData: boolean;
isLoadingMore: boolean;
fetchMore: () => void;
isEmpty: boolean;
isReachingEnd: boolean;
error: unknown;
titles: T[];
firstResultData?: BaseSearchResult<T> & S;
}
const useDiscover = <T extends BaseMedia, S = Record<string, never>>(
endpoint: string,
options?: Record<string, unknown>
): DiscoverResult<T, S> => {
const settings = useSettings();
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<
BaseSearchResult<T> & S
>(
(pageIndex: number, previousPageData) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
const params: Record<string, unknown> = {
page: pageIndex + 1,
language: locale,
...options,
};
const finalQueryString = Object.keys(params)
.map((paramKey) => `${paramKey}=${params[paramKey]}`)
.join('&');
return `${endpoint}?${finalQueryString}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && !!data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
let titles = (data ?? []).reduce((a, v) => [...a, ...v.results], [] as T[]);
if (settings.currentSettings.hideAvailable) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
);
}
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (!!data && (data[data?.length - 1]?.results.length ?? 0) < 20);
return {
isLoadingInitialData,
isLoadingMore,
fetchMore,
isEmpty,
isReachingEnd,
error,
titles,
firstResultData: data?.[0],
};
};
export default useDiscover;

@ -23,6 +23,7 @@ const globalMessages = defineMessages({
edit: 'Edit',
experimental: 'Experimental',
advanced: 'Advanced',
loading: 'Loading…',
});
export default globalMessages;

@ -1,9 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import DiscoverMovies from '../../../../../components/Discover/DiscoverMovies';
import DiscoverMovieGenre from '../../../../../components/Discover/DiscoverMovieGenre';
const DiscoverMoviesGenrePage: NextPage = () => {
return <DiscoverMovies />;
return <DiscoverMovieGenre />;
};
export default DiscoverMoviesGenrePage;

@ -1,9 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import DiscoverMovies from '../../../../../components/Discover/DiscoverMovies';
import DiscoverMovieStudio from '../../../../../components/Discover/DiscoverStudio';
const DiscoverMoviesStudioPage: NextPage = () => {
return <DiscoverMovies />;
return <DiscoverMovieStudio />;
};
export default DiscoverMoviesStudioPage;

@ -1,9 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import DiscoverTv from '../../../../../components/Discover/DiscoverTv';
import DiscoverTvGenre from '../../../../../components/Discover/DiscoverTvGenre';
const DiscoverTvGenrePage: NextPage = () => {
return <DiscoverTv />;
return <DiscoverTvGenre />;
};
export default DiscoverTvGenrePage;

@ -1,9 +1,9 @@
import React from 'react';
import { NextPage } from 'next';
import DiscoverTv from '../../../../../components/Discover/DiscoverTv';
import DiscoverNetwork from '../../../../../components/Discover/DiscoverNetwork';
const DiscoverTvNetworkPage: NextPage = () => {
return <DiscoverTv />;
return <DiscoverNetwork />;
};
export default DiscoverTvNetworkPage;

Loading…
Cancel
Save