diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index 5006a004..fbf36e6b 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -17,6 +17,8 @@ export enum Permission { AUTO_APPROVE_4K = 32768, AUTO_APPROVE_4K_MOVIE = 65536, AUTO_APPROVE_4K_TV = 131072, + REQUEST_MOVIE = 262144, + REQUEST_TV = 524288, } export interface PermissionCheckOptions { diff --git a/server/routes/request.ts b/server/routes/request.ts index df0b5545..96f47694 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -139,287 +139,289 @@ requestRoutes.get('/', async (req, res, next) => { } }); -requestRoutes.post( - '/', - isAuthenticated(Permission.REQUEST), - async (req, res, next) => { - const tmdb = new TheMovieDb(); - const mediaRepository = getRepository(Media); - const requestRepository = getRepository(MediaRequest); - const userRepository = getRepository(User); +requestRoutes.post('/', async (req, res, next) => { + const tmdb = new TheMovieDb(); + const mediaRepository = getRepository(Media); + const requestRepository = getRepository(MediaRequest); + const userRepository = getRepository(User); - try { - let requestUser = req.user; + try { + let requestUser = req.user; - if ( - req.body.userId && - !req.user?.hasPermission([ - Permission.MANAGE_USERS, - Permission.MANAGE_REQUESTS, - ]) - ) { - return next({ - status: 403, - message: 'You do not have permission to modify the request user.', - }); - } else if (req.body.userId) { - requestUser = await userRepository.findOneOrFail({ - where: { id: req.body.userId }, - }); - } + if ( + req.body.userId && + !req.user?.hasPermission([ + Permission.MANAGE_USERS, + Permission.MANAGE_REQUESTS, + ]) + ) { + return next({ + status: 403, + message: 'You do not have permission to modify the request user.', + }); + } else if (req.body.userId) { + requestUser = await userRepository.findOneOrFail({ + where: { id: req.body.userId }, + }); + } - if (!requestUser) { - return next({ - status: 500, - message: 'User missing from request context.', - }); - } + if (!requestUser) { + return next({ + status: 500, + message: 'User missing from request context.', + }); + } - if (req.body.is4k) { - if ( - req.body.mediaType === MediaType.MOVIE && - !req.user?.hasPermission( - [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], - { - type: 'or', - } - ) - ) { - return next({ - status: 403, - message: 'You do not have permission to make 4K movie requests.', - }); - } else if ( - req.body.mediaType === MediaType.TV && - !req.user?.hasPermission( - [Permission.REQUEST_4K, Permission.REQUEST_4K_TV], - { - type: 'or', - } - ) - ) { - return next({ - status: 403, - message: 'You do not have permission to make 4K series requests.', - }); + if ( + req.body.mediaType === MediaType.MOVIE && + !req.user?.hasPermission( + req.body.is4k + ? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE] + : [Permission.REQUEST, Permission.REQUEST_MOVIE], + { + type: 'or', } - } + ) + ) { + return next({ + status: 403, + message: `You do not have permission to make ${ + req.body.is4k ? '4K ' : '' + }movie requests.`, + }); + } else if ( + req.body.mediaType === MediaType.TV && + !req.user?.hasPermission( + req.body.is4k + ? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV] + : [Permission.REQUEST, Permission.REQUEST_TV], + { + type: 'or', + } + ) + ) { + return next({ + status: 403, + message: `You do not have permission to make ${ + req.body.is4k ? '4K ' : '' + }series requests.`, + }); + } - const quotas = await requestUser.getQuota(); + const quotas = await requestUser.getQuota(); - if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) { - return next({ - status: 403, - message: 'Movie Quota Exceeded', - }); - } else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) { - return next({ - status: 403, - message: 'Series Quota Exceeded', - }); + if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) { + return next({ + status: 403, + message: 'Movie Quota Exceeded', + }); + } else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) { + return next({ + status: 403, + message: 'Series Quota Exceeded', + }); + } + + const tmdbMedia = + req.body.mediaType === MediaType.MOVIE + ? await tmdb.getMovie({ movieId: req.body.mediaId }) + : await tmdb.getTvShow({ tvId: req.body.mediaId }); + + let media = await mediaRepository.findOne({ + where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType }, + relations: ['requests'], + }); + + if (!media) { + media = new Media({ + tmdbId: tmdbMedia.id, + tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id, + status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, + status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, + mediaType: req.body.mediaType, + }); + } else { + if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) { + media.status = MediaStatus.PENDING; } - const tmdbMedia = - req.body.mediaType === MediaType.MOVIE - ? await tmdb.getMovie({ movieId: req.body.mediaId }) - : await tmdb.getTvShow({ tvId: req.body.mediaId }); + if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) { + media.status4k = MediaStatus.PENDING; + } + } - let media = await mediaRepository.findOne({ - where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType }, - relations: ['requests'], + if (req.body.mediaType === MediaType.MOVIE) { + const existing = await requestRepository.findOne({ + where: { + media: { + tmdbId: tmdbMedia.id, + }, + requestedBy: req.user, + is4k: req.body.is4k, + }, }); - if (!media) { - media = new Media({ + if (existing) { + logger.warn('Duplicate request for media blocked', { tmdbId: tmdbMedia.id, - tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id, - status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, - status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN, mediaType: req.body.mediaType, }); - } else { - if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) { - media.status = MediaStatus.PENDING; - } - - if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) { - media.status4k = MediaStatus.PENDING; - } - } - - if (req.body.mediaType === MediaType.MOVIE) { - const existing = await requestRepository.findOne({ - where: { - media: { - tmdbId: tmdbMedia.id, - }, - requestedBy: req.user, - is4k: req.body.is4k, - }, + return next({ + status: 409, + message: 'Request for this media already exists.', }); + } - if (existing) { - logger.warn('Duplicate request for media blocked', { - tmdbId: tmdbMedia.id, - mediaType: req.body.mediaType, - }); - return next({ - status: 409, - message: 'Request for this media already exists.', - }); - } + await mediaRepository.save(media); + + const request = new MediaRequest({ + type: MediaType.MOVIE, + media, + requestedBy: requestUser, + // If the user is an admin or has the "auto approve" permission, automatically approve the request + status: req.user?.hasPermission( + [ + req.body.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + req.body.is4k + ? Permission.AUTO_APPROVE_4K_MOVIE + : Permission.AUTO_APPROVE_MOVIE, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + modifiedBy: req.user?.hasPermission( + [ + req.body.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + req.body.is4k + ? Permission.AUTO_APPROVE_4K_MOVIE + : Permission.AUTO_APPROVE_MOVIE, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? req.user + : undefined, + is4k: req.body.is4k, + serverId: req.body.serverId, + profileId: req.body.profileId, + rootFolder: req.body.rootFolder, + tags: req.body.tags, + }); - await mediaRepository.save(media); - - const request = new MediaRequest({ - type: MediaType.MOVIE, - media, - requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_MOVIE - : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - modifiedBy: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_MOVIE - : Permission.AUTO_APPROVE_MOVIE, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } + await requestRepository.save(request); + return res.status(201).json(request); + } else if (req.body.mediaType === MediaType.TV) { + const requestedSeasons = req.body.seasons as number[]; + let existingSeasons: number[] = []; + + // We need to check existing requests on this title to make sure we don't double up on seasons that were + // already requested. In the case they were, we just throw out any duplicates but still approve the request. + // (Unless there are no seasons, in which case we abort) + if (media.requests) { + existingSeasons = media.requests + .filter( + (request) => + request.is4k === req.body.is4k && + request.status !== MediaRequestStatus.DECLINED ) - ? req.user - : undefined, - is4k: req.body.is4k, - serverId: req.body.serverId, - profileId: req.body.profileId, - rootFolder: req.body.rootFolder, - tags: req.body.tags, - }); - - await requestRepository.save(request); - return res.status(201).json(request); - } else if (req.body.mediaType === MediaType.TV) { - const requestedSeasons = req.body.seasons as number[]; - let existingSeasons: number[] = []; - - // We need to check existing requests on this title to make sure we don't double up on seasons that were - // already requested. In the case they were, we just throw out any duplicates but still approve the request. - // (Unless there are no seasons, in which case we abort) - if (media.requests) { - existingSeasons = media.requests - .filter( - (request) => - request.is4k === req.body.is4k && - request.status !== MediaRequestStatus.DECLINED - ) - .reduce((seasons, request) => { - const combinedSeasons = request.seasons.map( - (season) => season.seasonNumber - ); - - return [...seasons, ...combinedSeasons]; - }, [] as number[]); - } + .reduce((seasons, request) => { + const combinedSeasons = request.seasons.map( + (season) => season.seasonNumber + ); - const finalSeasons = requestedSeasons.filter( - (rs) => !existingSeasons.includes(rs) - ); + return [...seasons, ...combinedSeasons]; + }, [] as number[]); + } - if (finalSeasons.length === 0) { - return next({ - status: 202, - message: 'No seasons available to request', - }); - } + const finalSeasons = requestedSeasons.filter( + (rs) => !existingSeasons.includes(rs) + ); - await mediaRepository.save(media); - - const request = new MediaRequest({ - type: MediaType.TV, - media, - requestedBy: requestUser, - // If the user is an admin or has the "auto approve" permission, automatically approve the request - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - modifiedBy: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? req.user - : undefined, - is4k: req.body.is4k, - serverId: req.body.serverId, - profileId: req.body.profileId, - rootFolder: req.body.rootFolder, - languageProfileId: req.body.languageProfileId, - tags: req.body.tags, - seasons: finalSeasons.map( - (sn) => - new SeasonRequest({ - seasonNumber: sn, - status: req.user?.hasPermission( - [ - req.body.is4k - ? Permission.AUTO_APPROVE_4K - : Permission.AUTO_APPROVE, - req.body.is4k - ? Permission.AUTO_APPROVE_4K_TV - : Permission.AUTO_APPROVE_TV, - Permission.MANAGE_REQUESTS, - ], - { type: 'or' } - ) - ? MediaRequestStatus.APPROVED - : MediaRequestStatus.PENDING, - }) - ), + if (finalSeasons.length === 0) { + return next({ + status: 202, + message: 'No seasons available to request', }); - - await requestRepository.save(request); - return res.status(201).json(request); } - next({ status: 500, message: 'Invalid media type' }); - } catch (e) { - next({ status: 500, message: e.message }); + await mediaRepository.save(media); + + const request = new MediaRequest({ + type: MediaType.TV, + media, + requestedBy: requestUser, + // If the user is an admin or has the "auto approve" permission, automatically approve the request + status: req.user?.hasPermission( + [ + req.body.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + req.body.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + modifiedBy: req.user?.hasPermission( + [ + req.body.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + req.body.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? req.user + : undefined, + is4k: req.body.is4k, + serverId: req.body.serverId, + profileId: req.body.profileId, + rootFolder: req.body.rootFolder, + languageProfileId: req.body.languageProfileId, + tags: req.body.tags, + seasons: finalSeasons.map( + (sn) => + new SeasonRequest({ + seasonNumber: sn, + status: req.user?.hasPermission( + [ + req.body.is4k + ? Permission.AUTO_APPROVE_4K + : Permission.AUTO_APPROVE, + req.body.is4k + ? Permission.AUTO_APPROVE_4K_TV + : Permission.AUTO_APPROVE_TV, + Permission.MANAGE_REQUESTS, + ], + { type: 'or' } + ) + ? MediaRequestStatus.APPROVED + : MediaRequestStatus.PENDING, + }) + ), + }); + + await requestRepository.save(request); + return res.status(201).json(request); } + + next({ status: 500, message: 'Invalid media type' }); + } catch (e) { + next({ status: 500, message: e.message }); } -); +}); requestRoutes.get('/count', async (_req, res, next) => { const requestRepository = getRepository(MediaRequest); diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx index fd8fbaee..180bd2ed 100644 --- a/src/components/CollectionDetails/index.tsx +++ b/src/components/CollectionDetails/index.tsx @@ -108,11 +108,18 @@ const CollectionDetails: React.FC = ({ } const hasRequestable = + hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { + type: 'or', + }) && data.parts.filter( (part) => !part.mediaInfo || part.mediaInfo.status === MediaStatus.UNKNOWN ).length > 0; const hasRequestable4k = + settings.currentSettings.movie4kEnabled && + hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], { + type: 'or', + }) && data.parts.filter( (part) => !part.mediaInfo || part.mediaInfo.status4k === MediaStatus.UNKNOWN @@ -323,55 +330,42 @@ const CollectionDetails: React.FC = ({
- {hasPermission(Permission.REQUEST) && - (hasRequestable || - (settings.currentSettings.movie4kEnabled && - hasPermission( - [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], - { type: 'or' } - ) && - hasRequestable4k)) && ( - { - setRequestModal(true); - setIs4k(!hasRequestable); - }} - text={ - <> - - - {intl.formatMessage( - hasRequestable - ? messages.requestcollection - : messages.requestcollection4k - )} - - - } - > - {settings.currentSettings.movie4kEnabled && - hasPermission( - [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], - { type: 'or' } - ) && - hasRequestable && - hasRequestable4k && ( - { - setRequestModal(true); - setIs4k(true); - }} - > - - - {intl.formatMessage(messages.requestcollection4k)} - - - )} - - )} + {(hasRequestable || hasRequestable4k) && ( + { + setRequestModal(true); + setIs4k(!hasRequestable); + }} + text={ + <> + + + {intl.formatMessage( + hasRequestable + ? messages.requestcollection + : messages.requestcollection4k + )} + + + } + > + {hasRequestable && hasRequestable4k && ( + { + setRequestModal(true); + setIs4k(true); + }} + > + + + {intl.formatMessage(messages.requestcollection4k)} + + + )} + + )}
{data.overview && ( diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index 4b44bc3d..c0fa90f5 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -70,9 +70,9 @@ const messages = defineMessages({ openradarr4k: 'Open Movie in 4K Radarr', downloadstatus: 'Download Status', playonplex: 'Play on Plex', - play4konplex: 'Play 4K on Plex', + play4konplex: 'Play in 4K on Plex', markavailable: 'Mark as Available', - mark4kavailable: 'Mark 4K as Available', + mark4kavailable: 'Mark as Available in 4K', }); interface MovieDetailsProps { @@ -112,7 +112,12 @@ const MovieDetails: React.FC = ({ movie }) => { const mediaLinks: PlayButtonLink[] = []; - if (data.mediaInfo?.plexUrl) { + if ( + data.mediaInfo?.plexUrl && + hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], { + type: 'or', + }) + ) { mediaLinks.push({ text: intl.formatMessage(messages.playonplex), url: data.mediaInfo?.plexUrl, diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx index 5c0f60fe..71c6fc8b 100644 --- a/src/components/PermissionEdit/index.tsx +++ b/src/components/PermissionEdit/index.tsx @@ -12,42 +12,41 @@ export const messages = defineMessages({ 'Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.', settings: 'Manage Settings', settingsDescription: - 'Grant permission to modify all Overseerr settings. A user must have this permission to grant it to others.', + 'Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.', managerequests: 'Manage Requests', managerequestsDescription: - 'Grant permission to manage Overseerr requests (includes approving and denying requests). All requests made by a user with this permission will be automatically approved.', + 'Grant permission to manage Overseerr requests. All requests made by a user with this permission will be automatically approved.', request: 'Request', - requestDescription: 'Grant permission to request movies and series.', - vote: 'Vote', - voteDescription: - 'Grant permission to vote on requests (voting not yet implemented).', + requestDescription: 'Grant permission to request non-4K media.', + requestMovies: 'Request Movies', + requestMoviesDescription: 'Grant permission to request non-4K movies.', + requestTv: 'Request Series', + requestTvDescription: 'Grant permission to request non-4K series.', autoapprove: 'Auto-Approve', - autoapproveDescription: - 'Grant automatic approval for all non-4K requests made by this user.', + autoapproveDescription: 'Grant automatic approval for all non-4K requests.', autoapproveMovies: 'Auto-Approve Movies', autoapproveMoviesDescription: - 'Grant automatic approval for non-4K movie requests made by this user.', + 'Grant automatic approval for non-4K movie requests.', autoapproveSeries: 'Auto-Approve Series', autoapproveSeriesDescription: - 'Grant automatic approval for non-4K series requests made by this user.', + 'Grant automatic approval for non-4K series requests.', autoapprove4k: 'Auto-Approve 4K', - autoapprove4kDescription: - 'Grant automatic approval for all 4K requests made by this user.', + autoapprove4kDescription: 'Grant automatic approval for all 4K requests.', autoapprove4kMovies: 'Auto-Approve 4K Movies', autoapprove4kMoviesDescription: - 'Grant automatic approval for 4K movie requests made by this user.', + 'Grant automatic approval for 4K movie requests.', autoapprove4kSeries: 'Auto-Approve 4K Series', autoapprove4kSeriesDescription: - 'Grant automatic approval for 4K series requests made by this user.', + 'Grant automatic approval for 4K series requests.', request4k: 'Request 4K', - request4kDescription: 'Grant permission to request 4K movies and series.', + request4kDescription: 'Grant permission to request 4K media.', request4kMovies: 'Request 4K Movies', request4kMoviesDescription: 'Grant permission to request 4K movies.', request4kTv: 'Request 4K Series', - request4kTvDescription: 'Grant permission to request 4K Series.', + request4kTvDescription: 'Grant permission to request 4K series.', advancedrequest: 'Advanced Requests', advancedrequestDescription: - 'Grant permission to use advanced request options (e.g., changing servers, profiles, or paths).', + 'Grant permission to use advanced request options.', viewrequests: 'View Requests', viewrequestsDescription: "Grant permission to view other users' requests.", }); @@ -111,27 +110,18 @@ export const PermissionEdit: React.FC = ({ name: intl.formatMessage(messages.request), description: intl.formatMessage(messages.requestDescription), permission: Permission.REQUEST, - }, - { - id: 'request4k', - name: intl.formatMessage(messages.request4k), - description: intl.formatMessage(messages.request4kDescription), - permission: Permission.REQUEST_4K, - requires: [{ permissions: [Permission.REQUEST] }], children: [ { - id: 'request4k-movies', - name: intl.formatMessage(messages.request4kMovies), - description: intl.formatMessage(messages.request4kMoviesDescription), - permission: Permission.REQUEST_4K_MOVIE, - requires: [{ permissions: [Permission.REQUEST] }], + id: 'request-movies', + name: intl.formatMessage(messages.requestMovies), + description: intl.formatMessage(messages.requestMoviesDescription), + permission: Permission.REQUEST_MOVIE, }, { - id: 'request4k-tv', - name: intl.formatMessage(messages.request4kTv), - description: intl.formatMessage(messages.request4kTvDescription), - permission: Permission.REQUEST_4K_TV, - requires: [{ permissions: [Permission.REQUEST] }], + id: 'request-tv', + name: intl.formatMessage(messages.requestTv), + description: intl.formatMessage(messages.requestTvDescription), + permission: Permission.REQUEST_TV, }, ], }, @@ -149,7 +139,12 @@ export const PermissionEdit: React.FC = ({ messages.autoapproveMoviesDescription ), permission: Permission.AUTO_APPROVE_MOVIE, - requires: [{ permissions: [Permission.REQUEST] }], + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_MOVIE], + type: 'or', + }, + ], }, { id: 'autoapprovetv', @@ -158,7 +153,32 @@ export const PermissionEdit: React.FC = ({ messages.autoapproveSeriesDescription ), permission: Permission.AUTO_APPROVE_TV, - requires: [{ permissions: [Permission.REQUEST] }], + requires: [ + { + permissions: [Permission.REQUEST, Permission.REQUEST_TV], + type: 'or', + }, + ], + }, + ], + }, + { + id: 'request4k', + name: intl.formatMessage(messages.request4k), + description: intl.formatMessage(messages.request4kDescription), + permission: Permission.REQUEST_4K, + children: [ + { + id: 'request4k-movies', + name: intl.formatMessage(messages.request4kMovies), + description: intl.formatMessage(messages.request4kMoviesDescription), + permission: Permission.REQUEST_4K_MOVIE, + }, + { + id: 'request4k-tv', + name: intl.formatMessage(messages.request4kTv), + description: intl.formatMessage(messages.request4kTvDescription), + permission: Permission.REQUEST_4K_TV, }, ], }, @@ -169,8 +189,7 @@ export const PermissionEdit: React.FC = ({ permission: Permission.AUTO_APPROVE_4K, requires: [ { - permissions: [Permission.REQUEST, Permission.REQUEST_4K], - type: 'and', + permissions: [Permission.REQUEST_4K], }, ], children: [ @@ -182,9 +201,6 @@ export const PermissionEdit: React.FC = ({ ), permission: Permission.AUTO_APPROVE_4K_MOVIE, requires: [ - { - permissions: [Permission.REQUEST], - }, { permissions: [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE], type: 'or', @@ -199,9 +215,6 @@ export const PermissionEdit: React.FC = ({ ), permission: Permission.AUTO_APPROVE_4K_TV, requires: [ - { - permissions: [Permission.REQUEST], - }, { permissions: [Permission.REQUEST_4K, Permission.REQUEST_4K_TV], type: 'or', diff --git a/src/components/PermissionOption/index.tsx b/src/components/PermissionOption/index.tsx index 314c9944..35b0a655 100644 --- a/src/components/PermissionOption/index.tsx +++ b/src/components/PermissionOption/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { hasPermission } from '../../../server/lib/permissions'; -import { Permission, User } from '../../hooks/useUser'; import useSettings from '../../hooks/useSettings'; +import { Permission, User } from '../../hooks/useUser'; export interface PermissionItem { id: string; diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index dafae462..5ba5bf5d 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -123,7 +123,15 @@ const RequestButton: React.FC = ({ const buttons: ButtonOption[] = []; if ( (!media || media.status === MediaStatus.UNKNOWN) && - hasPermission(Permission.REQUEST) + hasPermission( + [ + Permission.REQUEST, + mediaType === 'movie' + ? Permission.REQUEST_MOVIE + : Permission.REQUEST_TV, + ], + { type: 'or' } + ) ) { buttons.push({ id: 'request', @@ -138,9 +146,15 @@ const RequestButton: React.FC = ({ if ( (!media || media.status4k === MediaStatus.UNKNOWN) && - (hasPermission(Permission.REQUEST_4K) || - (mediaType === 'movie' && hasPermission(Permission.REQUEST_4K_MOVIE)) || - (mediaType === 'tv' && hasPermission(Permission.REQUEST_4K_TV))) && + hasPermission( + [ + Permission.REQUEST_4K, + mediaType === 'movie' + ? Permission.REQUEST_4K_MOVIE + : Permission.REQUEST_4K_TV, + ], + { type: 'or' } + ) && ((settings.currentSettings.movie4kEnabled && mediaType === 'movie') || (settings.currentSettings.series4kEnabled && mediaType === 'tv')) ) { @@ -302,7 +316,9 @@ const RequestButton: React.FC = ({ if ( mediaType === 'tv' && (!activeRequest || activeRequest.requestedBy.id !== user?.id) && - hasPermission(Permission.REQUEST) && + hasPermission([Permission.REQUEST, Permission.REQUEST_TV], { + type: 'or', + }) && media && media.status !== MediaStatus.AVAILABLE && media.status !== MediaStatus.UNKNOWN && diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index a237dca2..a55a1725 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -69,6 +69,14 @@ const TitleCard: React.FC = ({ const closeModal = useCallback(() => setShowRequestModal(false), []); + const showRequestButton = hasPermission( + [ + Permission.REQUEST, + mediaType === 'movie' ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV, + ], + { type: 'or' } + ); + return (
= ({
= ({ className="text-xs whitespace-normal" style={{ WebkitLineClamp: - !hasPermission(Permission.REQUEST) || + !showRequestButton || (currentStatus && currentStatus !== MediaStatus.UNKNOWN) ? 5 @@ -228,7 +236,7 @@ const TitleCard: React.FC = ({
- {hasPermission(Permission.REQUEST) && + {showRequestButton && (!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (