import RTAudFresh from '@app/assets/rt_aud_fresh.svg'; import RTAudRotten from '@app/assets/rt_aud_rotten.svg'; import RTFresh from '@app/assets/rt_fresh.svg'; import RTRotten from '@app/assets/rt_rotten.svg'; import TmdbLogo from '@app/assets/tmdb_logo.svg'; import Button from '@app/components/Common/Button'; import CachedImage from '@app/components/Common/CachedImage'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import type { PlayButtonLink } from '@app/components/Common/PlayButton'; import PlayButton from '@app/components/Common/PlayButton'; import Tag from '@app/components/Common/Tag'; import Tooltip from '@app/components/Common/Tooltip'; import ExternalLinkBlock from '@app/components/ExternalLinkBlock'; import IssueModal from '@app/components/IssueModal'; import ManageSlideOver from '@app/components/ManageSlideOver'; import MediaSlider from '@app/components/MediaSlider'; import PersonCard from '@app/components/PersonCard'; import RequestButton from '@app/components/RequestButton'; import Slider from '@app/components/Slider'; import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import { sortCrewPriority } from '@app/utils/creditHelpers'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { ArrowRightCircleIcon, CloudIcon, CogIcon, ExclamationTriangleIcon, FilmIcon, PlayIcon, TicketIcon, } from '@heroicons/react/24/outline'; import { ChevronDoubleDownIcon, ChevronDoubleUpIcon, } from '@heroicons/react/24/solid'; import type { RTRating } from '@server/api/rottentomatoes'; import { IssueStatus } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import { hasFlag } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; const messages = defineMessages({ originaltitle: 'Original Title', releasedate: '{releaseCount, plural, one {Release Date} other {Release Dates}}', revenue: 'Revenue', budget: 'Budget', watchtrailer: 'Watch Trailer', originallanguage: 'Original Language', overview: 'Overview', runtime: '{minutes} minutes', cast: 'Cast', recommendations: 'Recommendations', similar: 'Similar Titles', overviewunavailable: 'Overview unavailable.', studio: '{studioCount, plural, one {Studio} other {Studios}}', viewfullcrew: 'View Full Crew', playonplex: 'Play on Plex', play4konplex: 'Play in 4K on Plex', markavailable: 'Mark as Available', mark4kavailable: 'Mark as Available in 4K', showmore: 'Show More', showless: 'Show Less', streamingproviders: 'Currently Streaming On', productioncountries: 'Production {countryCount, plural, one {Country} other {Countries}}', theatricalrelease: 'Theatrical Release', digitalrelease: 'Digital Release', physicalrelease: 'Physical Release', reportissue: 'Report an Issue', managemovie: 'Manage Movie', rtcriticsscore: 'Rotten Tomatoes Tomatometer', rtaudiencescore: 'Rotten Tomatoes Audience Score', tmdbuserscore: 'TMDB User Score', }); interface MovieDetailsProps { movie?: MovieDetailsType; } const MovieDetails = ({ movie }: MovieDetailsProps) => { const settings = useSettings(); const { user, hasPermission } = useUser(); const router = useRouter(); const intl = useIntl(); const { locale } = useLocale(); const [showManager, setShowManager] = useState( router.query.manage == '1' ? true : false ); const minStudios = 3; const [showMoreStudios, setShowMoreStudios] = useState(false); const [showIssueModal, setShowIssueModal] = useState(false); const { data, error, mutate: revalidate, } = useSWR(`/api/v1/movie/${router.query.movieId}`, { fallbackData: movie, refreshInterval: refreshIntervalHelper( { downloadStatus: movie?.mediaInfo?.downloadStatus, downloadStatus4k: movie?.mediaInfo?.downloadStatus4k, }, 15000 ), }); const { data: ratingData } = useSWR( `/api/v1/movie/${router.query.movieId}/ratings` ); const sortedCrew = useMemo( () => sortCrewPriority(data?.credits.crew ?? []), [data] ); useEffect(() => { setShowManager(router.query.manage == '1' ? true : false); }, [router.query.manage]); const { plexUrl, plexUrl4k } = useDeepLinks({ plexUrl: data?.mediaInfo?.plexUrl, plexUrl4k: data?.mediaInfo?.plexUrl4k, iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl, iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k, }); if (!data && !error) { return ; } if (!data) { return ; } const showAllStudios = data.productionCompanies.length <= minStudios + 1; const mediaLinks: PlayButtonLink[] = []; if ( plexUrl && hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { type: 'or', }) ) { mediaLinks.push({ text: intl.formatMessage(messages.playonplex), url: plexUrl, svg: , }); } if ( settings.currentSettings.movie4kEnabled && plexUrl4k && hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], { type: 'or', }) ) { mediaLinks.push({ text: intl.formatMessage(messages.play4konplex), url: 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 region = user?.settings?.region ? user.settings.region : settings.currentSettings.region ? settings.currentSettings.region : 'US'; const releases = data.releases.results.find( (r) => r.iso_3166_1 === region )?.release_dates; // Release date types: // 1. Premiere // 2. Theatrical (limited) // 3. Theatrical // 4. Digital // 5. Physical // 6. TV const filteredReleases = uniqBy( releases?.filter((r) => r.type > 2 && r.type < 6), 'type' ); const movieAttributes: React.ReactNode[] = []; const certification = releases?.find((r) => r.certification)?.certification; if (certification) { movieAttributes.push( {certification} ); } if (data.runtime) { movieAttributes.push( intl.formatMessage(messages.runtime, { minutes: data.runtime }) ); } if (data.genres.length) { movieAttributes.push( data.genres .map((g) => ( {g.name} )) .reduce((prev, curr) => ( <> {intl.formatMessage(globalMessages.delimitedlist, { a: prev, b: curr, })} )) ); } const streamingProviders = data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) ?.flatrate ?? []; return (
{data.backdropPath && (
)} setShowIssueModal(false)} show={showIssueModal} mediaType="movie" tmdbId={data.id} /> { setShowManager(false); router.push({ pathname: router.pathname, query: { movieId: router.query.movieId }, }); }} revalidate={() => revalidate()} show={showManager} />
0} tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" plexUrl={plexUrl} serviceUrl={data.mediaInfo?.serviceUrl} /> {settings.currentSettings.movie4kEnabled && hasPermission( [ Permission.MANAGE_REQUESTS, Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE, ], { type: 'or', } ) && ( 0 } tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" plexUrl={plexUrl4k} serviceUrl={data.mediaInfo?.serviceUrl4k} /> )}

