import React, { useState, useContext, useMemo } from 'react'; import { FormattedMessage, FormattedDate, defineMessages, useIntl, } from 'react-intl'; import useSWR from 'swr'; import { useRouter } from 'next/router'; import Button from '../Common/Button'; import type { TvResult } from '../../../server/models/Search'; import Link from 'next/link'; import Slider from '../Slider'; import TitleCard from '../TitleCard'; import PersonCard from '../PersonCard'; import { LanguageContext } from '../../context/LanguageContext'; import LoadingSpinner from '../Common/LoadingSpinner'; import { useUser, Permission } from '../../hooks/useUser'; import { TvDetails as TvDetailsType } from '../../../server/models/Tv'; import { MediaStatus } from '../../../server/constants/media'; import RequestModal from '../RequestModal'; import ButtonWithDropdown from '../Common/ButtonWithDropdown'; import axios from 'axios'; import SlideOver from '../Common/SlideOver'; import RequestBlock from '../RequestBlock'; import Error from '../../pages/_error'; import TmdbLogo from '../../assets/tmdb_logo.svg'; import RTFresh from '../../assets/rt_fresh.svg'; import RTRotten from '../../assets/rt_rotten.svg'; import RTAudFresh from '../../assets/rt_aud_fresh.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import type { RTRating } from '../../../server/api/rottentomatoes'; import Head from 'next/head'; import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb'; import ExternalLinkBlock from '../ExternalLinkBlock'; import { sortCrewPriority } from '../../utils/creditHelpers'; import { Crew } from '../../../server/models/common'; import StatusBadge from '../StatusBadge'; const messages = defineMessages({ firstAirDate: 'First Air Date', userrating: 'User Rating', status: 'Status', originallanguage: 'Original Language', overview: 'Overview', cast: 'Cast', recommendations: 'Recommendations', similar: 'Similar Series', cancelrequest: 'Cancel Request', watchtrailer: 'Watch Trailer', available: 'Available', unavailable: 'Unavailable', request: 'Request', requestmore: 'Request More', pending: 'Pending', overviewunavailable: 'Overview unavailable', approverequests: 'Approve {requestCount} {requestCount, plural, one {Request} other {Requests}}', declinerequests: 'Decline {requestCount} {requestCount, plural, one {Request} other {Requests}}', manageModalTitle: 'Manage Series', manageModalRequests: 'Requests', manageModalNoRequests: 'No Requests', manageModalClearMedia: 'Clear All Media Data', manageModalClearMediaWarning: 'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.', approve: 'Approve', decline: 'Decline', showtype: 'Show Type', anime: 'Anime', network: 'Network', viewfullcrew: 'View Full Crew', }); interface TvDetailsProps { tv?: TvDetailsType; } interface SearchResult { page: number; totalResults: number; totalPages: number; results: TvResult[]; } enum MediaRequestStatus { PENDING = 1, APPROVED, DECLINED, AVAILABLE, } const TvDetails: React.FC = ({ tv }) => { const { hasPermission } = useUser(); const router = useRouter(); const intl = useIntl(); const { locale } = useContext(LanguageContext); const [showRequestModal, setShowRequestModal] = useState(false); const [showManager, setShowManager] = useState(false); const { data, error, revalidate } = useSWR( `/api/v1/tv/${router.query.tvId}?language=${locale}`, { initialData: tv, } ); const { data: recommended, error: recommendedError } = useSWR( `/api/v1/tv/${router.query.tvId}/recommendations?language=${locale}` ); const { data: similar, error: similarError } = useSWR( `/api/v1/tv/${router.query.tvId}/similar?language=${locale}` ); const { data: ratingData } = useSWR( `/api/v1/tv/${router.query.tvId}/ratings` ); const sortedCrew = useMemo(() => sortCrewPriority(data?.credits.crew ?? []), [ data, ]); if (!data && !error) { return ; } if (!data) { return ; } const activeRequests = data.mediaInfo?.requests?.filter( (request) => request.status === MediaRequestStatus.PENDING ); const trailerUrl = data.relatedVideos ?.filter((r) => r.type === 'Trailer') .sort((a, b) => a.size - b.size) .pop()?.url; const modifyRequests = async (type: 'approve' | 'decline'): Promise => { if (!activeRequests) { return; } await Promise.all( activeRequests.map(async (request) => { return axios.get(`/api/v1/request/${request.id}/${type}`); }) ); revalidate(); }; const deleteMedia = async () => { if (data?.mediaInfo?.id) { await axios.delete(`/api/v1/media/${data?.mediaInfo?.id}`); revalidate(); } }; const isComplete = data.seasons.filter((season) => season.seasonNumber !== 0).length <= ( data.mediaInfo?.seasons.filter( (season) => season.status === MediaStatus.AVAILABLE ) ?? [] ).length; return (
{data.name} - Overseerr { revalidate(); setShowRequestModal(false); }} onCancel={() => setShowRequestModal(false)} /> setShowManager(false)} subText={data.name} >

