mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
2 Commits
preview-mu
...
0xsysr3ll/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd33336a39 | ||
|
|
7756ec89e1 |
@@ -1263,6 +1263,10 @@ components:
|
||||
type: number
|
||||
rootFolder:
|
||||
type: string
|
||||
ignoreQuota:
|
||||
type: boolean
|
||||
example: false
|
||||
description: If true, this request will not count against the user's quota. Requires MANAGE_REQUESTS permission.
|
||||
required:
|
||||
- id
|
||||
- status
|
||||
@@ -6139,6 +6143,10 @@ paths:
|
||||
userId:
|
||||
type: number
|
||||
nullable: true
|
||||
ignoreQuota:
|
||||
type: boolean
|
||||
example: false
|
||||
description: If true, this request will not count against the user's quota. Requires MANAGE_REQUESTS permission.
|
||||
required:
|
||||
- mediaType
|
||||
- mediaId
|
||||
|
||||
@@ -111,10 +111,20 @@ export class MediaRequest {
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||
const canBypassQuota = user.hasPermission([Permission.MANAGE_REQUESTS]);
|
||||
|
||||
if (!canBypassQuota) {
|
||||
if (
|
||||
requestBody.mediaType === MediaType.MOVIE &&
|
||||
quotas.movie.restricted
|
||||
) {
|
||||
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||
} else if (
|
||||
requestBody.mediaType === MediaType.TV &&
|
||||
quotas.tv.restricted
|
||||
) {
|
||||
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||
}
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
@@ -371,6 +381,7 @@ export class MediaRequest {
|
||||
rootFolder: rootFolder,
|
||||
tags: tags,
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
ignoreQuota: requestBody.ignoreQuota ?? false,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
@@ -434,6 +445,7 @@ export class MediaRequest {
|
||||
if (finalSeasons.length === 0) {
|
||||
throw new NoSeasonsAvailableError('No seasons available to request');
|
||||
} else if (
|
||||
!canBypassQuota &&
|
||||
quotas.tv.limit &&
|
||||
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||
) {
|
||||
@@ -502,6 +514,7 @@ export class MediaRequest {
|
||||
})
|
||||
),
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
ignoreQuota: requestBody.ignoreQuota ?? false,
|
||||
});
|
||||
|
||||
await requestRepository.save(request);
|
||||
@@ -606,6 +619,9 @@ export class MediaRequest {
|
||||
@Column({ default: false })
|
||||
public isAutoRequest: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
public ignoreQuota: boolean;
|
||||
|
||||
constructor(init?: Partial<MediaRequest>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@@ -290,6 +290,7 @@ export class User {
|
||||
createdAt: AfterDate(movieDate),
|
||||
type: MediaType.MOVIE,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
ignoreQuota: false,
|
||||
},
|
||||
})
|
||||
: 0;
|
||||
@@ -323,6 +324,9 @@ export class User {
|
||||
.andWhere('request.status != :declinedStatus', {
|
||||
declinedStatus: MediaRequestStatus.DECLINED,
|
||||
})
|
||||
.andWhere('request.ignoreQuota = :ignoreQuota', {
|
||||
ignoreQuota: false,
|
||||
})
|
||||
.addSelect((subQuery) => {
|
||||
return subQuery
|
||||
.select('COUNT(season.id)', 'seasonCount')
|
||||
|
||||
@@ -22,4 +22,5 @@ export type MediaRequestBody = {
|
||||
languageProfileId?: number;
|
||||
userId?: number;
|
||||
tags?: number[];
|
||||
ignoreQuota?: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIgnoreQuotaToMediaRequest1760028688313
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIgnoreQuotaToMediaRequest1760028688313';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD "ignoreQuota" boolean NOT NULL DEFAULT false`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP COLUMN "ignoreQuota"`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIgnoreQuotaToMediaRequest1760028688313
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIgnoreQuotaToMediaRequest1760028688313';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedBy" integer, "modifiedBy" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "ignoreQuota" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_media_request_media" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_media_request_user" FOREIGN KEY ("requestedBy") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_media_request_user_2" FOREIGN KEY ("modifiedBy") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedBy", "modifiedBy", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedBy", "modifiedBy", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "media_request"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media_request"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedBy" integer, "modifiedBy" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_media_request_media" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_media_request_user" FOREIGN KEY ("requestedBy") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_media_request_user_2" FOREIGN KEY ("modifiedBy") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedBy", "modifiedBy", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedBy", "modifiedBy", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "temporary_media_request"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
|
||||
import SlideCheckbox from '@app/components/Common/SlideCheckbox';
|
||||
import type { User } from '@app/hooks/useUser';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
@@ -38,6 +39,9 @@ const messages = defineMessages('components.RequestModal.AdvancedRequester', {
|
||||
tags: 'Tags',
|
||||
selecttags: 'Select tags',
|
||||
notagoptions: 'No tags.',
|
||||
ignoreQuotaTitle: 'Bypass User Quota',
|
||||
ignoreQuotaDescription:
|
||||
"This request will not count against the user's quota limits. Use with caution.",
|
||||
});
|
||||
|
||||
export type RequestOverrides = {
|
||||
@@ -47,6 +51,7 @@ export type RequestOverrides = {
|
||||
tags?: number[];
|
||||
language?: number;
|
||||
user?: User;
|
||||
ignoreQuota?: boolean;
|
||||
};
|
||||
|
||||
interface AdvancedRequesterProps {
|
||||
@@ -55,6 +60,7 @@ interface AdvancedRequesterProps {
|
||||
isAnime?: boolean;
|
||||
defaultOverrides?: RequestOverrides;
|
||||
requestUser?: User;
|
||||
quota?: { movie: { limit?: number }; tv: { limit?: number } };
|
||||
onChange: (overrides: RequestOverrides) => void;
|
||||
}
|
||||
|
||||
@@ -64,6 +70,7 @@ const AdvancedRequester = ({
|
||||
isAnime = false,
|
||||
defaultOverrides,
|
||||
requestUser,
|
||||
quota,
|
||||
onChange,
|
||||
}: AdvancedRequesterProps) => {
|
||||
const intl = useIntl();
|
||||
@@ -97,6 +104,10 @@ const AdvancedRequester = ({
|
||||
defaultOverrides?.tags ?? []
|
||||
);
|
||||
|
||||
const [ignoreQuota, setIgnoreQuota] = useState<boolean>(
|
||||
defaultOverrides?.ignoreQuota ?? false
|
||||
);
|
||||
|
||||
const { data: serverData, isValidating } =
|
||||
useSWR<ServiceCommonServerWithDetails>(
|
||||
selectedServer !== null
|
||||
@@ -273,6 +284,7 @@ const AdvancedRequester = ({
|
||||
user: selectedUser ?? undefined,
|
||||
language: selectedLanguage !== -1 ? selectedLanguage : undefined,
|
||||
tags: selectedTags,
|
||||
ignoreQuota: ignoreQuota || undefined,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
@@ -282,6 +294,7 @@ const AdvancedRequester = ({
|
||||
selectedUser,
|
||||
selectedLanguage,
|
||||
selectedTags,
|
||||
ignoreQuota,
|
||||
]);
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -540,6 +553,25 @@ const AdvancedRequester = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentHasPermission([Permission.MANAGE_REQUESTS]) &&
|
||||
quota &&
|
||||
((type === 'movie' && quota.movie.limit && quota.movie.limit > 0) ||
|
||||
(type === 'tv' && quota.tv.limit && quota.tv.limit > 0)) && (
|
||||
<div className="mb-2">
|
||||
<label htmlFor="ignoreQuota">
|
||||
{intl.formatMessage(messages.ignoreQuotaTitle)}
|
||||
</label>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-400">
|
||||
{intl.formatMessage(messages.ignoreQuotaDescription)}
|
||||
</p>
|
||||
<SlideCheckbox
|
||||
checked={ignoreQuota}
|
||||
onClick={() => setIgnoreQuota(!ignoreQuota)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentHasPermission([
|
||||
Permission.MANAGE_REQUESTS,
|
||||
Permission.MANAGE_USERS,
|
||||
|
||||
@@ -94,6 +94,7 @@ const MovieRequestModal = ({
|
||||
mediaId: data?.id,
|
||||
mediaType: 'movie',
|
||||
is4k,
|
||||
ignoreQuota: requestOverrides?.ignoreQuota,
|
||||
...overrideParams,
|
||||
});
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
@@ -320,7 +321,10 @@ const MovieRequestModal = ({
|
||||
backgroundClickable
|
||||
onCancel={onCancel}
|
||||
onOk={sendRequest}
|
||||
okDisabled={isUpdating || quota?.movie.restricted}
|
||||
okDisabled={
|
||||
isUpdating ||
|
||||
(quota?.movie.restricted && !requestOverrides?.ignoreQuota)
|
||||
}
|
||||
title={intl.formatMessage(
|
||||
is4k ? messages.requestmovie4ktitle : messages.requestmovietitle
|
||||
)}
|
||||
@@ -359,6 +363,7 @@ const MovieRequestModal = ({
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
quota={quota}
|
||||
onChange={(overrides) => {
|
||||
setRequestOverrides(overrides);
|
||||
}}
|
||||
|
||||
@@ -199,6 +199,7 @@ const TvRequestModal = ({
|
||||
tvdbId: tvdbId ?? data?.externalIds.tvdbId,
|
||||
mediaType: 'tv',
|
||||
is4k,
|
||||
ignoreQuota: requestOverrides?.ignoreQuota,
|
||||
seasons: settings.currentSettings.partialRequestsEnabled
|
||||
? selectedSeasons.sort((a, b) => a - b)
|
||||
: getAllSeasons().filter(
|
||||
@@ -439,7 +440,8 @@ const TvRequestModal = ({
|
||||
? false
|
||||
: !settings.currentSettings.partialRequestsEnabled &&
|
||||
quota?.tv.limit &&
|
||||
unrequestedSeasons.length > quota.tv.limit
|
||||
unrequestedSeasons.length > quota.tv.limit &&
|
||||
!requestOverrides?.ignoreQuota
|
||||
? true
|
||||
: getAllRequestedSeasons().length >= getAllSeasons().length ||
|
||||
(settings.currentSettings.partialRequestsEnabled &&
|
||||
@@ -726,6 +728,7 @@ const TvRequestModal = ({
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
)}
|
||||
quota={quota}
|
||||
onChange={(overrides) => setRequestOverrides(overrides)}
|
||||
requestUser={editRequest?.requestedBy}
|
||||
defaultOverrides={
|
||||
|
||||
@@ -520,6 +520,8 @@
|
||||
"components.RequestModal.AdvancedRequester.default": "{name} (Default)",
|
||||
"components.RequestModal.AdvancedRequester.destinationserver": "Destination Server",
|
||||
"components.RequestModal.AdvancedRequester.folder": "{path} ({space})",
|
||||
"components.RequestModal.AdvancedRequester.ignoreQuotaDescription": "This request will not count against the user's quota limits. Use with caution.",
|
||||
"components.RequestModal.AdvancedRequester.ignoreQuotaTitle": "Bypass User Quota",
|
||||
"components.RequestModal.AdvancedRequester.languageprofile": "Language Profile",
|
||||
"components.RequestModal.AdvancedRequester.notagoptions": "No tags.",
|
||||
"components.RequestModal.AdvancedRequester.qualityprofile": "Quality Profile",
|
||||
|
||||
Reference in New Issue
Block a user