import React, { useState } from 'react'; import Modal from '../Common/Modal'; import { useUser } from '../../hooks/useUser'; import { Permission } from '../../../server/lib/permissions'; import { defineMessages, useIntl } from 'react-intl'; import { MediaRequest } from '../../../server/entity/MediaRequest'; import useSWR from 'swr'; import { useToasts } from 'react-toast-notifications'; import { ANIME_KEYWORD_ID } from '../../../server/api/themoviedb'; import axios from 'axios'; import { MediaStatus, MediaRequestStatus, } from '../../../server/constants/media'; import { TvDetails } from '../../../server/models/Tv'; import Badge from '../Common/Badge'; import globalMessages from '../../i18n/globalMessages'; import SeasonRequest from '../../../server/entity/SeasonRequest'; import Alert from '../Common/Alert'; import AdvancedRequester, { RequestOverrides } from './AdvancedRequester'; const messages = defineMessages({ requestadmin: 'Your request will be immediately approved.', cancelrequest: 'This will remove your request. Are you sure you want to continue?', requestSuccess: '{title} successfully requested!', requesttitle: 'Request {title}', request4ktitle: 'Request {title} in 4K', requesting: 'Requesting...', requestseasons: 'Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}}', selectseason: 'Select season(s)', season: 'Season', numberofepisodes: '# of Episodes', status: 'Status', seasonnumber: 'Season {number}', extras: 'Extras', notrequested: 'Not Requested', errorediting: 'Something went wrong editing the request.', requestedited: 'Request edited.', requestcancelled: 'Request cancelled.', }); interface RequestModalProps extends React.HTMLAttributes { tmdbId: number; onCancel?: () => void; onComplete?: (newStatus: MediaStatus) => void; onUpdating?: (isUpdating: boolean) => void; is4k?: boolean; editRequest?: MediaRequest; } const TvRequestModal: React.FC = ({ onCancel, onComplete, tmdbId, onUpdating, editRequest, is4k = false, }) => { const { addToast } = useToasts(); const editingSeasons: number[] = (editRequest?.seasons ?? []).map( (season) => season.seasonNumber ); const { data, error } = useSWR(`/api/v1/tv/${tmdbId}`); const [ requestOverrides, setRequestOverrides, ] = useState(null); const [selectedSeasons, setSelectedSeasons] = useState( editRequest ? editingSeasons : [] ); const intl = useIntl(); const { hasPermission } = useUser(); const updateRequest = async () => { if (!editRequest) { return; } if (onUpdating) { onUpdating(true); } try { if (selectedSeasons.length > 0) { await axios.put(`/api/v1/request/${editRequest.id}`, { mediaType: 'tv', serverId: requestOverrides?.server, profileId: requestOverrides?.profile, rootFolder: requestOverrides?.folder, seasons: selectedSeasons, }); } else { await axios.delete(`/api/v1/request/${editRequest.id}`); } addToast( {selectedSeasons.length > 0 ? intl.formatMessage(messages.requestedited) : intl.formatMessage(messages.requestcancelled)} , { appearance: 'success', autoDismiss: true, } ); if (onComplete) { onComplete(MediaStatus.PENDING); } } catch (e) { addToast({intl.formatMessage(messages.errorediting)}, { appearance: 'error', autoDismiss: true, }); } finally { if (onUpdating) { onUpdating(false); } } }; const sendRequest = async () => { if (selectedSeasons.length === 0) { return; } if (onUpdating) { onUpdating(true); } let overrideParams = {}; if (requestOverrides) { overrideParams = { serverId: requestOverrides.server, profileId: requestOverrides.profile, rootFolder: requestOverrides.folder, }; } const response = await axios.post('/api/v1/request', { mediaId: data?.id, tvdbId: data?.externalIds.tvdbId, mediaType: 'tv', is4k, seasons: selectedSeasons, ...overrideParams, }); if (response.data) { if (onComplete) { onComplete(response.data.media.status); } addToast( {intl.formatMessage(messages.requestSuccess, { title: data?.name, strong: function strong(msg) { return {msg}; }, })} , { appearance: 'success', autoDismiss: true } ); if (onUpdating) { onUpdating(false); } } }; const getAllRequestedSeasons = (): number[] => { const requestedSeasons = (data?.mediaInfo?.requests ?? []) .filter((request) => request.is4k === is4k) .reduce((requestedSeasons, request) => { return [ ...requestedSeasons, ...request.seasons .filter((season) => !editingSeasons.includes(season.seasonNumber)) .map((sr) => sr.seasonNumber), ]; }, [] as number[]); const availableSeasons = (data?.mediaInfo?.seasons ?? []) .filter( (season) => (season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE || season[is4k ? 'status4k' : 'status'] === MediaStatus.PARTIALLY_AVAILABLE) && !requestedSeasons.includes(season.seasonNumber) ) .map((season) => season.seasonNumber); return [...requestedSeasons, ...availableSeasons]; }; const isSelectedSeason = (seasonNumber: number): boolean => selectedSeasons.includes(seasonNumber); const toggleSeason = (seasonNumber: number): void => { // If this season already has a pending request, don't allow it to be toggled if (getAllRequestedSeasons().includes(seasonNumber)) { return; } if (selectedSeasons.includes(seasonNumber)) { setSelectedSeasons((seasons) => seasons.filter((sn) => sn !== seasonNumber) ); } else { setSelectedSeasons((seasons) => [...seasons, seasonNumber]); } }; const toggleAllSeasons = (): void => { if ( data && selectedSeasons.length >= 0 && selectedSeasons.length < data?.seasons .filter((season) => season.seasonNumber !== 0) .filter( (season) => !getAllRequestedSeasons().includes(season.seasonNumber) ).length ) { setSelectedSeasons( data.seasons .filter((season) => season.seasonNumber !== 0) .filter( (season) => !getAllRequestedSeasons().includes(season.seasonNumber) ) .map((season) => season.seasonNumber) ); } else { setSelectedSeasons([]); } }; const isAllSeasons = (): boolean => { if (!data) { return false; } return ( selectedSeasons.length === data.seasons .filter((season) => season.seasonNumber !== 0) .filter( (season) => !getAllRequestedSeasons().includes(season.seasonNumber) ).length ); }; const getSeasonRequest = ( seasonNumber: number ): SeasonRequest | undefined => { let seasonRequest: SeasonRequest | undefined; if ( data?.mediaInfo && (data.mediaInfo.requests || []).filter((request) => request.is4k === is4k) .length > 0 ) { data.mediaInfo.requests .filter((request) => request.is4k === is4k) .forEach((request) => { if (!seasonRequest) { seasonRequest = request.seasons.find( (season) => season.seasonNumber === seasonNumber ); } }); } return seasonRequest; }; return ( (editRequest ? updateRequest() : sendRequest())} title={intl.formatMessage( is4k ? messages.request4ktitle : messages.requesttitle, { title: data?.name } )} okText={ editRequest && selectedSeasons.length === 0 ? 'Cancel Request' : selectedSeasons.length === 0 ? intl.formatMessage(messages.selectseason) : intl.formatMessage(messages.requestseasons, { seasonCount: selectedSeasons.length, }) } okDisabled={editRequest ? false : selectedSeasons.length === 0} okButtonType={ editRequest && selectedSeasons.length === 0 ? 'danger' : `primary` } iconSvg={ } > {(hasPermission(Permission.MANAGE_REQUESTS) || hasPermission(Permission.AUTO_APPROVE) || hasPermission(Permission.AUTO_APPROVE_MOVIE)) && !editRequest && (

{intl.formatMessage(messages.requestadmin)}

)}
{data?.seasons .filter((season) => season.seasonNumber !== 0) .map((season) => { const seasonRequest = getSeasonRequest( season.seasonNumber ); const mediaSeason = data?.mediaInfo?.seasons.find( (sn) => sn.seasonNumber === season.seasonNumber && sn[is4k ? 'status4k' : 'status'] !== MediaStatus.UNKNOWN ); return ( ); })}
toggleAllSeasons()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === 'Space') { toggleAllSeasons(); } }} className="relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 cursor-pointer group focus:outline-none" > {intl.formatMessage(messages.season)} {intl.formatMessage(messages.numberofepisodes)} {intl.formatMessage(messages.status)}
toggleSeason(season.seasonNumber)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === 'Space') { toggleSeason(season.seasonNumber); } }} className={`group relative inline-flex items-center justify-center flex-shrink-0 h-5 w-10 cursor-pointer focus:outline-none ${ mediaSeason || (!!seasonRequest && !editingSeasons.includes(season.seasonNumber)) ? 'opacity-50' : '' }`} > {season.seasonNumber === 0 ? intl.formatMessage(messages.extras) : intl.formatMessage(messages.seasonnumber, { number: season.seasonNumber, })} {season.episodeCount} {!seasonRequest && !mediaSeason && ( {intl.formatMessage(messages.notrequested)} )} {!mediaSeason && seasonRequest?.status === MediaRequestStatus.PENDING && ( {intl.formatMessage(globalMessages.pending)} )} {!mediaSeason && seasonRequest?.status === MediaRequestStatus.APPROVED && ( {intl.formatMessage(globalMessages.requested)} )} {!mediaSeason && seasonRequest?.status === MediaRequestStatus.AVAILABLE && ( {intl.formatMessage(globalMessages.available)} )} {mediaSeason?.[is4k ? 'status4k' : 'status'] === MediaStatus.PARTIALLY_AVAILABLE && ( {intl.formatMessage( globalMessages.partiallyavailable )} )} {mediaSeason?.[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE && ( {intl.formatMessage(globalMessages.available)} )}
{hasPermission(Permission.REQUEST_ADVANCED) && (
keyword.id === ANIME_KEYWORD_ID )} onChange={(overrides) => setRequestOverrides(overrides)} defaultOverrides={ editRequest ? { folder: editRequest.rootFolder, profile: editRequest.profileId, server: editRequest.serverId, } : undefined } />
)}
); }; export default TvRequestModal;