{data.title}{' '} {data.releaseDate && ( ({data.releaseDate.slice(0, 4)}) )}

{movieAttributes.length > 0 && movieAttributes .map((t, k) => {t}) .reduce((prev, curr) => ( <> {prev} | {curr} ))}
revalidate()} /> {(data.mediaInfo?.status === MediaStatus.AVAILABLE || (settings.currentSettings.movie4kEnabled && hasPermission( [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], { type: 'or', } ) && data.mediaInfo?.status4k === MediaStatus.AVAILABLE)) && hasPermission( [Permission.CREATE_ISSUES, Permission.MANAGE_ISSUES], { type: 'or', } ) && ( )} {hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && ( )}
{data.tagline &&
{data.tagline}
}

{intl.formatMessage(messages.overview)}

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

{sortedCrew.length > 0 && ( <>
    {sortedCrew.slice(0, 6).map((person) => (
  • {person.job} {person.name}
  • ))}
)} {data.keywords.length > 0 && (
{data.keywords.map((keyword) => ( {keyword.name} ))}
)}
{data.collection && (
)}
{(!!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 && ( {Math.round(data.voteAverage * 10)}% )}
)} {data.originalTitle && data.originalLanguage !== locale.slice(0, 2) && (
{intl.formatMessage(messages.originaltitle)} {data.originalTitle}
)}
{intl.formatMessage(globalMessages.status)} {data.status}
{filteredReleases && filteredReleases.length > 0 ? (
{intl.formatMessage(messages.releasedate, { releaseCount: filteredReleases.length, })} {filteredReleases.map((r, i) => ( {r.type === 3 ? ( // Theatrical ) : r.type === 4 ? ( // Digital ) : ( // Physical )} {intl.formatDate(r.release_date, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC', })} ))}
) : ( data.releaseDate && (
{intl.formatMessage(messages.releasedate, { releaseCount: 1, })} {intl.formatDate(data.releaseDate, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC', })}
) )} {data.revenue > 0 && (
{intl.formatMessage(messages.revenue)} {intl.formatNumber(data.revenue, { currency: 'USD', style: 'currency', })}
)} {data.budget > 0 && (
{intl.formatMessage(messages.budget)} {intl.formatNumber(data.budget, { currency: 'USD', style: 'currency', })}
)} {data.originalLanguage && ( )} {data.productionCountries.length > 0 && (
{intl.formatMessage(messages.productioncountries, { countryCount: data.productionCountries.length, })} {data.productionCountries.map((c) => { return ( {hasFlag(c.iso_3166_1) && ( )} {intl.formatDisplayName(c.iso_3166_1, { type: 'region', fallback: 'none', }) ?? c.name} ); })}
)} {data.productionCompanies.length > 0 && (
{intl.formatMessage(messages.studio, { studioCount: data.productionCompanies.length, })} {data.productionCompanies .slice( 0, showAllStudios || showMoreStudios ? data.productionCompanies.length : minStudios ) .map((s) => { return ( {s.name} ); })} {!showAllStudios && ( )}
)} {!!streamingProviders.length && (
{intl.formatMessage(messages.streamingproviders)} {streamingProviders.map((p) => { return ( {p.name} ); })}
)}
{data.credits.cast.length > 0 && ( <> ( ))} /> )}
); }; export default MovieDetails;