feat(blocklist): add support for collections (#1841)

This commit is contained in:
0xsysr3ll
2026-03-30 00:19:45 +02:00
committed by GitHub
parent 56b79ff38c
commit 993ae4c58e
8 changed files with 445 additions and 39 deletions

View File

@@ -4798,6 +4798,49 @@ paths:
responses: responses:
'204': '204':
description: Succesfully removed media item description: Succesfully removed media item
/blocklist/collection/{collectionId}:
post:
summary: Add collection to blocklist
description: Adds all movies in a collection to the blocklist
tags:
- blocklist
parameters:
- in: path
name: collectionId
description: Collection ID
required: true
example: '1424991'
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
type: object
responses:
'201':
description: Successfully added collection to blocklist
'500':
description: Error adding collection to blocklist
delete:
summary: Remove collection from blocklist
description: Removes all movies in a collection from the blocklist
tags:
- blocklist
parameters:
- in: path
name: collectionId
description: Collection ID
required: true
example: '1424991'
schema:
type: string
responses:
'204':
description: Successfully removed collection from blocklist
'500':
description: Error removing collection from blocklist
/watchlist: /watchlist:
post: post:
summary: Add media to watchlist summary: Add media to watchlist

View File

@@ -1,5 +1,6 @@
import { MediaType } from '@server/constants/media'; import TheMovieDb from '@server/api/themoviedb';
import { getRepository } from '@server/datasource'; import { MediaStatus, MediaType } from '@server/constants/media';
import dataSource, { getRepository } from '@server/datasource';
import { Blocklist } from '@server/entity/Blocklist'; import { Blocklist } from '@server/entity/Blocklist';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import type { BlocklistResultsResponse } from '@server/interfaces/api/blocklistInterfaces'; import type { BlocklistResultsResponse } from '@server/interfaces/api/blocklistInterfaces';
@@ -7,7 +8,7 @@ import { Permission } from '@server/lib/permissions';
import logger from '@server/logger'; import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth'; import { isAuthenticated } from '@server/middleware/auth';
import { Router } from 'express'; import { Router } from 'express';
import { EntityNotFoundError, QueryFailedError } from 'typeorm'; import { EntityNotFoundError, In, QueryFailedError } from 'typeorm';
import { z } from 'zod'; import { z } from 'zod';
const blocklistRoutes = Router(); const blocklistRoutes = Router();
@@ -17,6 +18,7 @@ export const blocklistAdd = z.object({
mediaType: z.nativeEnum(MediaType), mediaType: z.nativeEnum(MediaType),
title: z.coerce.string().optional(), title: z.coerce.string().optional(),
user: z.coerce.number(), user: z.coerce.number(),
blocklistedTags: z.string().optional(),
}); });
const blocklistGet = z.object({ const blocklistGet = z.object({
@@ -158,6 +160,107 @@ blocklistRoutes.post(
} }
); );
blocklistRoutes.post(
'/collection/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.locale,
});
const uniqueParts = [
...new Map(collection.parts.map((p) => [p.id, p])).values(),
];
const partIds = uniqueParts.map((p) => p.id);
if (partIds.length === 0) {
return res.status(201).send();
}
await dataSource.transaction(async (em) => {
const blocklistRepository = em.getRepository(Blocklist);
const mediaRepository = em.getRepository(Media);
const [existingBlocklists, existingMedia] = await Promise.all([
blocklistRepository.find({
where: { tmdbId: In(partIds), mediaType: MediaType.MOVIE },
}),
mediaRepository.find({
where: { tmdbId: In(partIds), mediaType: MediaType.MOVIE },
}),
]);
const blocklistByTmdbId = new Map(
existingBlocklists.map((b) => [b.tmdbId, b])
);
const mediaByTmdbId = new Map(existingMedia.map((m) => [m.tmdbId, m]));
await Promise.all(
uniqueParts.map(async (part) => {
if (blocklistByTmdbId.has(part.id)) {
return;
}
let blocklist = new Blocklist({
tmdbId: part.id,
mediaType: MediaType.MOVIE,
title: part.title,
user: req.user,
});
try {
await blocklistRepository.save(blocklist);
} catch (error) {
if (
!(error instanceof QueryFailedError) ||
error.driverError.errno !== 19
) {
throw error;
}
const row = await blocklistRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (!row) {
throw error;
}
blocklist = row;
}
let media = mediaByTmdbId.get(part.id);
if (!media) {
media = new Media({
tmdbId: part.id,
status: MediaStatus.BLOCKLISTED,
status4k: MediaStatus.BLOCKLISTED,
mediaType: MediaType.MOVIE,
blocklist: Promise.resolve(blocklist),
});
} else {
media.status = MediaStatus.BLOCKLISTED;
media.status4k = MediaStatus.BLOCKLISTED;
media.blocklist = Promise.resolve(blocklist);
}
await mediaRepository.save(media);
})
);
});
return res.status(201).send();
} catch (e) {
logger.error('Error blocklisting collection', {
label: 'Blocklist',
errorMessage: e.message,
collectionId: req.params.id,
});
return next({ status: 500, message: e.message });
}
}
);
blocklistRoutes.delete( blocklistRoutes.delete(
'/:id', '/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], { isAuthenticated([Permission.MANAGE_BLOCKLIST], {
@@ -208,4 +311,54 @@ blocklistRoutes.delete(
} }
); );
blocklistRoutes.delete(
'/collection/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.locale,
});
await dataSource.transaction(async (em) => {
const blocklistRepository = em.getRepository(Blocklist);
const mediaRepository = em.getRepository(Media);
await Promise.all(
collection.parts.map(async (part) => {
const blocklistItem = await blocklistRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (blocklistItem) {
await blocklistRepository.remove(blocklistItem);
const mediaItem = await mediaRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (mediaItem) {
await mediaRepository.remove(mediaItem);
}
}
})
);
});
return res.status(204).send();
} catch (e) {
logger.error('Error unblocklisting collection', {
label: 'Blocklist',
errorMessage: e.message,
collectionId: req.params.id,
});
return next({ status: 500, message: e.message });
}
}
);
export default blocklistRoutes; export default blocklistRoutes;