{intl.formatMessage(messages.manageModalRequests)}

    {data.mediaInfo?.requests?.map((request) => (
  • revalidate()} />
  • ))} {(data.mediaInfo?.requests ?? []).length === 0 && (
  • {intl.formatMessage(messages.manageModalNoRequests)}
  • )}
{data?.mediaInfo && (
{intl.formatMessage(messages.manageModalClearMediaWarning)}
)}

{data.name} {data.firstAirDate && ( ({data.firstAirDate.slice(0, 4)}) )}

{data.genres.map((g) => g.name).join(', ')}
{trailerUrl && ( )} {(!data.mediaInfo || data.mediaInfo.status === MediaStatus.UNKNOWN) && ( )} {data.mediaInfo && data.mediaInfo.status !== MediaStatus.UNKNOWN && !isComplete && ( } text={ <> } className="ml-2" onClick={() => setShowRequestModal(true)} > {hasPermission(Permission.MANAGE_REQUESTS) && activeRequests && activeRequests.length > 0 && ( <> modifyRequests('approve')} > modifyRequests('decline')} > )} )} {hasPermission(Permission.MANAGE_REQUESTS) && ( )}

{data.overview ? data.overview : intl.formatMessage(messages.overviewunavailable)}

    {(data.createdBy.length > 0 ? [ ...data.createdBy.map( (person): Partial => ({ id: person.id, job: 'Creator', name: person.name, }) ), ...sortedCrew, ] : sortedCrew ) .slice(0, 6) .map((person) => (
  • {person.job} {person.name}
  • ))}
{sortedCrew.length > 0 && ( )}
{(data.voteCount > 0 || ratingData) && (
{ratingData?.criticsRating && (ratingData?.criticsScore ?? 0) > 0 && ( <> {ratingData.criticsRating === 'Rotten' ? ( ) : ( )} {ratingData.criticsScore}% )} {ratingData?.audienceRating && (ratingData?.audienceScore ?? 0) > 0 && ( <> {ratingData.audienceRating === 'Spilled' ? ( ) : ( )} {ratingData.audienceScore}% )} {data.voteCount > 0 && ( <> {data.voteAverage}/10 )}
)} {data.keywords.some( (keyword) => keyword.id === ANIME_KEYWORD_ID ) && (
{intl.formatMessage(messages.showtype)} {intl.formatMessage(messages.anime)}
)} {data.firstAirDate && (
)}
{data.status}
{data.spokenLanguages.some( (lng) => lng.iso_639_1 === data.originalLanguage ) && (
{ data.spokenLanguages.find( (lng) => lng.iso_639_1 === data.originalLanguage )?.name }
)} {data.networks.length > 0 && (
{data.networks.map((n) => n.name).join(', ')}
)}
( ))} /> {(recommended?.results ?? []).length > 0 && ( <> ( ))} /> )} {(similar?.results ?? []).length > 0 && ( <> ( ))} /> )}
); }; export default TvDetails;