import { ArrowCircleRightIcon, CogIcon, FilmIcon, PlayIcon, } from '@heroicons/react/outline'; import { CheckCircleIcon, DocumentRemoveIcon, ExternalLinkIcon, } from '@heroicons/react/solid'; import axios from 'axios'; import Link from 'next/link'; import { useRouter } from 'next/router'; import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; import type { RTRating } from '../../../server/api/rottentomatoes'; import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb/constants'; import { MediaStatus } from '../../../server/constants/media'; import { Crew } from '../../../server/models/common'; import { TvDetails as TvDetailsType } from '../../../server/models/Tv'; import RTAudFresh from '../../assets/rt_aud_fresh.svg'; import RTAudRotten from '../../assets/rt_aud_rotten.svg'; import RTFresh from '../../assets/rt_fresh.svg'; import RTRotten from '../../assets/rt_rotten.svg'; import TmdbLogo from '../../assets/tmdb_logo.svg'; import useLocale from '../../hooks/useLocale'; import useSettings from '../../hooks/useSettings'; import { Permission, useUser } from '../../hooks/useUser'; import globalMessages from '../../i18n/globalMessages'; import Error from '../../pages/_error'; import { sortCrewPriority } from '../../utils/creditHelpers'; import Button from '../Common/Button'; import CachedImage from '../Common/CachedImage'; import ConfirmButton from '../Common/ConfirmButton'; import LoadingSpinner from '../Common/LoadingSpinner'; import PageTitle from '../Common/PageTitle'; import PlayButton, { PlayButtonLink } from '../Common/PlayButton'; import SlideOver from '../Common/SlideOver'; import DownloadBlock from '../DownloadBlock'; import ExternalLinkBlock from '../ExternalLinkBlock'; import MediaSlider from '../MediaSlider'; import PersonCard from '../PersonCard'; import RequestBlock from '../RequestBlock'; import RequestButton from '../RequestButton'; import RequestModal from '../RequestModal'; import Slider from '../Slider'; import StatusBadge from '../StatusBadge'; const messages = defineMessages({ firstAirDate: 'First Air Date', nextAirDate: 'Next Air Date', originallanguage: 'Original Language', overview: 'Overview', cast: 'Cast', recommendations: 'Recommendations', similar: 'Similar Series', watchtrailer: 'Watch Trailer', overviewunavailable: 'Overview unavailable.', manageModalTitle: 'Manage Series', manageModalRequests: 'Requests', manageModalNoRequests: 'No requests.', manageModalClearMedia: 'Clear Media Data', manageModalClearMediaWarning: '* This will irreversibly remove all data for this series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.', originaltitle: 'Original Title', showtype: 'Series Type', anime: 'Anime', network: '{networkCount, plural, one {Network} other {Networks}}', viewfullcrew: 'View Full Crew', opensonarr: 'Open Series in Sonarr', opensonarr4k: 'Open Series in 4K Sonarr', downloadstatus: 'Download Status', playonplex: 'Play on Plex', play4konplex: 'Play in 4K on Plex', markavailable: 'Mark as Available', mark4kavailable: 'Mark as Available in 4K', allseasonsmarkedavailable: '* All seasons will be marked as available.', seasons: '{seasonCount, plural, one {# Season} other {# Seasons}}', episodeRuntime: 'Episode Runtime', episodeRuntimeMinutes: '{runtime} minutes', }); interface TvDetailsProps { tv?: TvDetailsType; } const TvDetails: React.FC = ({ tv }) => { const settings = useSettings(); const { user, hasPermission } = useUser(); const router = useRouter(); const intl = useIntl(); const { locale } = useLocale(); const [showRequestModal, setShowRequestModal] = useState(false); const [showManager, setShowManager] = useState(false); const { data, error, revalidate } = useSWR( `/api/v1/tv/${router.query.tvId}`, { initialData: tv, } ); 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 mediaLinks: PlayButtonLink[] = []; if (data.mediaInfo?.plexUrl) { mediaLinks.push({ text: intl.formatMessage(messages.playonplex), url: data.mediaInfo?.plexUrl, svg: , }); } if ( data.mediaInfo?.plexUrl4k && hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { type: 'or', }) ) { mediaLinks.push({ text: intl.formatMessage(messages.play4konplex), url: data.mediaInfo?.plexUrl4k, svg: , }); } const trailerUrl = data.relatedVideos ?.filter((r) => r.type === 'Trailer') .sort((a, b) => a.size - b.size) .pop()?.url; if (trailerUrl) { mediaLinks.push({ text: intl.formatMessage(messages.watchtrailer), url: trailerUrl, svg: , }); } const deleteMedia = async () => { if (data?.mediaInfo?.id) { await axios.delete(`/api/v1/media/${data?.mediaInfo?.id}`); revalidate(); } }; const markAvailable = async (is4k = false) => { await axios.post(`/api/v1/media/${data?.mediaInfo?.id}/available`, { is4k, }); revalidate(); }; const region = user?.settings?.region ? user.settings.region : settings.currentSettings.region ? settings.currentSettings.region : 'US'; const seriesAttributes: React.ReactNode[] = []; if ( data.contentRatings.results.length && data.contentRatings.results.find( (r) => r.iso_3166_1 === region || data.contentRatings.results[0].rating ) ) { seriesAttributes.push( {data.contentRatings.results.find((r) => r.iso_3166_1 === region) ?.rating || data.contentRatings.results[0].rating} ); } const seasonCount = data.seasons.filter( (season) => season.seasonNumber !== 0 ).length; if (seasonCount) { seriesAttributes.push( intl.formatMessage(messages.seasons, { seasonCount: seasonCount }) ); } if (data.genres.length) { seriesAttributes.push( data.genres .map((g) => ( {g.name} )) .reduce((prev, curr) => ( <> {intl.formatMessage(globalMessages.delimitedlist, { a: prev, b: curr, })} )) ); } const isComplete = seasonCount <= ( data.mediaInfo?.seasons.filter( (season) => season.status === MediaStatus.AVAILABLE ) ?? [] ).length; const is4kComplete = seasonCount <= ( data.mediaInfo?.seasons.filter( (season) => season.status4k === MediaStatus.AVAILABLE ) ?? [] ).length; return (
{data.backdropPath && (
)} { revalidate(); setShowRequestModal(false); }} onCancel={() => setShowRequestModal(false)} /> setShowManager(false)} subText={data.name} > {((data?.mediaInfo?.downloadStatus ?? []).length > 0 || (data?.mediaInfo?.downloadStatus4k ?? []).length > 0) && ( <>

{intl.formatMessage(messages.downloadstatus)}

    {data.mediaInfo?.downloadStatus?.map((status, index) => (
  • ))} {data.mediaInfo?.downloadStatus4k?.map((status, index) => (
  • ))}
)} {data?.mediaInfo && (data.mediaInfo.status !== MediaStatus.AVAILABLE || (data.mediaInfo.status4k !== MediaStatus.AVAILABLE && settings.currentSettings.series4kEnabled)) && (
{data?.mediaInfo && data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
)} {data?.mediaInfo && data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && settings.currentSettings.series4kEnabled && (
)}
{intl.formatMessage(messages.allseasonsmarkedavailable)}
)}

