feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions (#1474)

* feat(perms): add separate REQUEST_MOVIE and REQUEST_TV permissions

* fix(perms): do not require regular request perms for 4K requests
pull/1510/head
TheCatLady 3 years ago committed by GitHub
parent ed99e4976d
commit 91b9e0f679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 {

@ -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);

@ -108,11 +108,18 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}
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<CollectionDetailsProps> = ({
</span>
</div>
<div className="media-actions">
{hasPermission(Permission.REQUEST) &&
(hasRequestable ||
(settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable4k)) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<DownloadIcon />
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{settings.currentSettings.movie4kEnabled &&
hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) &&
hasRequestable &&
hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<DownloadIcon />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
{(hasRequestable || hasRequestable4k) && (
<ButtonWithDropdown
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(!hasRequestable);
}}
text={
<>
<DownloadIcon />
<span>
{intl.formatMessage(
hasRequestable
? messages.requestcollection
: messages.requestcollection4k
)}
</span>
</>
}
>
{hasRequestable && hasRequestable4k && (
<ButtonWithDropdown.Item
buttonType="primary"
onClick={() => {
setRequestModal(true);
setIs4k(true);
}}
>
<DownloadIcon />
<span>
{intl.formatMessage(messages.requestcollection4k)}
</span>
</ButtonWithDropdown.Item>
)}
</ButtonWithDropdown>
)}
</div>
</div>
{data.overview && (

@ -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<MovieDetailsProps> = ({ 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,

@ -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<PermissionEditProps> = ({
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<PermissionEditProps> = ({
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<PermissionEditProps> = ({
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<PermissionEditProps> = ({
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<PermissionEditProps> = ({
),
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<PermissionEditProps> = ({
),
permission: Permission.AUTO_APPROVE_4K_TV,
requires: [
{
permissions: [Permission.REQUEST],
},
{
permissions: [Permission.REQUEST_4K, Permission.REQUEST_4K_TV],
type: 'or',

@ -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;

@ -123,7 +123,15 @@ const RequestButton: React.FC<RequestButtonProps> = ({
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<RequestButtonProps> = ({
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<RequestButtonProps> = ({
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 &&

@ -69,6 +69,14 @@ const TitleCard: React.FC<TitleCardProps> = ({
const closeModal = useCallback(() => setShowRequestModal(false), []);
const showRequestButton = hasPermission(
[
Permission.REQUEST,
mediaType === 'movie' ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV,
],
{ type: 'or' }
);
return (
<div className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}>
<RequestModal
@ -185,7 +193,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
<div className="flex items-end w-full h-full">
<div
className={`px-2 text-white ${
!hasPermission(Permission.REQUEST) ||
!showRequestButton ||
(currentStatus && currentStatus !== MediaStatus.UNKNOWN)
? 'pb-2'
: 'pb-11'
@ -209,7 +217,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
className="text-xs whitespace-normal"
style={{
WebkitLineClamp:
!hasPermission(Permission.REQUEST) ||
!showRequestButton ||
(currentStatus &&
currentStatus !== MediaStatus.UNKNOWN)
? 5
@ -228,7 +236,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
</Link>
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
{hasPermission(Permission.REQUEST) &&
{showRequestButton &&
(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
<Button
buttonType="primary"

@ -63,7 +63,7 @@ const messages = defineMessages({
manageModalNoRequests: 'No requests.',
manageModalClearMedia: 'Clear Media Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this TV series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
'* This will irreversibly remove all data for this series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
originaltitle: 'Original Title',
showtype: 'Series Type',
anime: 'Anime',
@ -73,9 +73,9 @@ const messages = defineMessages({
opensonarr4k: 'Open Series in 4K Sonarr',
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',
allseasonsmarkedavailable: '* All seasons will be marked as available.',
seasons: '{seasonCount, plural, one {# Season} other {# Seasons}}',
episodeRuntime: 'Episode Runtime',

@ -69,7 +69,7 @@
"components.MovieDetails.manageModalNoRequests": "No requests.",
"components.MovieDetails.manageModalRequests": "Requests",
"components.MovieDetails.manageModalTitle": "Manage Movie",
"components.MovieDetails.mark4kavailable": "Mark 4K as Available",
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
"components.MovieDetails.markavailable": "Mark as Available",
"components.MovieDetails.openradarr": "Open Movie in Radarr",
"components.MovieDetails.openradarr4k": "Open Movie in 4K Radarr",
@ -77,7 +77,7 @@
"components.MovieDetails.originaltitle": "Original Title",
"components.MovieDetails.overview": "Overview",
"components.MovieDetails.overviewunavailable": "Overview unavailable.",
"components.MovieDetails.play4konplex": "Play 4K on Plex",
"components.MovieDetails.play4konplex": "Play in 4K on Plex",
"components.MovieDetails.playonplex": "Play on Plex",
"components.MovieDetails.recommendations": "Recommendations",
"components.MovieDetails.releasedate": "Release Date",
@ -103,37 +103,39 @@
"components.PermissionEdit.admin": "Admin",
"components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.",
"components.PermissionEdit.advancedrequest": "Advanced Requests",
"components.PermissionEdit.advancedrequestDescription": "Grant permission to use advanced request options (e.g., changing servers, profiles, or paths).",
"components.PermissionEdit.advancedrequestDescription": "Grant permission to use advanced request options.",
"components.PermissionEdit.autoapprove": "Auto-Approve",
"components.PermissionEdit.autoapprove4k": "Auto-Approve 4K",
"components.PermissionEdit.autoapprove4kDescription": "Grant automatic approval for all 4K requests made by this user.",
"components.PermissionEdit.autoapprove4kDescription": "Grant automatic approval for all 4K requests.",
"components.PermissionEdit.autoapprove4kMovies": "Auto-Approve 4K Movies",
"components.PermissionEdit.autoapprove4kMoviesDescription": "Grant automatic approval for 4K movie requests made by this user.",
"components.PermissionEdit.autoapprove4kMoviesDescription": "Grant automatic approval for 4K movie requests.",
"components.PermissionEdit.autoapprove4kSeries": "Auto-Approve 4K Series",
"components.PermissionEdit.autoapprove4kSeriesDescription": "Grant automatic approval for 4K series requests made by this user.",
"components.PermissionEdit.autoapproveDescription": "Grant automatic approval for all non-4K requests made by this user.",
"components.PermissionEdit.autoapprove4kSeriesDescription": "Grant automatic approval for 4K series requests.",
"components.PermissionEdit.autoapproveDescription": "Grant automatic approval for all non-4K requests.",
"components.PermissionEdit.autoapproveMovies": "Auto-Approve Movies",
"components.PermissionEdit.autoapproveMoviesDescription": "Grant automatic approval for non-4K movie requests made by this user.",
"components.PermissionEdit.autoapproveMoviesDescription": "Grant automatic approval for non-4K movie requests.",
"components.PermissionEdit.autoapproveSeries": "Auto-Approve Series",
"components.PermissionEdit.autoapproveSeriesDescription": "Grant automatic approval for non-4K series requests made by this user.",
"components.PermissionEdit.autoapproveSeriesDescription": "Grant automatic approval for non-4K series requests.",
"components.PermissionEdit.managerequests": "Manage Requests",
"components.PermissionEdit.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.",
"components.PermissionEdit.managerequestsDescription": "Grant permission to manage Overseerr requests. All requests made by a user with this permission will be automatically approved.",
"components.PermissionEdit.request": "Request",
"components.PermissionEdit.request4k": "Request 4K",
"components.PermissionEdit.request4kDescription": "Grant permission to request 4K movies and series.",
"components.PermissionEdit.request4kDescription": "Grant permission to request 4K media.",
"components.PermissionEdit.request4kMovies": "Request 4K Movies",
"components.PermissionEdit.request4kMoviesDescription": "Grant permission to request 4K movies.",
"components.PermissionEdit.request4kTv": "Request 4K Series",
"components.PermissionEdit.request4kTvDescription": "Grant permission to request 4K Series.",
"components.PermissionEdit.requestDescription": "Grant permission to request movies and series.",
"components.PermissionEdit.request4kTvDescription": "Grant permission to request 4K series.",
"components.PermissionEdit.requestDescription": "Grant permission to request non-4K media.",
"components.PermissionEdit.requestMovies": "Request Movies",
"components.PermissionEdit.requestMoviesDescription": "Grant permission to request non-4K movies.",
"components.PermissionEdit.requestTv": "Request Series",
"components.PermissionEdit.requestTvDescription": "Grant permission to request non-4K series.",
"components.PermissionEdit.settings": "Manage Settings",
"components.PermissionEdit.settingsDescription": "Grant permission to modify all Overseerr settings. A user must have this permission to grant it to others.",
"components.PermissionEdit.settingsDescription": "Grant permission to modify Overseerr settings. A user must have this permission to grant it to others.",
"components.PermissionEdit.users": "Manage Users",
"components.PermissionEdit.usersDescription": "Grant permission to manage Overseerr users. Users with this permission cannot modify users with or grant the Admin privilege.",
"components.PermissionEdit.viewrequests": "View Requests",
"components.PermissionEdit.viewrequestsDescription": "Grant permission to view other users' requests.",
"components.PermissionEdit.vote": "Vote",
"components.PermissionEdit.voteDescription": "Grant permission to vote on requests (voting not yet implemented).",
"components.PersonDetails.alsoknownas": "Also Known As: {names}",
"components.PersonDetails.appearsin": "Appearances",
"components.PersonDetails.ascharacter": "as {character}",
@ -658,11 +660,11 @@
"components.TvDetails.episodeRuntimeMinutes": "{runtime} minutes",
"components.TvDetails.firstAirDate": "First Air Date",
"components.TvDetails.manageModalClearMedia": "Clear Media Data",
"components.TvDetails.manageModalClearMediaWarning": "* This will irreversibly remove all data for this TV series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.",
"components.TvDetails.manageModalClearMediaWarning": "* This will irreversibly remove all data for this series, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.",
"components.TvDetails.manageModalNoRequests": "No requests.",
"components.TvDetails.manageModalRequests": "Requests",
"components.TvDetails.manageModalTitle": "Manage Series",
"components.TvDetails.mark4kavailable": "Mark 4K as Available",
"components.TvDetails.mark4kavailable": "Mark as Available in 4K",
"components.TvDetails.markavailable": "Mark as Available",
"components.TvDetails.network": "{networkCount, plural, one {Network} other {Networks}}",
"components.TvDetails.nextAirDate": "Next Air Date",
@ -672,7 +674,7 @@
"components.TvDetails.originaltitle": "Original Title",
"components.TvDetails.overview": "Overview",
"components.TvDetails.overviewunavailable": "Overview unavailable.",
"components.TvDetails.play4konplex": "Play 4K on Plex",
"components.TvDetails.play4konplex": "Play in 4K on Plex",
"components.TvDetails.playonplex": "Play on Plex",
"components.TvDetails.recommendations": "Recommendations",
"components.TvDetails.seasons": "{seasonCount, plural, one {# Season} other {# Seasons}}",

Loading…
Cancel
Save