diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 002eca0ef..0cb616a11 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -45,6 +45,7 @@ class Media { try { const media = await mediaRepository.findOne({ where: { tmdbId: id }, + relations: ['requests'], }); return media; @@ -75,7 +76,7 @@ class Media { @Column({ type: 'int', default: MediaStatus.UNKNOWN }) public status: MediaStatus; - @OneToMany(() => MediaRequest, (request) => request.media) + @OneToMany(() => MediaRequest, (request) => request.media, { cascade: true }) public requests: MediaRequest[]; @CreateDateColumn() diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index e35250a87..cb55ec9ad 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -27,7 +27,9 @@ export class MediaRequest { @Column({ type: 'integer' }) public status: MediaRequestStatus; - @ManyToOne(() => Media, (media) => media.requests, { eager: true }) + @ManyToOne(() => Media, (media) => media.requests, { + eager: true, + }) public media: Media; @ManyToOne(() => User, (user) => user.requests, { eager: true }) diff --git a/server/routes/request.ts b/server/routes/request.ts index 94f3b6251..9b5dcc805 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -108,7 +108,9 @@ requestRoutes.post( const request = new MediaRequest({ type: MediaType.TV, - media, + media: { + id: media.id, + } as Media, requestedBy: req.user, // If the user is an admin or has the "auto approve" permission, automatically approve the request status: req.user?.hasPermission(Permission.AUTO_APPROVE) diff --git a/src/components/Common/Badge/index.tsx b/src/components/Common/Badge/index.tsx new file mode 100644 index 000000000..3cd1453d1 --- /dev/null +++ b/src/components/Common/Badge/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +interface BadgeProps { + badgeType?: 'default' | 'primary' | 'danger' | 'warning' | 'success'; +} + +const Badge: React.FC = ({ badgeType = 'default', children }) => { + const badgeStyle = [ + 'px-2 inline-flex text-xs leading-5 font-semibold rounded-full', + ]; + + switch (badgeType) { + case 'danger': + badgeStyle.push('bg-red-600 text-red-100'); + break; + case 'warning': + badgeStyle.push('bg-orange-400 text-orange-100'); + break; + case 'success': + badgeStyle.push('bg-green-500 text-green-100'); + break; + default: + badgeStyle.push('bg-indigo-500 text-indigo-100'); + } + + return {children}; +}; + +export default Badge; diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 7bea8d5eb..44ee1af71 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -20,6 +20,7 @@ import LoadingSpinner from '../Common/LoadingSpinner'; import { useUser, Permission } from '../../hooks/useUser'; import { MediaStatus } from '../../../server/constants/media'; import RequestModal from '../RequestModal'; +import Badge from '../Common/Badge'; const messages = defineMessages({ releasedate: 'Release Date', @@ -98,7 +99,6 @@ const MovieDetails: React.FC = ({ movie }) => { tmdbId={data.id} show={showRequestModal} type="movie" - requestId={data.mediaInfo?.requests?.[0]?.id} onComplete={() => { revalidate(); setShowRequestModal(false); @@ -114,10 +114,21 @@ const MovieDetails: React.FC = ({ movie }) => { />
- - {data.releaseDate.slice(0, 4)} - -

{data.title}

+
+ {data.mediaInfo?.status === MediaStatus.AVAILABLE && ( + Available + )} + {data.mediaInfo?.status === MediaStatus.PROCESSING && ( + Unavailable + )} + {data.mediaInfo?.status === MediaStatus.PENDING && ( + Pending + )} +
+

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

{(data.runtime ?? 0) > 0 && ( <> diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index ace40931c..f76697786 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -7,8 +7,13 @@ import { MediaRequest } from '../../../server/entity/MediaRequest'; import useSWR from 'swr'; import { useToasts } from 'react-toast-notifications'; import axios from 'axios'; -import type { MediaStatus } from '../../../server/constants/media'; +import { + MediaStatus, + MediaRequestStatus, +} from '../../../server/constants/media'; import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv'; +import type SeasonRequest from '../../../server/entity/SeasonRequest'; +import Badge from '../Common/Badge'; const messages = defineMessages({ requestadmin: 'Your request will be immediately approved.', @@ -71,11 +76,24 @@ const TvRequestModal: React.FC = ({ } }; + const getAllRequestedSeasons = (): number[] => + (data?.mediaInfo?.requests ?? []).reduce((requestedSeasons, request) => { + return [ + ...requestedSeasons, + ...request.seasons.map((sr) => sr.seasonNumber), + ]; + }, [] as number[]); + const isSelectedSeason = (seasonNumber: number): boolean => { return 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) @@ -90,11 +108,18 @@ const TvRequestModal: React.FC = ({ data && selectedSeasons.length >= 0 && selectedSeasons.length < - data?.seasons.filter((season) => season.seasonNumber !== 0).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 { @@ -108,7 +133,11 @@ const TvRequestModal: React.FC = ({ } return ( selectedSeasons.length === - data.seasons.filter((season) => season.seasonNumber !== 0).length + data.seasons + .filter((season) => season.seasonNumber !== 0) + .filter( + (season) => !getAllRequestedSeasons().includes(season.seasonNumber) + ).length ); }; @@ -116,6 +145,23 @@ const TvRequestModal: React.FC = ({ ? intl.formatMessage(messages.requestadmin) : undefined; + const getSeasonRequest = ( + seasonNumber: number + ): SeasonRequest | undefined => { + let seasonRequest: SeasonRequest | undefined; + if (data?.mediaInfo && (data.mediaInfo.requests || []).length > 0) { + data.mediaInfo.requests.forEach((request) => { + if (!seasonRequest) { + seasonRequest = request.seasons.find( + (season) => season.seasonNumber === seasonNumber + ); + } + }); + } + + return seasonRequest; + }; + return ( = ({ {data?.seasons .filter((season) => season.seasonNumber !== 0) - .map((season) => ( - - - 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" - > + .map((season) => { + const seasonRequest = getSeasonRequest( + season.seasonNumber + ); + return ( + + - - - - - {season.seasonNumber === 0 - ? 'Extras' - : `Season ${season.seasonNumber}`} - - - {season.episodeCount} - - - - Available - - - - ))} + } + onClick={() => 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 ${ + seasonRequest ? 'opacity-50' : '' + }`} + > + + + + + + {season.seasonNumber === 0 + ? 'Extras' + : `Season ${season.seasonNumber}`} + + + {season.episodeCount} + + + {!seasonRequest && Not Requested} + {seasonRequest?.status === + MediaRequestStatus.PENDING && ( + Pending + )} + {seasonRequest?.status === + MediaRequestStatus.APPROVED && ( + Unavailable + )} + {seasonRequest?.status === + MediaRequestStatus.AVAILABLE && ( + Available + )} + + + ); + })}
diff --git a/src/components/RequestModal/index.tsx b/src/components/RequestModal/index.tsx index 3a118bd7e..6a417d9c3 100644 --- a/src/components/RequestModal/index.tsx +++ b/src/components/RequestModal/index.tsx @@ -6,7 +6,6 @@ import type { MediaStatus } from '../../../server/constants/media'; import TvRequestModal from './TvRequestModal'; interface RequestModalProps { - requestId?: number; show: boolean; type: 'movie' | 'tv'; tmdbId: number; @@ -18,7 +17,6 @@ interface RequestModalProps { const RequestModal: React.FC = ({ type, - requestId, show, tmdbId, onComplete, @@ -26,16 +24,12 @@ const RequestModal: React.FC = ({ onUpdating, onCancel, }) => { - const { data } = useSWR( - requestId ? `/api/v1/request/${requestId}` : null - ); if (type === 'tv') { return ( @@ -47,7 +41,6 @@ const RequestModal: React.FC = ({ onComplete={onComplete} onCancel={onCancel} visible={show} - request={data} tmdbId={tmdbId} onUpdating={onUpdating} /> diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 511821e96..76789870d 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -14,6 +14,7 @@ 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 Badge from '../Common/Badge'; const messages = defineMessages({ userrating: 'User Rating', @@ -88,7 +89,6 @@ const TvDetails: React.FC = ({ tv }) => { tmdbId={data.id} show={showRequestModal} type="tv" - requestId={data.mediaInfo?.requests?.[0]?.id} onComplete={() => { revalidate(); setShowRequestModal(false); @@ -104,17 +104,31 @@ const TvDetails: React.FC = ({ tv }) => { />
- - {data.firstAirDate.slice(0, 4)} - -

{data.name}

+
+ {data.mediaInfo?.status === MediaStatus.AVAILABLE && ( + Available + )} + {data.mediaInfo?.status === MediaStatus.PARTIALLY_AVAILABLE && ( + Partially Available + )} + {data.mediaInfo?.status === MediaStatus.PROCESSING && ( + Unavailable + )} + {data.mediaInfo?.status === MediaStatus.PENDING && ( + Pending + )} +
+

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

{data.genres.map((g) => g.name).join(', ')}
{(!data.mediaInfo || - data.mediaInfo.status === MediaStatus.UNKNOWN) && ( + data.mediaInfo.status !== MediaStatus.AVAILABLE) && ( )} - {data.mediaInfo?.status === MediaStatus.PENDING && ( - - )} - {data.mediaInfo?.status === MediaStatus.PROCESSING && ( - - )} - {data.mediaInfo?.status === MediaStatus.AVAILABLE && ( - - )}