{intl.formatMessage(messages.manageModalRequests)}

    {data.mediaInfo?.requests?.map((request) => (
  • revalidate()} />
  • ))} {(data.mediaInfo?.requests ?? []).length === 0 && (
  • {intl.formatMessage(messages.manageModalNoRequests)}
  • )}
{(data?.mediaInfo?.serviceUrl || data?.mediaInfo?.serviceUrl4k) && (
{data?.mediaInfo?.serviceUrl && ( )} {data?.mediaInfo?.serviceUrl4k && ( )}
)} {data?.mediaInfo && (
deleteMedia()} confirmText={intl.formatMessage(globalMessages.areyousure)} className="w-full" > {intl.formatMessage(messages.manageModalClearMedia)}
{intl.formatMessage(messages.manageModalClearMediaWarning)}
)}
0} plexUrl={data.mediaInfo?.plexUrl} /> {settings.currentSettings.series4kEnabled && hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], { type: 'or', }) && ( 0 } plexUrl4k={data.mediaInfo?.plexUrl4k} /> )}

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

{seriesAttributes.length > 0 && seriesAttributes .map((t, k) => {t}) .reduce((prev, curr) => ( <> {prev} | {curr} ))}
revalidate()} tmdbId={data?.id} media={data?.mediaInfo} isShowComplete={isComplete} is4kShowComplete={is4kComplete} /> {hasPermission(Permission.MANAGE_REQUESTS) && ( )}
{data.tagline &&
{data.tagline}
}

{intl.formatMessage(messages.overview)}

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

{sortedCrew.length > 0 && ( <>
    {(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}
  • ))}
)}
{(!!data.voteCount || (ratingData?.criticsRating && !!ratingData?.criticsScore) || (ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
{ratingData?.criticsRating && !!ratingData?.criticsScore && ( {ratingData.criticsRating === 'Rotten' ? ( ) : ( )} {ratingData.criticsScore}% )} {ratingData?.audienceRating && !!ratingData?.audienceScore && ( {ratingData.audienceRating === 'Spilled' ? ( ) : ( )} {ratingData.audienceScore}% )} {!!data.voteCount && ( {data.voteAverage}/10 )}
)} {data.originalName && data.originalLanguage !== locale.slice(0, 2) && (
{intl.formatMessage(messages.originaltitle)} {data.originalName}
)} {data.keywords.some( (keyword) => keyword.id === ANIME_KEYWORD_ID ) && (
{intl.formatMessage(messages.showtype)} {intl.formatMessage(messages.anime)}
)}
{intl.formatMessage(globalMessages.status)} {data.status}
{data.firstAirDate && (
{intl.formatMessage(messages.firstAirDate)} {intl.formatDate(data.firstAirDate, { year: 'numeric', month: 'long', day: 'numeric', })}
)} {data.nextEpisodeToAir && (
{intl.formatMessage(messages.nextAirDate)} {intl.formatDate(data.nextEpisodeToAir.airDate, { year: 'numeric', month: 'long', day: 'numeric', })}
)} {data.episodeRunTime.length > 0 && (
{intl.formatMessage(messages.episodeRuntime)} {intl.formatMessage(messages.episodeRuntimeMinutes, { runtime: data.episodeRunTime[0], })}
)} {data.originalLanguage && ( )} {data.networks.length > 0 && (
{intl.formatMessage(messages.network, { networkCount: data.networks.length, })} {data.networks .map((n) => ( {n.name} )) .reduce((prev, curr) => ( <> {prev}, {curr} ))}
)}
{data.credits.cast.length > 0 && ( <> ( ))} /> )}
); }; export default TvDetails;