View File

@@ -2,6 +2,8 @@ import Modal from '@app/components/Common/Modal';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react'; import { Transition } from '@headlessui/react';
import type { Collection } from '@server/models/Collection';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
@@ -21,8 +23,18 @@ const messages = defineMessages('component.BlocklistModal', {
blocklisting: 'Blocklisting', blocklisting: 'Blocklisting',
}); });
const isCollection = (
data: MovieDetails | TvDetails | Collection | null
): data is Collection => {
return (
data !== null &&
data !== undefined &&
(data as Collection).parts !== undefined
);
};
const isMovie = ( const isMovie = (
movie: MovieDetails | TvDetails | null movie: MovieDetails | TvDetails | Collection | null
): movie is MovieDetails => { ): movie is MovieDetails => {
if (!movie) return false; if (!movie) return false;
return (movie as MovieDetails).title !== undefined; return (movie as MovieDetails).title !== undefined;
@@ -37,7 +49,9 @@ const BlocklistModal = ({
isUpdating, isUpdating,
}: BlocklistModalProps) => { }: BlocklistModalProps) => {
const intl = useIntl(); const intl = useIntl();
const [data, setData] = useState<TvDetails | MovieDetails | null>(null); const [data, setData] = useState<
TvDetails | MovieDetails | Collection | null
>(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
@@ -68,11 +82,19 @@ const BlocklistModal = ({
loading={!data && !error} loading={!data && !error}
backgroundClickable backgroundClickable
title={`${intl.formatMessage(globalMessages.blocklist)} ${ title={`${intl.formatMessage(globalMessages.blocklist)} ${
isMovie(data) type === 'collection'
? intl.formatMessage(globalMessages.movie) ? intl.formatMessage(globalMessages.collection)
: intl.formatMessage(globalMessages.tvshow) : isMovie(data)
? intl.formatMessage(globalMessages.movie)
: intl.formatMessage(globalMessages.tvshow)
}`}
subTitle={`${
isCollection(data)
? data.name
: isMovie(data)
? data.title
: data?.name
}`} }`}
subTitle={`${isMovie(data) ? data.title : data?.name}`}
onCancel={onCancel} onCancel={onCancel}
onOk={onComplete} onOk={onComplete}
okText={ okText={

View File

@@ -1,7 +1,10 @@
import BlocklistModal from '@app/components/BlocklistModal';
import Button from '@app/components/Common/Button';
import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown'; import ButtonWithDropdown from '@app/components/Common/ButtonWithDropdown';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle'; import PageTitle from '@app/components/Common/PageTitle';
import Tooltip from '@app/components/Common/Tooltip';
import RequestModal from '@app/components/RequestModal'; import RequestModal from '@app/components/RequestModal';
import Slider from '@app/components/Slider'; import Slider from '@app/components/Slider';
import StatusBadge from '@app/components/StatusBadge'; import StatusBadge from '@app/components/StatusBadge';
@@ -12,19 +15,27 @@ import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error'; import ErrorPage from '@app/pages/_error';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper'; import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import {
ArrowDownTrayIcon,
EyeIcon,
EyeSlashIcon,
} from '@heroicons/react/24/outline';
import { MediaStatus } from '@server/constants/media'; import { MediaStatus } from '@server/constants/media';
import type { Collection } from '@server/models/Collection'; import type { Collection } from '@server/models/Collection';
import axios from 'axios';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages('components.CollectionDetails', { const messages = defineMessages('components.CollectionDetails', {
overview: 'Overview', overview: 'Overview',
numberofmovies: '{count} Movies', numberofmovies: '{count} Movies',
removefromblocklistpartialcount:
'{removeLabel} ({count, plural, one {# movie} other {# movies}})',
requestcollection: 'Request Collection', requestcollection: 'Request Collection',
requestcollection4k: 'Request Collection in 4K', requestcollection4k: 'Request Collection in 4K',
}); });
@@ -40,6 +51,9 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
const { hasPermission } = useUser(); const { hasPermission } = useUser();
const [requestModal, setRequestModal] = useState(false); const [requestModal, setRequestModal] = useState(false);
const [is4k, setIs4k] = useState(false); const [is4k, setIs4k] = useState(false);
const [showBlocklistModal, setShowBlocklistModal] = useState(false);
const [isBlocklistUpdating, setIsBlocklistUpdating] = useState(false);
const { addToast } = useToasts();
const returnCollectionDownloadItems = (data: Collection | undefined) => { const returnCollectionDownloadItems = (data: Collection | undefined) => {
const [downloadStatus, downloadStatus4k] = [ const [downloadStatus, downloadStatus4k] = [
@@ -70,6 +84,63 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
const { data: genres } = const { data: genres } =
useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`); useSWR<{ id: number; name: string }[]>(`/api/v1/genres/movie`);
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlocklistUpdating(true);
try {
await axios.post(`/api/v1/blocklist/collection/${data?.id}`);
addToast(
<span>
{intl.formatMessage(globalMessages.blocklistSuccess, {
title: data?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
} catch {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsBlocklistUpdating(false);
setShowBlocklistModal(false);
};
const onClickUnblocklistBtn = async (): Promise<void> => {
if (!data) return;
setIsBlocklistUpdating(true);
try {
await axios.delete(`/api/v1/blocklist/collection/${data.id}`);
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title: data.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
revalidate();
} catch {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
setIsBlocklistUpdating(false);
};
const [downloadStatus, downloadStatus4k] = useMemo(() => { const [downloadStatus, downloadStatus4k] = useMemo(() => {
const downloadItems = returnCollectionDownloadItems(data); const downloadItems = returnCollectionDownloadItems(data);
return [downloadItems.downloadStatus, downloadItems.downloadStatus4k]; return [downloadItems.downloadStatus, downloadItems.downloadStatus4k];
@@ -97,7 +168,17 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
let collectionStatus = MediaStatus.UNKNOWN; let collectionStatus = MediaStatus.UNKNOWN;
let collectionStatus4k = MediaStatus.UNKNOWN; let collectionStatus4k = MediaStatus.UNKNOWN;
if ( const blocklistedParts = data.parts.filter(
(part) =>
part.mediaInfo && part.mediaInfo.status === MediaStatus.BLOCKLISTED
);
const isCollectionBlocklisted = blocklistedParts.length > 0;
const isCollectionPartiallyBlocklisted =
blocklistedParts.length > 0 && blocklistedParts.length < data.parts.length;
if (isCollectionBlocklisted) {
collectionStatus = MediaStatus.BLOCKLISTED;
} else if (
data.parts.every( data.parts.every(
(part) => (part) =>
part.mediaInfo && part.mediaInfo.status === MediaStatus.AVAILABLE part.mediaInfo && part.mediaInfo.status === MediaStatus.AVAILABLE
@@ -152,6 +233,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
part.mediaInfo.status4k === MediaStatus.UNKNOWN part.mediaInfo.status4k === MediaStatus.UNKNOWN
).length > 0; ).length > 0;
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
const collectionAttributes: React.ReactNode[] = []; const collectionAttributes: React.ReactNode[] = [];
collectionAttributes.push( collectionAttributes.push(
@@ -188,11 +274,6 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
); );
} }
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
return ( return (
<div <div
className="media-page" className="media-page"
@@ -231,6 +312,15 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
}} }}
onCancel={() => setRequestModal(false)} onCancel={() => setRequestModal(false)}
/> />
<BlocklistModal
tmdbId={data.id}
type="collection"
show={showBlocklistModal}
onCancel={() => setShowBlocklistModal(false)}
onComplete={onClickHideItemBtn}
isUpdating={isBlocklistUpdating}
/>
<div className="media-header"> <div className="media-header">
<div className="media-poster"> <div className="media-poster">
<CachedImage <CachedImage
@@ -254,6 +344,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
status={collectionStatus} status={collectionStatus}
downloadItem={downloadStatus} downloadItem={downloadStatus}
title={titles} title={titles}
statusLabelOverride={
isCollectionPartiallyBlocklisted
? intl.formatMessage(globalMessages.partiallyblocklisted)
: undefined
}
inProgress={data.parts.some( inProgress={data.parts.some(
(part) => (part.mediaInfo?.downloadStatus ?? []).length > 0 (part) => (part.mediaInfo?.downloadStatus ?? []).length > 0
)} )}
@@ -292,6 +387,48 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
</span> </span>
</div> </div>
<div className="media-actions"> <div className="media-actions">
{hasPermission([Permission.MANAGE_BLOCKLIST], { type: 'or' }) &&
(isCollectionBlocklisted ? (
<Tooltip
content={
blocklistedParts.length === data.parts.length
? intl.formatMessage(globalMessages.removefromBlocklist)
: intl.formatMessage(
messages.removefromblocklistpartialcount,
{
removeLabel: intl.formatMessage(
globalMessages.removefromBlocklist
),
count: blocklistedParts.length,
}
)
}
>
<Button
buttonType="ghost"
className="z-40 mr-2"
buttonSize="md"
onClick={onClickUnblocklistBtn}
disabled={isBlocklistUpdating}
>
<EyeIcon />
</Button>
</Tooltip>
) : (
<Tooltip
content={intl.formatMessage(globalMessages.addToBlocklist)}
>
<Button
buttonType="ghost"
className="z-40 mr-2"
buttonSize="md"
onClick={() => setShowBlocklistModal(true)}
disabled={isBlocklistUpdating}
>
<EyeSlashIcon />
</Button>
</Tooltip>
))}
{(hasRequestable || hasRequestable4k) && ( {(hasRequestable || hasRequestable4k) && (
<ButtonWithDropdown <ButtonWithDropdown
buttonType="primary" buttonType="primary"
@@ -349,8 +486,9 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
isEmpty={data.parts.length === 0} isEmpty={data.parts.length === 0}
items={data.parts items={data.parts
.filter((title) => { .filter((title) => {
if (!blocklistVisibility) if (!blocklistVisibility) {
return title.mediaInfo?.status !== MediaStatus.BLOCKLISTED; return title.mediaInfo?.status !== MediaStatus.BLOCKLISTED;
}
return title; return title;
}) })
.map((title) => ( .map((title) => (
@@ -365,6 +503,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
userScore={title.voteAverage} userScore={title.voteAverage}
year={title.releaseDate} year={title.releaseDate}
mediaType={title.mediaType} mediaType={title.mediaType}
mutateParent={revalidate}
/> />
))} ))}
/> />

View File

@@ -31,6 +31,7 @@ interface StatusBadgeProps {
tmdbId?: number; tmdbId?: number;
mediaType?: 'movie' | 'tv'; mediaType?: 'movie' | 'tv';
title?: string | string[]; title?: string | string[];
statusLabelOverride?: string;
} }
const StatusBadge = ({ const StatusBadge = ({
@@ -43,6 +44,7 @@ const StatusBadge = ({
tmdbId, tmdbId,
mediaType, mediaType,
title, title,
statusLabelOverride,
}: StatusBadgeProps) => { }: StatusBadgeProps) => {
const intl = useIntl(); const intl = useIntl();
const { hasPermission } = useUser(); const { hasPermission } = useUser();
@@ -364,7 +366,9 @@ const StatusBadge = ({
<Tooltip content={mediaLinkDescription}> <Tooltip content={mediaLinkDescription}>
<Badge badgeType="danger" href={mediaLink}> <Badge badgeType="danger" href={mediaLink}>
{intl.formatMessage(is4k ? messages.status4k : messages.status, { {intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.blocklisted), status:
statusLabelOverride ??
intl.formatMessage(globalMessages.blocklisted),
})} })}
</Badge> </Badge>
</Tooltip> </Tooltip>

View File

@@ -175,12 +175,16 @@ const TitleCard = ({
if (topNode) { if (topNode) {
try { try {
await axios.post('/api/v1/blocklist', { if (mediaType === 'collection') {
tmdbId: id, await axios.post(`/api/v1/blocklist/collection/${id}`);
mediaType, } else {
title, await axios.post('/api/v1/blocklist', {
user: user?.id, tmdbId: id,
}); mediaType,
title,
user: user?.id,
});
}
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.blocklistSuccess, { {intl.formatMessage(globalMessages.blocklistSuccess, {
@@ -191,6 +195,9 @@ const TitleCard = ({
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
setCurrentStatus(MediaStatus.BLOCKLISTED); setCurrentStatus(MediaStatus.BLOCKLISTED);
if (mutateParent) {
mutateParent();
}
} catch (e) { } catch (e) {
if (e?.response?.status === 412) { if (e?.response?.status === 412) {
addToast( addToast(
@@ -225,22 +232,57 @@ const TitleCard = ({
const topNode = cardRef.current; const topNode = cardRef.current;
if (topNode) { if (topNode) {
const res = await axios.delete( try {
`/api/v1/blocklist/${id}?mediaType=${mediaType}` if (mediaType === 'collection') {
); const res = await axios.delete(`/api/v1/blocklist/collection/${id}`);
if (res.status === 204) { if (res.status === 204) {
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, { {intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title, title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})} })}
</span>, </span>,
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
setCurrentStatus(MediaStatus.UNKNOWN); setCurrentStatus(MediaStatus.UNKNOWN);
} else { if (mutateParent) {
mutateParent();
}
} else {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
} else {
const res = await axios.delete(
`/api/v1/blocklist/${id}?mediaType=${mediaType}`
);
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.UNKNOWN);
if (mutateParent) {
mutateParent();
}
} else {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
}
} catch {
addToast(intl.formatMessage(globalMessages.blocklistError), { addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,

View File

@@ -60,6 +60,7 @@ const globalMessages = defineMessages('i18n', {
resolved: 'Resolved', resolved: 'Resolved',
blocklist: 'Blocklist', blocklist: 'Blocklist',
blocklisted: 'Blocklisted', blocklisted: 'Blocklisted',
partiallyblocklisted: 'Partially Blocklisted',
blocklistSuccess: '<strong>{title}</strong> was successfully blocklisted.', blocklistSuccess: '<strong>{title}</strong> was successfully blocklisted.',
blocklistError: 'Something went wrong. Please try again.', blocklistError: 'Something went wrong. Please try again.',
blocklistDuplicateError: blocklistDuplicateError:

View File

@@ -18,6 +18,7 @@
"components.Blocklist.showAllBlocklisted": "Show All Blocklisted Media", "components.Blocklist.showAllBlocklisted": "Show All Blocklisted Media",
"components.CollectionDetails.numberofmovies": "{count} Movies", "components.CollectionDetails.numberofmovies": "{count} Movies",
"components.CollectionDetails.overview": "Overview", "components.CollectionDetails.overview": "Overview",
"components.CollectionDetails.removefromblocklistpartialcount": "{removeLabel} ({count, plural, one {# movie} other {# movies}})",
"components.CollectionDetails.requestcollection": "Request Collection", "components.CollectionDetails.requestcollection": "Request Collection",
"components.CollectionDetails.requestcollection4k": "Request Collection in 4K", "components.CollectionDetails.requestcollection4k": "Request Collection in 4K",
"components.Discover.CreateSlider.addSlider": "Add Slider", "components.Discover.CreateSlider.addSlider": "Add Slider",
@@ -1601,6 +1602,7 @@
"i18n.notrequested": "Not Requested", "i18n.notrequested": "Not Requested",
"i18n.open": "Open", "i18n.open": "Open",
"i18n.partiallyavailable": "Partially Available", "i18n.partiallyavailable": "Partially Available",
"i18n.partiallyblocklisted": "Partially Blocklisted",
"i18n.pending": "Pending", "i18n.pending": "Pending",
"i18n.previous": "Previous", "i18n.previous": "Previous",
"i18n.processing": "Processing", "i18n.processing": "Processing",