diff --git a/overseerr-api.yml b/overseerr-api.yml index 3ff713f92..808051a16 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -1975,8 +1975,8 @@ paths: type: string example: job-name type: - type: string - enum: [process, command] + type: string + enum: [process, command] name: type: string example: A Job Name @@ -1984,8 +1984,8 @@ paths: type: string example: '2020-09-02T05:02:23.000Z' running: - type: boolean - example: false + type: boolean + example: false /settings/jobs/{jobId}/cancel: get: summary: Cancel a specific job @@ -2010,8 +2010,8 @@ paths: type: string example: job-name type: - type: string - enum: [process, command] + type: string + enum: [process, command] name: type: string example: A Job Name @@ -2019,8 +2019,8 @@ paths: type: string example: '2020-09-02T05:02:23.000Z' running: - type: boolean - example: false + type: boolean + example: false /settings/notifications: get: summary: Return notification settings @@ -3532,6 +3532,41 @@ paths: responses: '204': description: Succesfully removed media item + /media/{mediaId}/{status}: + get: + summary: Update media status + description: Updates a medias status and returns the media in JSON format + tags: + - media + parameters: + - in: path + name: mediaId + description: Media ID + required: true + example: 1 + schema: + type: string + - in: path + name: status + description: New status + required: true + example: available + schema: + type: string + enum: [available, partial, processing, pending, unknown] + - in: query + name: is4k + description: 4K Status + example: false + schema: + type: boolean + responses: + '200': + description: Returned media + content: + application/json: + schema: + $ref: '#/components/schemas/MediaInfo' /collection/{collectionId}: get: summary: Get collection details diff --git a/server/routes/media.ts b/server/routes/media.ts index 5d4fda63a..f6c6a505f 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import { getRepository, FindOperator, FindOneOptions, In } from 'typeorm'; import Media from '../entity/Media'; -import { MediaStatus } from '../constants/media'; +import { MediaStatus, MediaType } from '../constants/media'; import logger from '../logger'; import { isAuthenticated } from '../middleware/auth'; import { Permission } from '../lib/permissions'; @@ -82,6 +82,63 @@ mediaRoutes.get('/', async (req, res, next) => { } }); +mediaRoutes.get< + { + id: string; + status: 'available' | 'partial' | 'processing' | 'pending' | 'unknown'; + }, + Media +>( + '/:id/:status', + isAuthenticated(Permission.MANAGE_REQUESTS), + async (req, res, next) => { + const mediaRepository = getRepository(Media); + + const media = await mediaRepository.findOne({ + where: { id: Number(req.params.id) }, + }); + + if (!media) { + return next({ status: 404, message: 'Media does not exist.' }); + } + + const is4k = Boolean(req.query.is4k); + + switch (req.params.status) { + case 'available': + media[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE; + if (media.mediaType === MediaType.TV) { + // Mark all seasons available + media.seasons.forEach((season) => { + season[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE; + }); + } + break; + case 'partial': + if (media.mediaType === MediaType.MOVIE) { + return next({ + status: 400, + message: 'Only series can be set to be partially available', + }); + } + media.status = MediaStatus.PARTIALLY_AVAILABLE; + break; + case 'processing': + media.status = MediaStatus.PROCESSING; + break; + case 'pending': + media.status = MediaStatus.PENDING; + break; + case 'unknown': + media.status = MediaStatus.UNKNOWN; + } + + await mediaRepository.save(media); + + return res.status(200).json(media); + } +); + mediaRoutes.delete( '/:id', isAuthenticated(Permission.MANAGE_REQUESTS), diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index b3d298580..997e730ed 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -72,6 +72,8 @@ const messages = defineMessages({ downloadstatus: 'Download Status', playonplex: 'Play on Plex', play4konplex: 'Play 4K on Plex', + markavailable: 'Mark as Available', + mark4kavailable: 'Mark 4K as Available', }); interface MovieDetailsProps { @@ -118,6 +120,15 @@ const MovieDetails: React.FC = ({ movie }) => { } }; + const markAvailable = async (is4k = false) => { + await axios.get(`/api/v1/media/${data?.mediaInfo?.id}/available`, { + params: { + is4k, + }, + }); + revalidate(); + }; + return (
= ({ movie }) => {
)} + {data?.mediaInfo && + (data.mediaInfo.status !== MediaStatus.AVAILABLE || + data.mediaInfo.status4k !== MediaStatus.AVAILABLE) && ( +
+ {data?.mediaInfo && + data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( + + )} + {data?.mediaInfo && + data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && ( + + )} +
+ )}

{intl.formatMessage(messages.manageModalRequests)}

diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 9e3f452a2..c998e7b47 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -72,6 +72,9 @@ const messages = defineMessages({ downloadstatus: 'Download Status', playonplex: 'Play on Plex', play4konplex: 'Play 4K on Plex', + markavailable: 'Mark as Available', + mark4kavailable: 'Mark 4K as Available', + allseasonsmarkedavailable: '* All seasons will be marked as available.', }); interface TvDetailsProps { @@ -120,6 +123,15 @@ const TvDetails: React.FC = ({ tv }) => { } }; + const markAvailable = async (is4k = false) => { + await axios.get(`/api/v1/media/${data?.mediaInfo?.id}/available`, { + params: { + is4k, + }, + }); + revalidate(); + }; + const isComplete = data.seasons.filter((season) => season.seasonNumber !== 0).length <= ( @@ -183,6 +195,63 @@ const TvDetails: React.FC = ({ tv }) => { )} + {data?.mediaInfo && + (data.mediaInfo.status !== MediaStatus.AVAILABLE || + data.mediaInfo.status4k !== MediaStatus.AVAILABLE) && ( +
+
+ {data?.mediaInfo && + data?.mediaInfo.status !== MediaStatus.AVAILABLE && ( + + )} + {data?.mediaInfo && + data?.mediaInfo.status4k !== MediaStatus.AVAILABLE && ( + + )} +
+
+ {intl.formatMessage(messages.allseasonsmarkedavailable)} +
+
+ )}

{intl.formatMessage(messages.manageModalRequests)}

diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index e43b6e008..425d11951 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -53,6 +53,8 @@ "components.MovieDetails.manageModalNoRequests": "No Requests", "components.MovieDetails.manageModalRequests": "Requests", "components.MovieDetails.manageModalTitle": "Manage Movie", + "components.MovieDetails.mark4kavailable": "Mark 4K as Available", + "components.MovieDetails.markavailable": "Mark as Available", "components.MovieDetails.openradarr": "Open Movie in Radarr", "components.MovieDetails.openradarr4k": "Open Movie in 4K Radarr", "components.MovieDetails.originallanguage": "Original Language", @@ -494,6 +496,7 @@ "components.TitleCard.tvshow": "Series", "components.TvDetails.TvCast.fullseriescast": "Full Series Cast", "components.TvDetails.TvCrew.fullseriescrew": "Full Series Crew", + "components.TvDetails.allseasonsmarkedavailable": "* All seasons will be marked as available.", "components.TvDetails.anime": "Anime", "components.TvDetails.approve": "Approve", "components.TvDetails.areyousure": "Are you sure?", @@ -508,6 +511,8 @@ "components.TvDetails.manageModalNoRequests": "No Requests", "components.TvDetails.manageModalRequests": "Requests", "components.TvDetails.manageModalTitle": "Manage Series", + "components.TvDetails.mark4kavailable": "Mark 4K as Available", + "components.TvDetails.markavailable": "Mark as Available", "components.TvDetails.network": "Network", "components.TvDetails.opensonarr": "Open Series in Sonarr", "components.TvDetails.opensonarr4k": "Open Series in 4K Sonarr",