diff --git a/overseerr-api.yml b/overseerr-api.yml
index d24035380..f5e1d1622 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -38,6 +38,8 @@ tags:
     description: Endpoints related to getting service (Radarr/Sonarr) details.
   - name: watchlist
     description: Collection of media to watch later
+  - name: blacklist
+    description: Blacklisted media from discovery page.
 servers:
   - url: '{server}/api/v1'
     variables:
@@ -46,6 +48,19 @@ servers:
 
 components:
   schemas:
+    Blacklist:
+      type: object
+      properties:
+        tmdbId:
+          type: number
+          example: 1
+        title:
+          type: string
+        media:
+          $ref: '#/components/schemas/MediaInfo'
+        userId:
+          type: number
+          example: 1
     Watchlist:
       type: object
       properties:
@@ -4042,6 +4057,94 @@ paths:
                       restricted:
                         type: boolean
                         example: false
+  /blacklist:
+    get:
+      summary: Returns blacklisted items
+      description: Returns list of all blacklisted media
+      tags:
+        - settings
+      parameters:
+        - in: query
+          name: take
+          schema:
+            type: number
+            nullable: true
+            example: 25
+        - in: query
+          name: skip
+          schema:
+            type: number
+            nullable: true
+            example: 0
+        - in: query
+          name: search
+          schema:
+            type: string
+            nullable: true
+            example: dune
+      responses:
+        '200':
+          description: Blacklisted items returned
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  pageInfo:
+                    $ref: '#/components/schemas/PageInfo'
+                  results:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        user:
+                          $ref: '#/components/schemas/User'
+                        createdAt:
+                          type: string
+                          example: 2024-04-21T01:55:44.000Z
+                        id:
+                          type: number
+                          example: 1
+                        mediaType:
+                          type: string
+                          example: movie
+                        title:
+                          type: string
+                          example: Dune
+                        tmdbId:
+                          type: number
+                          example: 438631
+    post:
+      summary: Add media to blacklist
+      tags:
+        - blacklist
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Blacklist'
+      responses:
+        '201':
+          description: Item succesfully blacklisted
+        '412':
+          description: Item has already been blacklisted
+  /blacklist/{tmdbId}:
+    delete:
+      summary: Remove media from blacklist
+      tags:
+        - blacklist
+      parameters:
+        - in: path
+          name: tmdbId
+          description: tmdbId ID
+          required: true
+          example: '1'
+          schema:
+            type: string
+      responses:
+        '204':
+          description: Succesfully removed media item
   /watchlist:
     post:
       summary: Add media to watchlist
diff --git a/server/constants/media.ts b/server/constants/media.ts
index de2bf834d..dbcfbd347 100644
--- a/server/constants/media.ts
+++ b/server/constants/media.ts
@@ -16,4 +16,5 @@ export enum MediaStatus {
   PROCESSING,
   PARTIALLY_AVAILABLE,
   AVAILABLE,
+  BLACKLISTED,
 }
diff --git a/server/entity/Blacklist.ts b/server/entity/Blacklist.ts
new file mode 100644
index 000000000..5e24419dc
--- /dev/null
+++ b/server/entity/Blacklist.ts
@@ -0,0 +1,95 @@
+import { MediaStatus, type MediaType } from '@server/constants/media';
+import { getRepository } from '@server/datasource';
+import Media from '@server/entity/Media';
+import { User } from '@server/entity/User';
+import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
+import {
+  Column,
+  CreateDateColumn,
+  Entity,
+  Index,
+  JoinColumn,
+  ManyToOne,
+  OneToOne,
+  PrimaryGeneratedColumn,
+  Unique,
+} from 'typeorm';
+import type { ZodNumber, ZodOptional, ZodString } from 'zod';
+
+@Entity()
+@Unique(['tmdbId'])
+export class Blacklist implements BlacklistItem {
+  @PrimaryGeneratedColumn()
+  public id: number;
+
+  @Column({ type: 'varchar' })
+  public mediaType: MediaType;
+
+  @Column({ nullable: true, type: 'varchar' })
+  title?: string;
+
+  @Column()
+  @Index()
+  public tmdbId: number;
+
+  @ManyToOne(() => User, (user) => user.id, {
+    eager: true,
+  })
+  user: User;
+
+  @OneToOne(() => Media, (media) => media.blacklist, {
+    onDelete: 'CASCADE',
+  })
+  @JoinColumn()
+  public media: Media;
+
+  @CreateDateColumn()
+  public createdAt: Date;
+
+  constructor(init?: Partial<Blacklist>) {
+    Object.assign(this, init);
+  }
+
+  public static async addToBlacklist({
+    blacklistRequest,
+  }: {
+    blacklistRequest: {
+      mediaType: MediaType;
+      title?: ZodOptional<ZodString>['_output'];
+      tmdbId: ZodNumber['_output'];
+    };
+  }): Promise<void> {
+    const blacklist = new this({
+      ...blacklistRequest,
+    });
+
+    const mediaRepository = getRepository(Media);
+    let media = await mediaRepository.findOne({
+      where: {
+        tmdbId: blacklistRequest.tmdbId,
+      },
+    });
+
+    const blacklistRepository = getRepository(this);
+
+    await blacklistRepository.save(blacklist);
+
+    if (!media) {
+      media = new Media({
+        tmdbId: blacklistRequest.tmdbId,
+        status: MediaStatus.BLACKLISTED,
+        status4k: MediaStatus.BLACKLISTED,
+        mediaType: blacklistRequest.mediaType,
+        blacklist: blacklist,
+      });
+
+      await mediaRepository.save(media);
+    } else {
+      media.blacklist = blacklist;
+      media.status = MediaStatus.BLACKLISTED;
+      media.status4k = MediaStatus.BLACKLISTED;
+
+      await mediaRepository.save(media);
+    }
+  }
+}
diff --git a/server/entity/Media.ts b/server/entity/Media.ts
index 723eb213d..4f64178a7 100644
--- a/server/entity/Media.ts
+++ b/server/entity/Media.ts
@@ -3,6 +3,7 @@ import SonarrAPI from '@server/api/servarr/sonarr';
 import { MediaStatus, MediaType } from '@server/constants/media';
 import { MediaServerType } from '@server/constants/server';
 import { getRepository } from '@server/datasource';
+import { Blacklist } from '@server/entity/Blacklist';
 import type { User } from '@server/entity/User';
 import { Watchlist } from '@server/entity/Watchlist';
 import type { DownloadingItem } from '@server/lib/downloadtracker';
@@ -17,6 +18,7 @@ import {
   Entity,
   Index,
   OneToMany,
+  OneToOne,
   PrimaryGeneratedColumn,
   UpdateDateColumn,
 } from 'typeorm';
@@ -66,7 +68,7 @@ class Media {
 
     try {
       const media = await mediaRepository.findOne({
-        where: { tmdbId: id, mediaType },
+        where: { tmdbId: id, mediaType: mediaType },
         relations: { requests: true, issues: true },
       });
 
@@ -116,6 +118,11 @@ class Media {
   @OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
   public issues: Issue[];
 
+  @OneToOne(() => Blacklist, (blacklist) => blacklist.media, {
+    eager: true,
+  })
+  public blacklist: Blacklist;
+
   @CreateDateColumn()
   public createdAt: Date;
 
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index ba67ab7be..6b2c7b56e 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -40,6 +40,7 @@ export class RequestPermissionError extends Error {}
 export class QuotaRestrictedError extends Error {}
 export class DuplicateMediaRequestError extends Error {}
 export class NoSeasonsAvailableError extends Error {}
+export class BlacklistedMediaError extends Error {}
 
 type MediaRequestOptions = {
   isAutoRequest?: boolean;
@@ -143,6 +144,16 @@ export class MediaRequest {
         mediaType: requestBody.mediaType,
       });
     } else {
+      if (media.status === MediaStatus.BLACKLISTED) {
+        logger.warn('Request for media blocked due to being blacklisted', {
+          tmdbId: tmdbMedia.id,
+          mediaType: requestBody.mediaType,
+          label: 'Media Request',
+        });
+
+        throw new BlacklistedMediaError('This media is blacklisted.');
+      }
+
       if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
         media.status = MediaStatus.PENDING;
       }
diff --git a/server/interfaces/api/blacklistInterfaces.ts b/server/interfaces/api/blacklistInterfaces.ts
new file mode 100644
index 000000000..99e56585c
--- /dev/null
+++ b/server/interfaces/api/blacklistInterfaces.ts
@@ -0,0 +1,14 @@
+import type { User } from '@server/entity/User';
+import type { PaginatedResponse } from '@server/interfaces/api/common';
+
+export interface BlacklistItem {
+  tmdbId: number;
+  mediaType: 'movie' | 'tv';
+  title?: string;
+  createdAt?: Date;
+  user: User;
+}
+
+export interface BlacklistResultsResponse extends PaginatedResponse {
+  results: BlacklistItem[];
+}
diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts
index 4a4a90d84..bc477169c 100644
--- a/server/lib/permissions.ts
+++ b/server/lib/permissions.ts
@@ -27,6 +27,8 @@ export enum Permission {
   AUTO_REQUEST_TV = 33554432,
   RECENT_VIEW = 67108864,
   WATCHLIST_VIEW = 134217728,
+  MANAGE_BLACKLIST = 268435456,
+  VIEW_BLACKLIST = 1073741824,
 }
 
 export interface PermissionCheckOptions {
diff --git a/server/migration/1699901142442-AddBlacklist.ts b/server/migration/1699901142442-AddBlacklist.ts
new file mode 100644
index 000000000..eb0962707
--- /dev/null
+++ b/server/migration/1699901142442-AddBlacklist.ts
@@ -0,0 +1,20 @@
+import type { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddBlacklist1699901142442 implements MigrationInterface {
+  name = 'AddBlacklist1699901142442';
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      `CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')),"userId" integer, "mediaId" integer,CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId"))`
+    );
+
+    await queryRunner.query(
+      `CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
+    );
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`DROP TABLE "blacklist"`);
+    await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
+  }
+}
diff --git a/server/routes/blacklist.ts b/server/routes/blacklist.ts
new file mode 100644
index 000000000..4a07a4998
--- /dev/null
+++ b/server/routes/blacklist.ts
@@ -0,0 +1,148 @@
+import { MediaType } from '@server/constants/media';
+import { getRepository } from '@server/datasource';
+import { Blacklist } from '@server/entity/Blacklist';
+import Media from '@server/entity/Media';
+import { NotFoundError } from '@server/entity/Watchlist';
+import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces';
+import { Permission } from '@server/lib/permissions';
+import logger from '@server/logger';
+import { isAuthenticated } from '@server/middleware/auth';
+import { Router } from 'express';
+import rateLimit from 'express-rate-limit';
+import { QueryFailedError } from 'typeorm';
+import { z } from 'zod';
+
+const blacklistRoutes = Router();
+
+export const blacklistAdd = z.object({
+  tmdbId: z.coerce.number(),
+  mediaType: z.nativeEnum(MediaType),
+  title: z.coerce.string().optional(),
+  user: z.coerce.number(),
+});
+
+blacklistRoutes.get(
+  '/',
+  isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
+    type: 'or',
+  }),
+  rateLimit({ windowMs: 60 * 1000, max: 50 }),
+  async (req, res, next) => {
+    const pageSize = req.query.take ? Number(req.query.take) : 25;
+    const skip = req.query.skip ? Number(req.query.skip) : 0;
+    const search = (req.query.search as string) ?? '';
+
+    try {
+      let query = getRepository(Blacklist)
+        .createQueryBuilder('blacklist')
+        .leftJoinAndSelect('blacklist.user', 'user');
+
+      if (search.length > 0) {
+        query = query.where('blacklist.title like :title', {
+          title: `%${search}%`,
+        });
+      }
+
+      const [blacklistedItems, itemsCount] = await query
+        .orderBy('blacklist.createdAt', 'DESC')
+        .take(pageSize)
+        .skip(skip)
+        .getManyAndCount();
+
+      return res.status(200).json({
+        pageInfo: {
+          pages: Math.ceil(itemsCount / pageSize),
+          pageSize,
+          results: itemsCount,
+          page: Math.ceil(skip / pageSize) + 1,
+        },
+        results: blacklistedItems,
+      } as BlacklistResultsResponse);
+    } catch (error) {
+      logger.error('Something went wrong while retrieving blacklisted items', {
+        label: 'Blacklist',
+        errorMessage: error.message,
+      });
+      return next({
+        status: 500,
+        message: 'Unable to retrieve blacklisted items.',
+      });
+    }
+  }
+);
+
+blacklistRoutes.post(
+  '/',
+  isAuthenticated([Permission.MANAGE_BLACKLIST], {
+    type: 'or',
+  }),
+  async (req, res, next) => {
+    try {
+      const values = blacklistAdd.parse(req.body);
+
+      await Blacklist.addToBlacklist({
+        blacklistRequest: values,
+      });
+
+      return res.status(201).send();
+    } catch (error) {
+      if (!(error instanceof Error)) {
+        return;
+      }
+
+      if (error instanceof QueryFailedError) {
+        switch (error.driverError.errno) {
+          case 19:
+            return next({ status: 412, message: 'Item already blacklisted' });
+          default:
+            logger.warn('Something wrong with data blacklist', {
+              tmdbId: req.body.tmdbId,
+              mediaType: req.body.mediaType,
+              label: 'Blacklist',
+            });
+            return next({ status: 409, message: 'Something wrong' });
+        }
+      }
+
+      return next({ status: 500, message: error.message });
+    }
+  }
+);
+
+blacklistRoutes.delete(
+  '/:id',
+  isAuthenticated([Permission.MANAGE_BLACKLIST], {
+    type: 'or',
+  }),
+  async (req, res, next) => {
+    try {
+      const blacklisteRepository = getRepository(Blacklist);
+
+      const blacklistItem = await blacklisteRepository.findOneOrFail({
+        where: { tmdbId: Number(req.params.id) },
+      });
+
+      await blacklisteRepository.remove(blacklistItem);
+
+      const mediaRepository = getRepository(Media);
+
+      const mediaItem = await mediaRepository.findOneOrFail({
+        where: { tmdbId: Number(req.params.id) },
+      });
+
+      await mediaRepository.remove(mediaItem);
+
+      return res.status(204).send();
+    } catch (e) {
+      if (e instanceof NotFoundError) {
+        return next({
+          status: 401,
+          message: e.message,
+        });
+      }
+      return next({ status: 500, message: e.message });
+    }
+  }
+);
+
+export default blacklistRoutes;
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 12434256e..c7c8389e0 100644
--- a/server/routes/index.ts
+++ b/server/routes/index.ts
@@ -23,6 +23,7 @@ import restartFlag from '@server/utils/restartFlag';
 import { isPerson } from '@server/utils/typeHelpers';
 import { Router } from 'express';
 import authRoutes from './auth';
+import blacklistRoutes from './blacklist';
 import collectionRoutes from './collection';
 import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
 import issueRoutes from './issue';
@@ -144,6 +145,7 @@ router.use('/search', isAuthenticated(), searchRoutes);
 router.use('/discover', isAuthenticated(), discoverRoutes);
 router.use('/request', isAuthenticated(), requestRoutes);
 router.use('/watchlist', isAuthenticated(), watchlistRoutes);
+router.use('/blacklist', isAuthenticated(), blacklistRoutes);
 router.use('/movie', isAuthenticated(), movieRoutes);
 router.use('/tv', isAuthenticated(), tvRoutes);
 router.use('/media', isAuthenticated(), mediaRoutes);
diff --git a/server/routes/request.ts b/server/routes/request.ts
index 94ae8384a..320f149b5 100644
--- a/server/routes/request.ts
+++ b/server/routes/request.ts
@@ -8,6 +8,7 @@ import {
 import { getRepository } from '@server/datasource';
 import Media from '@server/entity/Media';
 import {
+  BlacklistedMediaError,
   DuplicateMediaRequestError,
   MediaRequest,
   NoSeasonsAvailableError,
@@ -243,6 +244,8 @@ requestRoutes.post<never, MediaRequest, MediaRequestBody>(
           return next({ status: 409, message: error.message });
         case NoSeasonsAvailableError:
           return next({ status: 202, message: error.message });
+        case BlacklistedMediaError:
+          return next({ status: 403, message: error.message });
         default:
           return next({ status: 500, message: error.message });
       }
diff --git a/src/components/Blacklist/index.tsx b/src/components/Blacklist/index.tsx
new file mode 100644
index 000000000..217f4cefd
--- /dev/null
+++ b/src/components/Blacklist/index.tsx
@@ -0,0 +1,417 @@
+import Badge from '@app/components/Common/Badge';
+import Button from '@app/components/Common/Button';
+import CachedImage from '@app/components/Common/CachedImage';
+import ConfirmButton from '@app/components/Common/ConfirmButton';
+import Header from '@app/components/Common/Header';
+import LoadingSpinner from '@app/components/Common/LoadingSpinner';
+import PageTitle from '@app/components/Common/PageTitle';
+import useDebouncedState from '@app/hooks/useDebouncedState';
+import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
+import { Permission, useUser } from '@app/hooks/useUser';
+import globalMessages from '@app/i18n/globalMessages';
+import Error from '@app/pages/_error';
+import defineMessages from '@app/utils/defineMessages';
+import {
+  ChevronLeftIcon,
+  ChevronRightIcon,
+  MagnifyingGlassIcon,
+  TrashIcon,
+} from '@heroicons/react/24/solid';
+import type {
+  BlacklistItem,
+  BlacklistResultsResponse,
+} from '@server/interfaces/api/blacklistInterfaces';
+import type { MovieDetails } from '@server/models/Movie';
+import type { TvDetails } from '@server/models/Tv';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import type { ChangeEvent } from 'react';
+import { useState } from 'react';
+import { useInView } from 'react-intersection-observer';
+import { FormattedRelativeTime, useIntl } from 'react-intl';
+import { useToasts } from 'react-toast-notifications';
+import useSWR from 'swr';
+
+const messages = defineMessages('components.Blacklist', {
+  blacklistsettings: 'Blacklist Settings',
+  blacklistSettingsDescription: 'Manage blacklisted media.',
+  mediaName: 'Name',
+  mediaType: 'Type',
+  mediaTmdbId: 'tmdb Id',
+  blacklistdate: 'date',
+  blacklistedby: '{date} by {user}',
+  blacklistNotFoundError: '<strong>{title}</strong> is not blacklisted.',
+});
+
+const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
+  return (movie as MovieDetails).title !== undefined;
+};
+
+const Blacklist = () => {
+  const [currentPageSize, setCurrentPageSize] = useState<number>(10);
+  const [searchFilter, debouncedSearchFilter, setSearchFilter] =
+    useDebouncedState('');
+  const router = useRouter();
+  const intl = useIntl();
+
+  const page = router.query.page ? Number(router.query.page) : 1;
+  const pageIndex = page - 1;
+  const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
+
+  const {
+    data,
+    error,
+    mutate: revalidate,
+  } = useSWR<BlacklistResultsResponse>(
+    `/api/v1/blacklist/?take=${currentPageSize}
+    &skip=${pageIndex * currentPageSize}
+    ${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`,
+    {
+      refreshInterval: 0,
+      revalidateOnFocus: false,
+    }
+  );
+
+  // check if there's no data and no errors in the table
+  // so as to show a spinner inside the table and not refresh the whole component
+  if (!data && error) {
+    return <Error statusCode={500} />;
+  }
+
+  const searchItem = (e: ChangeEvent<HTMLInputElement>) => {
+    // Remove the "page" query param from the URL
+    // so that the "skip" query param on line 62 is empty
+    // and the search returns results without skipping items
+    if (router.query.page) router.replace(router.basePath);
+
+    setSearchFilter(e.target.value as string);
+  };
+
+  const hasNextPage = data && data.pageInfo.pages > pageIndex + 1;
+  const hasPrevPage = pageIndex > 0;
+
+  return (
+    <>
+      <PageTitle title={[intl.formatMessage(globalMessages.blacklist)]} />
+      <Header>{intl.formatMessage(globalMessages.blacklist)}</Header>
+
+      <div className="mt-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
+        <div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
+          <span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
+            <MagnifyingGlassIcon className="h-6 w-6" />
+          </span>
+          <input
+            type="text"
+            className="rounded-r-only"
+            value={searchFilter}
+            onChange={(e) => searchItem(e)}
+          />
+        </div>
+      </div>
+
+      {!data ? (
+        <LoadingSpinner />
+      ) : data.results.length === 0 ? (
+        <div className="flex w-full flex-col items-center justify-center py-24 text-white">
+          <span className="text-2xl text-gray-400">
+            {intl.formatMessage(globalMessages.noresults)}
+          </span>
+        </div>
+      ) : (
+        data.results.map((item: BlacklistItem) => {
+          return (
+            <div className="py-2" key={`request-list-${item.tmdbId}`}>
+              <BlacklistedItem item={item} revalidateList={revalidate} />
+            </div>
+          );
+        })
+      )}
+
+      <div className="actions">
+        <nav
+          className="mb-3 flex flex-col items-center space-y-3 sm:flex-row sm:space-y-0"
+          aria-label="Pagination"
+        >
+          <div className="hidden lg:flex lg:flex-1">
+            <p className="text-sm">
+              {data &&
+                (data?.results.length ?? 0) > 0 &&
+                intl.formatMessage(globalMessages.showingresults, {
+                  from: pageIndex * currentPageSize + 1,
+                  to:
+                    data.results.length < currentPageSize
+                      ? pageIndex * currentPageSize + data.results.length
+                      : (pageIndex + 1) * currentPageSize,
+                  total: data.pageInfo.results,
+                  strong: (msg: React.ReactNode) => (
+                    <span className="font-medium">{msg}</span>
+                  ),
+                })}
+            </p>
+          </div>
+          <div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
+            <span className="-mt-3 items-center truncate text-sm sm:mt-0">
+              {intl.formatMessage(globalMessages.resultsperpage, {
+                pageSize: (
+                  <select
+                    id="pageSize"
+                    name="pageSize"
+                    onChange={(e) => {
+                      setCurrentPageSize(Number(e.target.value));
+                      router
+                        .push({
+                          pathname: router.pathname,
+                          query: router.query.userId
+                            ? { userId: router.query.userId }
+                            : {},
+                        })
+                        .then(() => window.scrollTo(0, 0));
+                    }}
+                    value={currentPageSize}
+                    className="short inline"
+                  >
+                    <option value="5">5</option>
+                    <option value="10">10</option>
+                    <option value="25">25</option>
+                    <option value="50">50</option>
+                    <option value="100">100</option>
+                  </select>
+                ),
+              })}
+            </span>
+          </div>
+          <div className="flex flex-auto justify-center space-x-2 sm:flex-1 sm:justify-end">
+            <Button
+              disabled={!hasPrevPage}
+              onClick={() => updateQueryParams('page', (page - 1).toString())}
+            >
+              <ChevronLeftIcon />
+              <span>{intl.formatMessage(globalMessages.previous)}</span>
+            </Button>
+            <Button
+              disabled={!hasNextPage}
+              onClick={() => updateQueryParams('page', (page + 1).toString())}
+            >
+              <span>{intl.formatMessage(globalMessages.next)}</span>
+              <ChevronRightIcon />
+            </Button>
+          </div>
+        </nav>
+      </div>
+    </>
+  );
+};
+
+export default Blacklist;
+
+interface BlacklistedItemProps {
+  item: BlacklistItem;
+  revalidateList: () => void;
+}
+
+const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
+  const [isUpdating, setIsUpdating] = useState<boolean>(false);
+  const { addToast } = useToasts();
+  const { ref, inView } = useInView({
+    triggerOnce: true,
+  });
+  const intl = useIntl();
+  const { hasPermission } = useUser();
+
+  const url =
+    item.mediaType === 'movie'
+      ? `/api/v1/movie/${item.tmdbId}`
+      : `/api/v1/tv/${item.tmdbId}`;
+  const { data: title, error } = useSWR<MovieDetails | TvDetails>(
+    inView ? url : null
+  );
+
+  if (!title && !error) {
+    return (
+      <div
+        className="h-64 w-full animate-pulse rounded-xl bg-gray-800 xl:h-28"
+        ref={ref}
+      />
+    );
+  }
+
+  const removeFromBlacklist = async (tmdbId: number, title?: string) => {
+    setIsUpdating(true);
+
+    const res = await fetch('/api/v1/blacklist/' + tmdbId, {
+      method: 'DELETE',
+    });
+
+    if (res.status === 204) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
+            title,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'success', autoDismiss: true }
+      );
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+
+    revalidateList();
+    setIsUpdating(false);
+  };
+
+  return (
+    <div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
+      {title && title.backdropPath && (
+        <div className="absolute inset-0 z-0 w-full bg-cover bg-center xl:w-2/3">
+          <CachedImage
+            src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
+            alt=""
+            style={{ width: '100%', height: '100%', objectFit: 'cover' }}
+            fill
+          />
+          <div
+            className="absolute inset-0"
+            style={{
+              backgroundImage:
+                'linear-gradient(90deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 1) 100%)',
+            }}
+          />
+        </div>
+      )}
+      <div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
+        <div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
+          <Link
+            href={
+              item.mediaType === 'movie'
+                ? `/movie/${item.tmdbId}`
+                : `/tv/${item.tmdbId}`
+            }
+            className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
+          >
+            <CachedImage
+              src={
+                title?.posterPath
+                  ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
+                  : '/images/overseerr_poster_not_found.png'
+              }
+              alt=""
+              sizes="100vw"
+              style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
+              width={600}
+              height={900}
+            />
+          </Link>
+          <div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
+            <div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
+              {title &&
+                (isMovie(title)
+                  ? title.releaseDate
+                  : title.firstAirDate
+                )?.slice(0, 4)}
+            </div>
+            <Link
+              href={
+                item.mediaType === 'movie'
+                  ? `/movie/${item.tmdbId}`
+                  : `/tv/${item.tmdbId}`
+              }
+            >
+              <span className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
+                {title && (isMovie(title) ? title.title : title.name)}
+              </span>
+            </Link>
+          </div>
+        </div>
+
+        <div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
+          <div className="card-field">
+            <span className="card-field-name">Status</span>
+            <Badge badgeType="danger">
+              {intl.formatMessage(globalMessages.blacklisted)}
+            </Badge>
+          </div>
+
+          {item.createdAt && (
+            <div className="card-field">
+              <span className="card-field-name">
+                {intl.formatMessage(globalMessages.blacklisted)}
+              </span>
+              <span className="flex truncate text-sm text-gray-300">
+                {intl.formatMessage(messages.blacklistedby, {
+                  date: (
+                    <FormattedRelativeTime
+                      value={Math.floor(
+                        (new Date(item.createdAt).getTime() - Date.now()) / 1000
+                      )}
+                      updateIntervalInSeconds={1}
+                      numeric="auto"
+                    />
+                  ),
+                  user: (
+                    <Link href={`/users/${item.user.id}`}>
+                      <span className="group flex items-center truncate">
+                        <CachedImage
+                          src={item.user.avatar}
+                          alt=""
+                          className="avatar-sm ml-1.5"
+                          width={20}
+                          height={20}
+                          style={{ objectFit: 'cover' }}
+                        />
+                        <span className="ml-1 truncate text-sm font-semibold group-hover:text-white group-hover:underline">
+                          {item.user.displayName}
+                        </span>
+                      </span>
+                    </Link>
+                  ),
+                })}
+              </span>
+            </div>
+          )}
+          <div className="card-field">
+            {item.mediaType === 'movie' ? (
+              <div className="pointer-events-none z-40 self-start rounded-full border border-blue-500 bg-blue-600 bg-opacity-80 shadow-md">
+                <div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
+                  {intl.formatMessage(globalMessages.movie)}
+                </div>
+              </div>
+            ) : (
+              <div className="pointer-events-none z-40 self-start rounded-full border border-purple-600 bg-purple-600 bg-opacity-80 shadow-md">
+                <div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
+                  {intl.formatMessage(globalMessages.tvshow)}
+                </div>
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+      <div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
+        {hasPermission(Permission.MANAGE_BLACKLIST) && (
+          <ConfirmButton
+            onClick={() =>
+              removeFromBlacklist(
+                item.tmdbId,
+                title && (isMovie(title) ? title.title : title.name)
+              )
+            }
+            confirmText={intl.formatMessage(
+              isUpdating ? globalMessages.deleting : globalMessages.areyousure
+            )}
+            className={`w-full ${
+              isUpdating ? 'pointer-events-none opacity-50' : ''
+            }`}
+          >
+            <TrashIcon />
+            <span>
+              {intl.formatMessage(globalMessages.removefromBlacklist)}
+            </span>
+          </ConfirmButton>
+        )}
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/BlacklistBlock/index.tsx b/src/components/BlacklistBlock/index.tsx
new file mode 100644
index 000000000..0908d3735
--- /dev/null
+++ b/src/components/BlacklistBlock/index.tsx
@@ -0,0 +1,129 @@
+import Badge from '@app/components/Common/Badge';
+import Button from '@app/components/Common/Button';
+import Tooltip from '@app/components/Common/Tooltip';
+import { useUser } from '@app/hooks/useUser';
+import globalMessages from '@app/i18n/globalMessages';
+import defineMessages from '@app/utils/defineMessages';
+import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid';
+import type { Blacklist } from '@server/entity/Blacklist';
+import Link from 'next/link';
+import { useState } from 'react';
+import { useIntl } from 'react-intl';
+import { useToasts } from 'react-toast-notifications';
+
+const messages = defineMessages('component.BlacklistBlock', {
+  blacklistedby: 'Blacklisted By',
+  blacklistdate: 'Blacklisted date',
+});
+
+interface BlacklistBlockProps {
+  blacklistItem: Blacklist;
+  onUpdate?: () => void;
+  onDelete?: () => void;
+}
+
+const BlacklistBlock = ({
+  blacklistItem,
+  onUpdate,
+  onDelete,
+}: BlacklistBlockProps) => {
+  const { user } = useUser();
+  const intl = useIntl();
+  const [isUpdating, setIsUpdating] = useState(false);
+  const { addToast } = useToasts();
+
+  const removeFromBlacklist = async (tmdbId: number, title?: string) => {
+    setIsUpdating(true);
+
+    const res = await fetch('/api/v1/blacklist/' + tmdbId, {
+      method: 'DELETE',
+    });
+
+    if (res.status === 204) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
+            title,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'success', autoDismiss: true }
+      );
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+
+    onUpdate && onUpdate();
+    onDelete && onDelete();
+
+    setIsUpdating(false);
+  };
+
+  return (
+    <div className="px-4 py-3 text-gray-300">
+      <div className="flex items-center justify-between">
+        <div className="mr-6 min-w-0 flex-1 flex-col items-center text-sm leading-5">
+          <div className="white mb-1 flex flex-nowrap">
+            <Tooltip content={intl.formatMessage(messages.blacklistedby)}>
+              <UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
+            </Tooltip>
+            <span className="w-40 truncate md:w-auto">
+              <Link
+                href={
+                  blacklistItem.user.id === user?.id
+                    ? '/profile'
+                    : `/users/${blacklistItem.user.id}`
+                }
+              >
+                <span className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
+                  {blacklistItem.user.displayName}
+                </span>
+              </Link>
+            </span>
+          </div>
+        </div>
+        <div className="ml-2 flex flex-shrink-0 flex-wrap">
+          <Tooltip
+            content={intl.formatMessage(globalMessages.removefromBlacklist)}
+          >
+            <Button
+              buttonType="danger"
+              onClick={() =>
+                removeFromBlacklist(blacklistItem.tmdbId, blacklistItem.title)
+              }
+              disabled={isUpdating}
+            >
+              <TrashIcon className="icon-sm" />
+            </Button>
+          </Tooltip>
+        </div>
+      </div>
+      <div className="mt-2 sm:flex sm:justify-between">
+        <div className="sm:flex">
+          <div className="mr-6 flex items-center text-sm leading-5">
+            <Badge badgeType="danger">
+              {intl.formatMessage(globalMessages.blacklisted)}
+            </Badge>
+          </div>
+        </div>
+        <div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
+          <Tooltip content={intl.formatMessage(messages.blacklistdate)}>
+            <CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
+          </Tooltip>
+          <span>
+            {intl.formatDate(blacklistItem.createdAt, {
+              year: 'numeric',
+              month: 'long',
+              day: 'numeric',
+            })}
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default BlacklistBlock;
diff --git a/src/components/BlacklistModal/index.tsx b/src/components/BlacklistModal/index.tsx
new file mode 100644
index 000000000..aeca8d411
--- /dev/null
+++ b/src/components/BlacklistModal/index.tsx
@@ -0,0 +1,79 @@
+import Modal from '@app/components/Common/Modal';
+import globalMessages from '@app/i18n/globalMessages';
+import defineMessages from '@app/utils/defineMessages';
+import { Transition } from '@headlessui/react';
+import type { MovieDetails } from '@server/models/Movie';
+import type { TvDetails } from '@server/models/Tv';
+import { useIntl } from 'react-intl';
+import useSWR from 'swr';
+
+interface BlacklistModalProps {
+  tmdbId: number;
+  type: 'movie' | 'tv' | 'collection';
+  show: boolean;
+  onComplete?: () => void;
+  onCancel?: () => void;
+  isUpdating?: boolean;
+}
+
+const messages = defineMessages('component.BlacklistModal', {
+  blacklisting: 'Blacklisting',
+});
+
+const isMovie = (
+  movie: MovieDetails | TvDetails | undefined
+): movie is MovieDetails => {
+  if (!movie) return false;
+  return (movie as MovieDetails).title !== undefined;
+};
+
+const BlacklistModal = ({
+  tmdbId,
+  type,
+  show,
+  onComplete,
+  onCancel,
+  isUpdating,
+}: BlacklistModalProps) => {
+  const intl = useIntl();
+
+  const { data, error } = useSWR<TvDetails | MovieDetails>(
+    `/api/v1/${type}/${tmdbId}`
+  );
+
+  return (
+    <Transition
+      as="div"
+      enter="transition-opacity duration-300"
+      enterFrom="opacity-0"
+      enterTo="opacity-100"
+      leave="transition-opacity duration-300"
+      leaveFrom="opacity-100"
+      leaveTo="opacity-0"
+      show={show}
+    >
+      <Modal
+        loading={!data && !error}
+        backgroundClickable
+        title={`${intl.formatMessage(globalMessages.blacklist)} ${
+          isMovie(data)
+            ? intl.formatMessage(globalMessages.movie)
+            : intl.formatMessage(globalMessages.tvshow)
+        }`}
+        subTitle={`${isMovie(data) ? data.title : data?.name}`}
+        onCancel={onCancel}
+        onOk={onComplete}
+        okText={
+          isUpdating
+            ? intl.formatMessage(messages.blacklisting)
+            : intl.formatMessage(globalMessages.blacklist)
+        }
+        okButtonType="danger"
+        okDisabled={isUpdating}
+        backdrop={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data?.backdropPath}`}
+      />
+    </Transition>
+  );
+};
+
+export default BlacklistModal;
diff --git a/src/components/CollectionDetails/index.tsx b/src/components/CollectionDetails/index.tsx
index 7afa28e4e..9e8ab32ad 100644
--- a/src/components/CollectionDetails/index.tsx
+++ b/src/components/CollectionDetails/index.tsx
@@ -183,6 +183,11 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
     );
   }
 
+  const blacklistVisibility = hasPermission(
+    [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
+    { type: 'or' }
+  );
+
   return (
     <div
       className="media-page"
@@ -335,20 +340,26 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
         sliderKey="collection-movies"
         isLoading={false}
         isEmpty={data.parts.length === 0}
-        items={data.parts.map((title) => (
-          <TitleCard
-            key={`collection-movie-${title.id}`}
-            id={title.id}
-            isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
-            image={title.posterPath}
-            status={title.mediaInfo?.status}
-            summary={title.overview}
-            title={title.title}
-            userScore={title.voteAverage}
-            year={title.releaseDate}
-            mediaType={title.mediaType}
-          />
-        ))}
+        items={data.parts
+          .filter((title) => {
+            if (!blacklistVisibility)
+              return title.mediaInfo?.status !== MediaStatus.BLACKLISTED;
+            return title;
+          })
+          .map((title) => (
+            <TitleCard
+              key={`collection-movie-${title.id}`}
+              id={title.id}
+              isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
+              image={title.posterPath}
+              status={title.mediaInfo?.status}
+              summary={title.overview}
+              title={title.title}
+              userScore={title.voteAverage}
+              year={title.releaseDate}
+              mediaType={title.mediaType}
+            />
+          ))}
       />
       <div className="extra-bottom-space relative" />
     </div>
diff --git a/src/components/Common/ListView/index.tsx b/src/components/Common/ListView/index.tsx
index 46c946ae2..f1c3bf66c 100644
--- a/src/components/Common/ListView/index.tsx
+++ b/src/components/Common/ListView/index.tsx
@@ -1,8 +1,10 @@
 import PersonCard from '@app/components/PersonCard';
 import TitleCard from '@app/components/TitleCard';
 import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
+import { Permission, useUser } from '@app/hooks/useUser';
 import useVerticalScroll from '@app/hooks/useVerticalScroll';
 import globalMessages from '@app/i18n/globalMessages';
+import { MediaStatus } from '@server/constants/media';
 import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
 import type {
   CollectionResult,
@@ -32,7 +34,14 @@ const ListView = ({
   mutateParent,
 }: ListViewProps) => {
   const intl = useIntl();
+  const { hasPermission } = useUser();
   useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
+
+  const blacklistVisibility = hasPermission(
+    [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
+    { type: 'or' }
+  );
+
   return (
     <>
       {isEmpty && (
@@ -55,76 +64,89 @@ const ListView = ({
             </li>
           );
         })}
-        {items?.map((title, index) => {
-          let titleCard: React.ReactNode;
-
-          switch (title.mediaType) {
-            case 'movie':
-              titleCard = (
-                <TitleCard
-                  key={title.id}
-                  id={title.id}
-                  isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
-                  image={title.posterPath}
-                  status={title.mediaInfo?.status}
-                  summary={title.overview}
-                  title={title.title}
-                  userScore={title.voteAverage}
-                  year={title.releaseDate}
-                  mediaType={title.mediaType}
-                  inProgress={
-                    (title.mediaInfo?.downloadStatus ?? []).length > 0
-                  }
-                  canExpand
-                />
-              );
-              break;
-            case 'tv':
-              titleCard = (
-                <TitleCard
-                  key={title.id}
-                  id={title.id}
-                  isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
-                  image={title.posterPath}
-                  status={title.mediaInfo?.status}
-                  summary={title.overview}
-                  title={title.name}
-                  userScore={title.voteAverage}
-                  year={title.firstAirDate}
-                  mediaType={title.mediaType}
-                  inProgress={
-                    (title.mediaInfo?.downloadStatus ?? []).length > 0
-                  }
-                  canExpand
-                />
+        {items
+          ?.filter((title) => {
+            if (!blacklistVisibility)
+              return (
+                (title as TvResult | MovieResult).mediaInfo?.status !==
+                MediaStatus.BLACKLISTED
               );
-              break;
-            case 'collection':
-              titleCard = (
-                <TitleCard
-                  id={title.id}
-                  image={title.posterPath}
-                  summary={title.overview}
-                  title={title.title}
-                  mediaType={title.mediaType}
-                  canExpand
-                />
-              );
-              break;
-            case 'person':
-              titleCard = (
-                <PersonCard
-                  personId={title.id}
-                  name={title.name}
-                  profilePath={title.profilePath}
-                  canExpand
-                />
-              );
-              break;
-          }
+            return title;
+          })
+          .map((title, index) => {
+            let titleCard: React.ReactNode;
 
-          return <li key={`${title.id}-${index}`}>{titleCard}</li>;
-        })}
+            switch (title.mediaType) {
+              case 'movie':
+                titleCard = (
+                  <TitleCard
+                    key={title.id}
+                    id={title.id}
+                    isAddedToWatchlist={
+                      title.mediaInfo?.watchlists?.length ?? 0
+                    }
+                    image={title.posterPath}
+                    status={title.mediaInfo?.status}
+                    summary={title.overview}
+                    title={title.title}
+                    userScore={title.voteAverage}
+                    year={title.releaseDate}
+                    mediaType={title.mediaType}
+                    inProgress={
+                      (title.mediaInfo?.downloadStatus ?? []).length > 0
+                    }
+                    canExpand
+                  />
+                );
+                break;
+              case 'tv':
+                titleCard = (
+                  <TitleCard
+                    key={title.id}
+                    id={title.id}
+                    isAddedToWatchlist={
+                      title.mediaInfo?.watchlists?.length ?? 0
+                    }
+                    image={title.posterPath}
+                    status={title.mediaInfo?.status}
+                    summary={title.overview}
+                    title={title.name}
+                    userScore={title.voteAverage}
+                    year={title.firstAirDate}
+                    mediaType={title.mediaType}
+                    inProgress={
+                      (title.mediaInfo?.downloadStatus ?? []).length > 0
+                    }
+                    canExpand
+                  />
+                );
+                break;
+              case 'collection':
+                titleCard = (
+                  <TitleCard
+                    id={title.id}
+                    image={title.posterPath}
+                    summary={title.overview}
+                    title={title.title}
+                    mediaType={title.mediaType}
+                    canExpand
+                  />
+                );
+                break;
+              case 'person':
+                titleCard = (
+                  <PersonCard
+                    personId={title.id}
+                    name={title.name}
+                    profilePath={title.profilePath}
+                    canExpand
+                  />
+                );
+                break;
+            }
+
+            return <li key={`${title.id}-${index}`}>{titleCard}</li>;
+          })}
         {isLoading &&
           !isReachingEnd &&
           [...Array(20)].map((_item, i) => (
diff --git a/src/components/Common/StatusBadgeMini/index.tsx b/src/components/Common/StatusBadgeMini/index.tsx
index a7e24a378..afcd72bfc 100644
--- a/src/components/Common/StatusBadgeMini/index.tsx
+++ b/src/components/Common/StatusBadgeMini/index.tsx
@@ -1,6 +1,11 @@
 import Spinner from '@app/assets/spinner.svg';
 import { CheckCircleIcon } from '@heroicons/react/20/solid';
-import { BellIcon, ClockIcon, MinusSmallIcon } from '@heroicons/react/24/solid';
+import {
+  BellIcon,
+  ClockIcon,
+  EyeSlashIcon,
+  MinusSmallIcon,
+} from '@heroicons/react/24/solid';
 import { MediaStatus } from '@server/constants/media';
 
 interface StatusBadgeMiniProps {
@@ -44,6 +49,10 @@ const StatusBadgeMini = ({
       );
       indicatorIcon = <BellIcon />;
       break;
+    case MediaStatus.BLACKLISTED:
+      badgeStyle.push('bg-red-500 border-white-400 ring-white-400 text-white');
+      indicatorIcon = <EyeSlashIcon />;
+      break;
     case MediaStatus.PARTIALLY_AVAILABLE:
       badgeStyle.push(
         'bg-green-500 border-green-400 ring-green-400 text-green-100'
diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx
index d9b7d3fbc..a947e2626 100644
--- a/src/components/Layout/Sidebar/index.tsx
+++ b/src/components/Layout/Sidebar/index.tsx
@@ -8,6 +8,7 @@ import {
   ClockIcon,
   CogIcon,
   ExclamationTriangleIcon,
+  EyeSlashIcon,
   FilmIcon,
   SparklesIcon,
   TvIcon,
@@ -25,6 +26,7 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
   browsemovies: 'Movies',
   browsetv: 'Series',
   requests: 'Requests',
+  blacklist: 'Blacklist',
   issues: 'Issues',
   users: 'Users',
   settings: 'Settings',
@@ -71,6 +73,17 @@ const SidebarLinks: SidebarLinkProps[] = [
     svgIcon: <ClockIcon className="mr-3 h-6 w-6" />,
     activeRegExp: /^\/requests/,
   },
+  {
+    href: '/blacklist',
+    messagesKey: 'blacklist',
+    svgIcon: <EyeSlashIcon className="mr-3 h-6 w-6" />,
+    activeRegExp: /^\/blacklist/,
+    requiredPermission: [
+      Permission.MANAGE_BLACKLIST,
+      Permission.VIEW_BLACKLIST,
+    ],
+    permissionType: 'or',
+  },
   {
     href: '/issues',
     messagesKey: 'issues',
diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx
index b669ebb43..0f96aa202 100644
--- a/src/components/ManageSlideOver/index.tsx
+++ b/src/components/ManageSlideOver/index.tsx
@@ -1,3 +1,4 @@
+import BlacklistBlock from '@app/components/BlacklistBlock';
 import Button from '@app/components/Common/Button';
 import ConfirmButton from '@app/components/Common/ConfirmButton';
 import SlideOver from '@app/components/Common/SlideOver';
@@ -284,6 +285,20 @@ const ManageSlideOver = ({
             </div>
           </div>
         )}
+        {data.mediaInfo?.status === MediaStatus.BLACKLISTED && (
+          <div>
+            <h3 className="mb-2 text-xl font-bold">
+              {intl.formatMessage(globalMessages.blacklist)}
+            </h3>
+            <div className="overflow-hidden rounded-md border border-gray-700 shadow">
+              <BlacklistBlock
+                blacklistItem={data.mediaInfo.blacklist}
+                onUpdate={() => revalidate()}
+                onDelete={() => onClose()}
+              />
+            </div>
+          </div>
+        )}
         {hasPermission(Permission.ADMIN) &&
           (data.mediaInfo?.serviceUrl ||
             data.mediaInfo?.tautulliUrl ||
@@ -603,32 +618,17 @@ const ManageSlideOver = ({
               </div>
             </div>
           )}
-        {hasPermission(Permission.ADMIN) && data?.mediaInfo && (
-          <div>
-            <h3 className="mb-2 text-xl font-bold">
-              {intl.formatMessage(messages.manageModalAdvanced)}
-            </h3>
-            <div className="space-y-2">
-              {data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
-                <Button
-                  onClick={() => markAvailable()}
-                  className="w-full"
-                  buttonType="success"
-                >
-                  <CheckCircleIcon />
-                  <span>
-                    {intl.formatMessage(
-                      mediaType === 'movie'
-                        ? messages.markavailable
-                        : messages.markallseasonsavailable
-                    )}
-                  </span>
-                </Button>
-              )}
-              {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE &&
-                settings.currentSettings.series4kEnabled && (
+        {hasPermission(Permission.ADMIN) &&
+          data?.mediaInfo &&
+          data.mediaInfo.status !== MediaStatus.BLACKLISTED && (
+            <div>
+              <h3 className="mb-2 text-xl font-bold">
+                {intl.formatMessage(messages.manageModalAdvanced)}
+              </h3>
+              <div className="space-y-2">
+                {data?.mediaInfo.status !== MediaStatus.AVAILABLE && (
                   <Button
-                    onClick={() => markAvailable(true)}
+                    onClick={() => markAvailable()}
                     className="w-full"
                     buttonType="success"
                   >
@@ -636,42 +636,59 @@ const ManageSlideOver = ({
                     <span>
                       {intl.formatMessage(
                         mediaType === 'movie'
-                          ? messages.mark4kavailable
-                          : messages.markallseasons4kavailable
+                          ? messages.markavailable
+                          : messages.markallseasonsavailable
                       )}
                     </span>
                   </Button>
                 )}
-              <div>
-                <ConfirmButton
-                  onClick={() => deleteMedia()}
-                  confirmText={intl.formatMessage(globalMessages.areyousure)}
-                  className="w-full"
-                >
-                  <DocumentMinusIcon />
-                  <span>
-                    {intl.formatMessage(messages.manageModalClearMedia)}
-                  </span>
-                </ConfirmButton>
-                <div className="mt-2 text-xs text-gray-400">
-                  {intl.formatMessage(messages.manageModalClearMediaWarning, {
-                    mediaType: intl.formatMessage(
-                      mediaType === 'movie' ? messages.movie : messages.tvshow
-                    ),
-                    mediaServerName:
-                      settings.currentSettings.mediaServerType ===
-                      MediaServerType.EMBY
-                        ? 'Emby'
-                        : settings.currentSettings.mediaServerType ===
-                          MediaServerType.PLEX
-                        ? 'Plex'
-                        : 'Jellyfin',
-                  })}
+                {data?.mediaInfo.status4k !== MediaStatus.AVAILABLE &&
+                  settings.currentSettings.series4kEnabled && (
+                    <Button
+                      onClick={() => markAvailable(true)}
+                      className="w-full"
+                      buttonType="success"
+                    >
+                      <CheckCircleIcon />
+                      <span>
+                        {intl.formatMessage(
+                          mediaType === 'movie'
+                            ? messages.mark4kavailable
+                            : messages.markallseasons4kavailable
+                        )}
+                      </span>
+                    </Button>
+                  )}
+                <div>
+                  <ConfirmButton
+                    onClick={() => deleteMedia()}
+                    confirmText={intl.formatMessage(globalMessages.areyousure)}
+                    className="w-full"
+                  >
+                    <DocumentMinusIcon />
+                    <span>
+                      {intl.formatMessage(messages.manageModalClearMedia)}
+                    </span>
+                  </ConfirmButton>
+                  <div className="mt-2 text-xs text-gray-400">
+                    {intl.formatMessage(messages.manageModalClearMediaWarning, {
+                      mediaType: intl.formatMessage(
+                        mediaType === 'movie' ? messages.movie : messages.tvshow
+                      ),
+                      mediaServerName:
+                        settings.currentSettings.mediaServerType ===
+                        MediaServerType.EMBY
+                          ? 'Emby'
+                          : settings.currentSettings.mediaServerType ===
+                            MediaServerType.PLEX
+                          ? 'Plex'
+                          : 'Jellyfin',
+                    })}
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-        )}
+          )}
       </div>
     </SlideOver>
   );
diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx
index 56e0afc80..006f0df92 100644
--- a/src/components/MediaSlider/index.tsx
+++ b/src/components/MediaSlider/index.tsx
@@ -3,8 +3,10 @@ import PersonCard from '@app/components/PersonCard';
 import Slider from '@app/components/Slider';
 import TitleCard from '@app/components/TitleCard';
 import useSettings from '@app/hooks/useSettings';
+import { useUser } from '@app/hooks/useUser';
 import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
 import { MediaStatus } from '@server/constants/media';
+import { Permission } from '@server/lib/permissions';
 import type {
   MovieResult,
   PersonResult,
@@ -41,6 +43,7 @@ const MediaSlider = ({
   onNewTitles,
 }: MediaSliderProps) => {
   const settings = useSettings();
+  const { hasPermission } = useUser();
   const { data, error, setSize, size } = useSWRInfinite<MixedResult>(
     (pageIndex: number, previousPageData: MixedResult | null) => {
       if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
@@ -90,50 +93,65 @@ const MediaSlider = ({
     return null;
   }
 
-  const finalTitles = titles.slice(0, 20).map((title) => {
-    switch (title.mediaType) {
-      case 'movie':
-        return (
-          <TitleCard
-            key={title.id}
-            id={title.id}
-            isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
-            image={title.posterPath}
-            status={title.mediaInfo?.status}
-            summary={title.overview}
-            title={title.title}
-            userScore={title.voteAverage}
-            year={title.releaseDate}
-            mediaType={title.mediaType}
-            inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
-          />
-        );
-      case 'tv':
-        return (
-          <TitleCard
-            key={title.id}
-            id={title.id}
-            isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
-            image={title.posterPath}
-            status={title.mediaInfo?.status}
-            summary={title.overview}
-            title={title.name}
-            userScore={title.voteAverage}
-            year={title.firstAirDate}
-            mediaType={title.mediaType}
-            inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
-          />
-        );
-      case 'person':
+  const blacklistVisibility = hasPermission(
+    [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
+    { type: 'or' }
+  );
+
+  const finalTitles = titles
+    .slice(0, 20)
+    .filter((title) => {
+      if (!blacklistVisibility)
         return (
-          <PersonCard
-            personId={title.id}
-            name={title.name}
-            profilePath={title.profilePath}
-          />
+          (title as TvResult | MovieResult).mediaInfo?.status !==
+          MediaStatus.BLACKLISTED
         );
-    }
-  });
+      return title;
+    })
+    .map((title) => {
+      switch (title.mediaType) {
+        case 'movie':
+          return (
+            <TitleCard
+              key={title.id}
+              id={title.id}
+              isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
+              image={title.posterPath}
+              status={title.mediaInfo?.status}
+              summary={title.overview}
+              title={title.title}
+              userScore={title.voteAverage}
+              year={title.releaseDate}
+              mediaType={title.mediaType}
+              inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
+            />
+          );
+        case 'tv':
+          return (
+            <TitleCard
+              key={title.id}
+              id={title.id}
+              isAddedToWatchlist={title.mediaInfo?.watchlists?.length ?? 0}
+              image={title.posterPath}
+              status={title.mediaInfo?.status}
+              summary={title.overview}
+              title={title.name}
+              userScore={title.voteAverage}
+              year={title.firstAirDate}
+              mediaType={title.mediaType}
+              inProgress={(title.mediaInfo?.downloadStatus ?? []).length > 0}
+            />
+          );
+        case 'person':
+          return (
+            <PersonCard
+              personId={title.id}
+              name={title.name}
+              profilePath={title.profilePath}
+            />
+          );
+      }
+    });
 
   if (linkUrl && titles.length > 20) {
     finalTitles.push(
diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx
index e4bc991ef..c6583e3df 100644
--- a/src/components/MovieDetails/index.tsx
+++ b/src/components/MovieDetails/index.tsx
@@ -5,6 +5,7 @@ import RTRotten from '@app/assets/rt_rotten.svg';
 import ImdbLogo from '@app/assets/services/imdb.svg';
 import Spinner from '@app/assets/spinner.svg';
 import TmdbLogo from '@app/assets/tmdb_logo.svg';
+import BlacklistModal from '@app/components/BlacklistModal';
 import Button from '@app/components/Common/Button';
 import CachedImage from '@app/components/Common/CachedImage';
 import LoadingSpinner from '@app/components/Common/LoadingSpinner';
@@ -35,6 +36,7 @@ import {
   CloudIcon,
   CogIcon,
   ExclamationTriangleIcon,
+  EyeSlashIcon,
   FilmIcon,
   PlayIcon,
   TicketIcon,
@@ -55,7 +57,7 @@ import 'country-flag-icons/3x2/flags.css';
 import { uniqBy } from 'lodash';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useIntl } from 'react-intl';
 import { useToasts } from 'react-toast-notifications';
 import useSWR from 'swr';
@@ -125,6 +127,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
   const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
     !movie?.onUserWatchlist
   );
+  const [isBlacklistUpdating, setIsBlacklistUpdating] =
+    useState<boolean>(false);
+  const [showBlacklistModal, setShowBlacklistModal] = useState(false);
   const { addToast } = useToasts();
 
   const {
@@ -155,6 +160,11 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
     setShowManager(router.query.manage == '1' ? true : false);
   }, [router.query.manage]);
 
+  const closeBlacklistModal = useCallback(
+    () => setShowBlacklistModal(false),
+    []
+  );
+
   const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
     mediaUrl: data?.mediaInfo?.mediaUrl,
     mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
@@ -374,6 +384,60 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
     }
   };
 
+  const onClickHideItemBtn = async (): Promise<void> => {
+    setIsBlacklistUpdating(true);
+
+    const res = await fetch('/api/v1/blacklist', {
+      method: 'POST',
+      headers: {
+        Accept: 'application/json',
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        tmdbId: movie?.id,
+        mediaType: 'movie',
+        title: movie?.title,
+        user: user?.id,
+      }),
+    });
+
+    if (res.status === 201) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.blacklistSuccess, {
+            title: movie?.title,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'success', autoDismiss: true }
+      );
+
+      revalidate();
+    } else if (res.status === 412) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.blacklistDuplicateError, {
+            title: movie?.title,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'info', autoDismiss: true }
+      );
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+
+    setIsBlacklistUpdating(false);
+    closeBlacklistModal();
+  };
+
+  const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
+    type: 'or',
+  });
+
   return (
     <div
       className="media-page"
@@ -419,6 +483,14 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
         revalidate={() => revalidate()}
         show={showManager}
       />
+      <BlacklistModal
+        tmdbId={data.id}
+        type="movie"
+        show={showBlacklistModal}
+        onCancel={closeBlacklistModal}
+        onComplete={onClickHideItemBtn}
+        isUpdating={isBlacklistUpdating}
+      />
       <div className="media-header">
         <div className="media-poster">
           <CachedImage
@@ -495,40 +567,61 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
           </span>
         </div>
         <div className="media-actions">
-          <>
-            {toggleWatchlist ? (
-              <Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
-                <Button
-                  buttonType={'ghost'}
-                  className="z-40 mr-2"
-                  buttonSize={'md'}
-                  onClick={onClickWatchlistBtn}
-                >
-                  {isUpdating ? (
-                    <Spinner className="h-3" />
-                  ) : (
-                    <StarIcon className={'h-3 text-amber-300'} />
-                  )}
-                </Button>
-              </Tooltip>
-            ) : (
+          {showHideButton &&
+            data?.mediaInfo?.status !== MediaStatus.PROCESSING &&
+            data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
+            data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
+            data?.mediaInfo?.status !== MediaStatus.PENDING &&
+            data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
               <Tooltip
-                content={intl.formatMessage(messages.removefromwatchlist)}
+                content={intl.formatMessage(globalMessages.addToBlacklist)}
               >
                 <Button
+                  buttonType={'ghost'}
                   className="z-40 mr-2"
                   buttonSize={'md'}
-                  onClick={onClickDeleteWatchlistBtn}
+                  onClick={() => setShowBlacklistModal(true)}
                 >
-                  {isUpdating ? (
-                    <Spinner className="h-3" />
-                  ) : (
-                    <MinusCircleIcon className={'h-3'} />
-                  )}
+                  <EyeSlashIcon className={'h-3'} />
                 </Button>
               </Tooltip>
             )}
-          </>
+          {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
+            <>
+              {toggleWatchlist ? (
+                <Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
+                  <Button
+                    buttonType={'ghost'}
+                    className="z-40 mr-2"
+                    buttonSize={'md'}
+                    onClick={onClickWatchlistBtn}
+                  >
+                    {isUpdating ? (
+                      <Spinner className="h-3" />
+                    ) : (
+                      <StarIcon className={'h-3 text-amber-300'} />
+                    )}
+                  </Button>
+                </Tooltip>
+              ) : (
+                <Tooltip
+                  content={intl.formatMessage(messages.removefromwatchlist)}
+                >
+                  <Button
+                    className="z-40 mr-2"
+                    buttonSize={'md'}
+                    onClick={onClickDeleteWatchlistBtn}
+                  >
+                    {isUpdating ? (
+                      <Spinner className="h-3" />
+                    ) : (
+                      <MinusCircleIcon className={'h-3'} />
+                    )}
+                  </Button>
+                </Tooltip>
+              )}
+            </>
+          )}
           <PlayButton links={mediaLinks} />
           <RequestButton
             mediaType="movie"
diff --git a/src/components/PermissionEdit/index.tsx b/src/components/PermissionEdit/index.tsx
index a220335bd..5a861de8a 100644
--- a/src/components/PermissionEdit/index.tsx
+++ b/src/components/PermissionEdit/index.tsx
@@ -78,6 +78,13 @@ export const messages = defineMessages('components.PermissionEdit', {
   viewwatchlists: 'View {mediaServerName} Watchlists',
   viewwatchlistsDescription:
     "Grant permission to view other users' {mediaServerName} Watchlists.",
+  manageblacklist: 'Manage Blacklist',
+  manageblacklistDescription: 'Grant permission to manage blacklisted media.',
+  blacklistedItems: 'Blacklist media.',
+  blacklistedItemsDescription: 'Grant permission to blacklist media.',
+  viewblacklistedItems: 'View blacklisted media.',
+  viewblacklistedItemsDescription:
+    'Grant permission to view blacklisted media.',
 });
 
 interface PermissionEditProps {
@@ -332,6 +339,22 @@ export const PermissionEdit = ({
         },
       ],
     },
+    {
+      id: 'manageblacklist',
+      name: intl.formatMessage(messages.manageblacklist),
+      description: intl.formatMessage(messages.manageblacklistDescription),
+      permission: Permission.MANAGE_BLACKLIST,
+      children: [
+        {
+          id: 'viewblacklisteditems',
+          name: intl.formatMessage(messages.viewblacklistedItems),
+          description: intl.formatMessage(
+            messages.viewblacklistedItemsDescription
+          ),
+          permission: Permission.VIEW_BLACKLIST,
+        },
+      ],
+    },
   ];
 
   return (
diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx
index cf27e55da..cbe04fe3d 100644
--- a/src/components/RequestButton/index.tsx
+++ b/src/components/RequestButton/index.tsx
@@ -300,6 +300,7 @@ const RequestButton = ({
     }) &&
     media &&
     media.status !== MediaStatus.AVAILABLE &&
+    media.status !== MediaStatus.BLACKLISTED &&
     !isShowComplete
   ) {
     buttons.push({
@@ -345,6 +346,7 @@ const RequestButton = ({
     }) &&
     media &&
     media.status4k !== MediaStatus.AVAILABLE &&
+    media.status !== MediaStatus.BLACKLISTED &&
     !is4kShowComplete &&
     settings.currentSettings.series4kEnabled
   ) {
diff --git a/src/components/RequestModal/CollectionRequestModal.tsx b/src/components/RequestModal/CollectionRequestModal.tsx
index 2cf09c223..b646f7b1d 100644
--- a/src/components/RequestModal/CollectionRequestModal.tsx
+++ b/src/components/RequestModal/CollectionRequestModal.tsx
@@ -66,7 +66,9 @@ const CollectionRequestModal = ({
     (quota?.movie.remaining ?? 0) - selectedParts.length;
 
   const getAllParts = (): number[] => {
-    return (data?.parts ?? []).map((part) => part.id);
+    return (data?.parts ?? [])
+      .filter((part) => part.mediaInfo?.status !== MediaStatus.BLACKLISTED)
+      .map((part) => part.id);
   };
 
   const getAllRequestedParts = (): number[] => {
@@ -248,6 +250,11 @@ const CollectionRequestModal = ({
     { type: 'or' }
   );
 
+  const blacklistVisibility = hasPermission(
+    [Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
+    { type: 'or' }
+  );
+
   return (
     <Modal
       loading={(!data && !error) || !quota}
@@ -344,122 +351,156 @@ const CollectionRequestModal = ({
                   </tr>
                 </thead>
                 <tbody className="divide-y divide-gray-700">
-                  {data?.parts.map((part) => {
-                    const partRequest = getPartRequest(part.id);
-                    const partMedia =
-                      part.mediaInfo &&
-                      part.mediaInfo[is4k ? 'status4k' : 'status'] !==
-                        MediaStatus.UNKNOWN
-                        ? part.mediaInfo
-                        : undefined;
+                  {data?.parts
+                    .filter((part) => {
+                      if (!blacklistVisibility)
+                        return (
+                          part.mediaInfo?.status !== MediaStatus.BLACKLISTED
+                        );
+                      return part;
+                    })
+                    .map((part) => {
+                      const partRequest = getPartRequest(part.id);
+                      const partMedia =
+                        part.mediaInfo &&
+                        part.mediaInfo[is4k ? 'status4k' : 'status'] !==
+                          MediaStatus.UNKNOWN
+                          ? part.mediaInfo
+                          : undefined;
 
-                    return (
-                      <tr key={`part-${part.id}`}>
-                        <td className="whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100">
-                          <span
-                            role="checkbox"
-                            tabIndex={0}
-                            aria-checked={
-                              !!partMedia || isSelectedPart(part.id)
-                            }
-                            onClick={() => togglePart(part.id)}
-                            onKeyDown={(e) => {
-                              if (e.key === 'Enter' || e.key === 'Space') {
-                                togglePart(part.id);
-                              }
-                            }}
-                            className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
-                              !!partMedia ||
-                              partRequest ||
-                              (quota?.movie.limit &&
-                                currentlyRemaining <= 0 &&
-                                !isSelectedPart(part.id))
-                                ? 'opacity-50'
-                                : ''
+                      return (
+                        <tr key={`part-${part.id}`}>
+                          <td
+                            className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${
+                              partMedia?.status === MediaStatus.BLACKLISTED &&
+                              'pointer-events-none opacity-50'
                             }`}
                           >
                             <span
-                              aria-hidden="true"
-                              className={`${
-                                !!partMedia ||
-                                partRequest ||
-                                isSelectedPart(part.id)
-                                  ? 'bg-indigo-500'
-                                  : 'bg-gray-700'
-                              } absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
-                            ></span>
-                            <span
-                              aria-hidden="true"
-                              className={`${
-                                !!partMedia ||
-                                partRequest ||
+                              role="checkbox"
+                              tabIndex={0}
+                              aria-checked={
+                                (!!partMedia &&
+                                  partMedia.status !==
+                                    MediaStatus.BLACKLISTED) ||
                                 isSelectedPart(part.id)
-                                  ? 'translate-x-5'
-                                  : 'translate-x-0'
-                              } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
-                            ></span>
-                          </span>
-                        </td>
-                        <td className="flex items-center px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
-                          <div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
-                            <CachedImage
-                              src={
-                                part.posterPath
-                                  ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
-                                  : '/images/overseerr_poster_not_found.png'
                               }
-                              alt=""
-                              sizes="100vw"
-                              style={{
-                                width: '100%',
-                                height: 'auto',
-                                objectFit: 'cover',
+                              onClick={() => togglePart(part.id)}
+                              onKeyDown={(e) => {
+                                if (e.key === 'Enter' || e.key === 'Space') {
+                                  togglePart(part.id);
+                                }
                               }}
-                              width={600}
-                              height={900}
-                            />
-                          </div>
-                          <div className="flex flex-col justify-center pl-2">
-                            <div className="text-xs font-medium">
-                              {part.releaseDate?.slice(0, 4)}
+                              className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
+                                (!!partMedia &&
+                                  partMedia.status !==
+                                    MediaStatus.BLACKLISTED) ||
+                                partRequest ||
+                                (quota?.movie.limit &&
+                                  currentlyRemaining <= 0 &&
+                                  !isSelectedPart(part.id))
+                                  ? 'opacity-50'
+                                  : ''
+                              }`}
+                            >
+                              <span
+                                aria-hidden="true"
+                                className={`${
+                                  (!!partMedia &&
+                                    partMedia.status !==
+                                      MediaStatus.BLACKLISTED) ||
+                                  partRequest ||
+                                  isSelectedPart(part.id)
+                                    ? 'bg-indigo-500'
+                                    : 'bg-gray-700'
+                                } absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
+                              ></span>
+                              <span
+                                aria-hidden="true"
+                                className={`${
+                                  (!!partMedia &&
+                                    partMedia.status !==
+                                      MediaStatus.BLACKLISTED) ||
+                                  partRequest ||
+                                  isSelectedPart(part.id)
+                                    ? 'translate-x-5'
+                                    : 'translate-x-0'
+                                } absolute left-0 inline-block h-5 w-5 rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
+                              ></span>
+                            </span>
+                          </td>
+                          <td
+                            className={`flex items-center px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 ${
+                              partMedia?.status === MediaStatus.BLACKLISTED &&
+                              'pointer-events-none opacity-50'
+                            }`}
+                          >
+                            <div className="relative h-auto w-10 flex-shrink-0 overflow-hidden rounded-md">
+                              <CachedImage
+                                src={
+                                  part.posterPath
+                                    ? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
+                                    : '/images/overseerr_poster_not_found.png'
+                                }
+                                alt=""
+                                sizes="100vw"
+                                style={{
+                                  width: '100%',
+                                  height: 'auto',
+                                  objectFit: 'cover',
+                                }}
+                                width={600}
+                                height={900}
+                              />
                             </div>
-                            <div className="text-base font-bold">
-                              {part.title}
+                            <div className="flex flex-col justify-center pl-2">
+                              <div className="text-xs font-medium">
+                                {part.releaseDate?.slice(0, 4)}
+                              </div>
+                              <div className="text-base font-bold">
+                                {part.title}
+                              </div>
                             </div>
-                          </div>
-                        </td>
-                        <td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
-                          {!partMedia && !partRequest && (
-                            <Badge>
-                              {intl.formatMessage(globalMessages.notrequested)}
-                            </Badge>
-                          )}
-                          {!partMedia &&
-                            partRequest?.status ===
-                              MediaRequestStatus.PENDING && (
-                              <Badge badgeType="warning">
-                                {intl.formatMessage(globalMessages.pending)}
+                          </td>
+                          <td className="whitespace-nowrap py-4 pr-2 text-sm leading-5 text-gray-200 md:px-6">
+                            {!partMedia && !partRequest && (
+                              <Badge>
+                                {intl.formatMessage(
+                                  globalMessages.notrequested
+                                )}
+                              </Badge>
+                            )}
+                            {!partMedia &&
+                              partRequest?.status ===
+                                MediaRequestStatus.PENDING && (
+                                <Badge badgeType="warning">
+                                  {intl.formatMessage(globalMessages.pending)}
+                                </Badge>
+                              )}
+                            {((!partMedia &&
+                              partRequest?.status ===
+                                MediaRequestStatus.APPROVED) ||
+                              partMedia?.[is4k ? 'status4k' : 'status'] ===
+                                MediaStatus.PROCESSING) && (
+                              <Badge badgeType="primary">
+                                {intl.formatMessage(globalMessages.requested)}
+                              </Badge>
+                            )}
+                            {partMedia?.[is4k ? 'status4k' : 'status'] ===
+                              MediaStatus.AVAILABLE && (
+                              <Badge badgeType="success">
+                                {intl.formatMessage(globalMessages.available)}
+                              </Badge>
+                            )}
+                            {partMedia?.status === MediaStatus.BLACKLISTED && (
+                              <Badge badgeType="danger">
+                                {intl.formatMessage(globalMessages.blacklisted)}
                               </Badge>
                             )}
-                          {((!partMedia &&
-                            partRequest?.status ===
-                              MediaRequestStatus.APPROVED) ||
-                            partMedia?.[is4k ? 'status4k' : 'status'] ===
-                              MediaStatus.PROCESSING) && (
-                            <Badge badgeType="primary">
-                              {intl.formatMessage(globalMessages.requested)}
-                            </Badge>
-                          )}
-                          {partMedia?.[is4k ? 'status4k' : 'status'] ===
-                            MediaStatus.AVAILABLE && (
-                            <Badge badgeType="success">
-                              {intl.formatMessage(globalMessages.available)}
-                            </Badge>
-                          )}
-                        </td>
-                      </tr>
-                    );
-                  })}
+                          </td>
+                        </tr>
+                      );
+                    })}
                 </tbody>
               </table>
             </div>
diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx
index 1d280d289..0821c0175 100644
--- a/src/components/StatusBadge/index.tsx
+++ b/src/components/StatusBadge/index.tsx
@@ -360,6 +360,17 @@ const StatusBadge = ({
         </Tooltip>
       );
 
+    case MediaStatus.BLACKLISTED:
+      return (
+        <Tooltip content={mediaLinkDescription}>
+          <Badge badgeType="danger" href={mediaLink}>
+            {intl.formatMessage(is4k ? messages.status4k : messages.status, {
+              status: intl.formatMessage(globalMessages.blacklisted),
+            })}
+          </Badge>
+        </Tooltip>
+      );
+
     default:
       return null;
   }
diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx
index b6c887968..2d10fdf1c 100644
--- a/src/components/TitleCard/index.tsx
+++ b/src/components/TitleCard/index.tsx
@@ -1,7 +1,9 @@
 import Spinner from '@app/assets/spinner.svg';
+import BlacklistModal from '@app/components/BlacklistModal';
 import Button from '@app/components/Common/Button';
 import CachedImage from '@app/components/Common/CachedImage';
 import StatusBadgeMini from '@app/components/Common/StatusBadgeMini';
+import Tooltip from '@app/components/Common/Tooltip';
 import RequestModal from '@app/components/RequestModal';
 import ErrorCard from '@app/components/TitleCard/ErrorCard';
 import Placeholder from '@app/components/TitleCard/Placeholder';
@@ -13,6 +15,8 @@ import { withProperties } from '@app/utils/typeHelpers';
 import { Transition } from '@headlessui/react';
 import {
   ArrowDownTrayIcon,
+  EyeIcon,
+  EyeSlashIcon,
   MinusCircleIcon,
   StarIcon,
 } from '@heroicons/react/24/outline';
@@ -20,7 +24,7 @@ import { MediaStatus } from '@server/constants/media';
 import type { Watchlist } from '@server/entity/Watchlist';
 import type { MediaType } from '@server/models/Search';
 import Link from 'next/link';
-import { Fragment, useCallback, useEffect, useState } from 'react';
+import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
 import { useIntl } from 'react-intl';
 import { useToasts } from 'react-toast-notifications';
 import { mutate } from 'swr';
@@ -65,7 +69,7 @@ const TitleCard = ({
 }: TitleCardProps) => {
   const isTouch = useIsTouch();
   const intl = useIntl();
-  const { hasPermission } = useUser();
+  const { user, hasPermission } = useUser();
   const [isUpdating, setIsUpdating] = useState(false);
   const [currentStatus, setCurrentStatus] = useState(status);
   const [showDetail, setShowDetail] = useState(false);
@@ -74,6 +78,8 @@ const TitleCard = ({
   const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
     !isAddedToWatchlist
   );
+  const [showBlacklistModal, setShowBlacklistModal] = useState(false);
+  const cardRef = useRef<HTMLDivElement>(null);
 
   // Just to get the year from the date
   if (year) {
@@ -94,6 +100,11 @@ const TitleCard = ({
     []
   );
 
+  const closeBlacklistModal = useCallback(
+    () => setShowBlacklistModal(false),
+    []
+  );
+
   const onClickWatchlistBtn = async (): Promise<void> => {
     setIsUpdating(true);
     try {
@@ -166,6 +177,99 @@ const TitleCard = ({
     }
   };
 
+  const onClickHideItemBtn = async (): Promise<void> => {
+    setIsUpdating(true);
+    const topNode = cardRef.current;
+
+    if (topNode) {
+      const res = await fetch('/api/v1/blacklist', {
+        method: 'POST',
+        headers: {
+          Accept: 'application/json',
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          tmdbId: id,
+          mediaType,
+          title,
+          user: user?.id,
+        }),
+      });
+
+      if (res.status === 201) {
+        addToast(
+          <span>
+            {intl.formatMessage(globalMessages.blacklistSuccess, {
+              title,
+              strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+            })}
+          </span>,
+          { appearance: 'success', autoDismiss: true }
+        );
+        setCurrentStatus(MediaStatus.BLACKLISTED);
+      } else if (res.status === 412) {
+        addToast(
+          <span>
+            {intl.formatMessage(globalMessages.blacklistDuplicateError, {
+              title,
+              strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+            })}
+          </span>,
+          { appearance: 'info', autoDismiss: true }
+        );
+      } else {
+        addToast(intl.formatMessage(globalMessages.blacklistError), {
+          appearance: 'error',
+          autoDismiss: true,
+        });
+      }
+
+      setIsUpdating(false);
+      closeBlacklistModal();
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+  };
+
+  const onClickShowBlacklistBtn = async (): Promise<void> => {
+    setIsUpdating(true);
+    const topNode = cardRef.current;
+
+    if (topNode) {
+      const res = await fetch('/api/v1/blacklist/' + id, {
+        method: 'DELETE',
+      });
+
+      if (res.status === 204) {
+        addToast(
+          <span>
+            {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
+              title,
+              strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+            })}
+          </span>,
+          { appearance: 'success', autoDismiss: true }
+        );
+        setCurrentStatus(MediaStatus.UNKNOWN);
+      } else {
+        addToast(intl.formatMessage(globalMessages.blacklistError), {
+          appearance: 'error',
+          autoDismiss: true,
+        });
+      }
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+
+    setIsUpdating(false);
+  };
+
   const closeModal = useCallback(() => setShowRequestModal(false), []);
 
   const showRequestButton = hasPermission(
@@ -178,10 +282,15 @@ const TitleCard = ({
     { type: 'or' }
   );
 
+  const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
+    type: 'or',
+  });
+
   return (
     <div
       className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
       data-testid="title-card"
+      ref={cardRef}
     >
       <RequestModal
         tmdbId={id}
@@ -197,6 +306,20 @@ const TitleCard = ({
         onUpdating={requestUpdating}
         onCancel={closeModal}
       />
+      <BlacklistModal
+        tmdbId={id}
+        type={
+          mediaType === 'movie'
+            ? 'movie'
+            : mediaType === 'collection'
+            ? 'collection'
+            : 'tv'
+        }
+        show={showBlacklistModal}
+        onCancel={closeBlacklistModal}
+        onComplete={onClickHideItemBtn}
+        isUpdating={isUpdating}
+      />
       <div
         className={`relative transform-gpu cursor-default overflow-hidden rounded-xl bg-gray-800 bg-cover outline-none ring-1 transition duration-300 ${
           showDetail
@@ -235,7 +358,7 @@ const TitleCard = ({
           />
           <div className="absolute left-0 right-0 flex items-center justify-between p-2">
             <div
-              className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
+              className={`pointer-events-none z-40 self-start rounded-full border bg-opacity-80 shadow-md ${
                 mediaType === 'movie' || mediaType === 'collection'
                   ? 'border-blue-500 bg-blue-600'
                   : 'border-purple-600 bg-purple-600'
@@ -249,8 +372,8 @@ const TitleCard = ({
                   : intl.formatMessage(globalMessages.tvshow)}
               </div>
             </div>
-            {showDetail && (
-              <>
+            {showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
+              <div className="flex flex-col gap-1">
                 {toggleWatchlist ? (
                   <Button
                     buttonType={'ghost'}
@@ -269,15 +392,49 @@ const TitleCard = ({
                     <MinusCircleIcon className={'h-3'} />
                   </Button>
                 )}
-              </>
+                {showHideButton &&
+                  currentStatus !== MediaStatus.PROCESSING &&
+                  currentStatus !== MediaStatus.AVAILABLE &&
+                  currentStatus !== MediaStatus.PARTIALLY_AVAILABLE &&
+                  currentStatus !== MediaStatus.PENDING && (
+                    <Button
+                      buttonType={'ghost'}
+                      className="z-40"
+                      buttonSize={'sm'}
+                      onClick={() => setShowBlacklistModal(true)}
+                    >
+                      <EyeSlashIcon className={'h-3'} />
+                    </Button>
+                  )}
+              </div>
             )}
+            {showDetail &&
+              showHideButton &&
+              currentStatus == MediaStatus.BLACKLISTED && (
+                <Tooltip
+                  content={intl.formatMessage(
+                    globalMessages.removefromBlacklist
+                  )}
+                >
+                  <Button
+                    buttonType={'ghost'}
+                    className="z-40"
+                    buttonSize={'sm'}
+                    onClick={() => onClickShowBlacklistBtn()}
+                  >
+                    <EyeIcon className={'h-3'} />
+                  </Button>
+                </Tooltip>
+              )}
             {currentStatus && currentStatus !== MediaStatus.UNKNOWN && (
-              <div className="pointer-events-none z-40 flex items-center">
-                <StatusBadgeMini
-                  status={currentStatus}
-                  inProgress={inProgress}
-                  shrink
-                />
+              <div className="flex flex-col items-center gap-1">
+                <div className="pointer-events-none z-40 flex">
+                  <StatusBadgeMini
+                    status={currentStatus}
+                    inProgress={inProgress}
+                    shrink
+                  />
+                </div>
               </div>
             )}
           </div>
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx
index 634c72d05..cf788237b 100644
--- a/src/components/TvDetails/index.tsx
+++ b/src/components/TvDetails/index.tsx
@@ -4,6 +4,7 @@ import RTFresh from '@app/assets/rt_fresh.svg';
 import RTRotten from '@app/assets/rt_rotten.svg';
 import Spinner from '@app/assets/spinner.svg';
 import TmdbLogo from '@app/assets/tmdb_logo.svg';
+import BlacklistModal from '@app/components/BlacklistModal';
 import Badge from '@app/components/Common/Badge';
 import Button from '@app/components/Common/Button';
 import CachedImage from '@app/components/Common/CachedImage';
@@ -38,6 +39,7 @@ import {
   ArrowRightCircleIcon,
   CogIcon,
   ExclamationTriangleIcon,
+  EyeSlashIcon,
   FilmIcon,
   PlayIcon,
 } from '@heroicons/react/24/outline';
@@ -61,7 +63,7 @@ import { countries } from 'country-flag-icons';
 import 'country-flag-icons/3x2/flags.css';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useIntl } from 'react-intl';
 import { useToasts } from 'react-toast-notifications';
 import useSWR from 'swr';
@@ -125,6 +127,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
   const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
     !tv?.onUserWatchlist
   );
+  const [isBlacklistUpdating, setIsBlacklistUpdating] =
+    useState<boolean>(false);
+  const [showBlacklistModal, setShowBlacklistModal] = useState(false);
   const { addToast } = useToasts();
 
   const {
@@ -155,6 +160,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
     setShowManager(router.query.manage == '1' ? true : false);
   }, [router.query.manage]);
 
+  const closeBlacklistModal = useCallback(
+    () => setShowBlacklistModal(false),
+    []
+  );
+
   const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
     mediaUrl: data?.mediaInfo?.mediaUrl,
     mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
@@ -397,6 +407,60 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
     }
   };
 
+  const onClickHideItemBtn = async (): Promise<void> => {
+    setIsBlacklistUpdating(true);
+
+    const res = await fetch('/api/v1/blacklist', {
+      method: 'POST',
+      headers: {
+        Accept: 'application/json',
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        tmdbId: tv?.id,
+        mediaType: 'tv',
+        title: tv?.name,
+        user: user?.id,
+      }),
+    });
+
+    if (res.status === 201) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.blacklistSuccess, {
+            title: tv?.name,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'success', autoDismiss: true }
+      );
+
+      revalidate();
+    } else if (res.status === 412) {
+      addToast(
+        <span>
+          {intl.formatMessage(globalMessages.blacklistDuplicateError, {
+            title: tv?.name,
+            strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
+          })}
+        </span>,
+        { appearance: 'info', autoDismiss: true }
+      );
+    } else {
+      addToast(intl.formatMessage(globalMessages.blacklistError), {
+        appearance: 'error',
+        autoDismiss: true,
+      });
+    }
+
+    setIsBlacklistUpdating(false);
+    closeBlacklistModal();
+  };
+
+  const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
+    type: 'or',
+  });
+
   return (
     <div
       className="media-page"
@@ -423,6 +487,14 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
         </div>
       )}
       <PageTitle title={data.name} />
+      <BlacklistModal
+        tmdbId={data.id}
+        type="tv"
+        show={showBlacklistModal}
+        onCancel={closeBlacklistModal}
+        onComplete={onClickHideItemBtn}
+        isUpdating={isBlacklistUpdating}
+      />
       <IssueModal
         onCancel={() => setShowIssueModal(false)}
         show={showIssueModal}
@@ -528,40 +600,61 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
           </span>
         </div>
         <div className="media-actions">
-          <>
-            {toggleWatchlist ? (
-              <Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
-                <Button
-                  buttonType={'ghost'}
-                  className="z-40 mr-2"
-                  buttonSize={'md'}
-                  onClick={onClickWatchlistBtn}
-                >
-                  {isUpdating ? (
-                    <Spinner className="h-3" />
-                  ) : (
-                    <StarIcon className={'h-3 text-amber-300'} />
-                  )}
-                </Button>
-              </Tooltip>
-            ) : (
+          {showHideButton &&
+            data?.mediaInfo?.status !== MediaStatus.PROCESSING &&
+            data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
+            data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
+            data?.mediaInfo?.status !== MediaStatus.PENDING &&
+            data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
               <Tooltip
-                content={intl.formatMessage(messages.removefromwatchlist)}
+                content={intl.formatMessage(globalMessages.addToBlacklist)}
               >
                 <Button
+                  buttonType={'ghost'}
                   className="z-40 mr-2"
                   buttonSize={'md'}
-                  onClick={onClickDeleteWatchlistBtn}
+                  onClick={() => setShowBlacklistModal(true)}
                 >
-                  {isUpdating ? (
-                    <Spinner className="h-3" />
-                  ) : (
-                    <MinusCircleIcon className={'h-3'} />
-                  )}
+                  <EyeSlashIcon className={'h-3'} />
                 </Button>
               </Tooltip>
             )}
-          </>
+          {data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
+            <>
+              {toggleWatchlist ? (
+                <Tooltip content={intl.formatMessage(messages.addtowatchlist)}>
+                  <Button
+                    buttonType={'ghost'}
+                    className="z-40 mr-2"
+                    buttonSize={'md'}
+                    onClick={onClickWatchlistBtn}
+                  >
+                    {isUpdating ? (
+                      <Spinner className="h-3" />
+                    ) : (
+                      <StarIcon className={'h-3 text-amber-300'} />
+                    )}
+                  </Button>
+                </Tooltip>
+              ) : (
+                <Tooltip
+                  content={intl.formatMessage(messages.removefromwatchlist)}
+                >
+                  <Button
+                    className="z-40 mr-2"
+                    buttonSize={'md'}
+                    onClick={onClickDeleteWatchlistBtn}
+                  >
+                    {isUpdating ? (
+                      <Spinner className="h-3" />
+                    ) : (
+                      <MinusCircleIcon className={'h-3'} />
+                    )}
+                  </Button>
+                </Tooltip>
+              )}
+            </>
+          )}
           <PlayButton links={mediaLinks} />
           <RequestButton
             mediaType="tv"
diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts
index d797087d0..6aa5ed1da 100644
--- a/src/i18n/globalMessages.ts
+++ b/src/i18n/globalMessages.ts
@@ -55,6 +55,16 @@ const globalMessages = defineMessages('i18n', {
   noresults: 'No results.',
   open: 'Open',
   resolved: 'Resolved',
+  blacklist: 'Blacklist',
+  blacklisted: 'Blacklisted',
+  blacklistSuccess: '<strong>{title}</strong> was successfully blacklisted.',
+  blacklistError: 'Something went wrong try again.',
+  blacklistDuplicateError:
+    '<strong>{title}</strong> has already been blacklisted.',
+  removeFromBlacklistSuccess:
+    '<strong>{title}</strong> was successfully removed from the Blacklist.',
+  addToBlacklist: 'Add to Blacklist',
+  removefromBlacklist: 'Remove from Blacklist',
 });
 
 export default globalMessages;
diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json
index cf66b67e9..42e8e6f5b 100644
--- a/src/i18n/locale/en.json
+++ b/src/i18n/locale/en.json
@@ -1,7 +1,18 @@
 {
+  "component.BlacklistBlock.blacklistdate": "Blacklisted date",
+  "component.BlacklistBlock.blacklistedby": "Blacklisted By",
+  "component.BlacklistModal.blacklisting": "Blacklisting",
   "components.AirDateBadge.airedrelative": "Aired {relativeTime}",
   "components.AirDateBadge.airsrelative": "Airing {relativeTime}",
   "components.AppDataWarning.dockerVolumeMissingDescription": "The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.",
+  "components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> is not blacklisted.",
+  "components.Blacklist.blacklistSettingsDescription": "Manage blacklisted media.",
+  "components.Blacklist.blacklistdate": "date",
+  "components.Blacklist.blacklistedby": "{date} by {user}",
+  "components.Blacklist.blacklistsettings": "Blacklist Settings",
+  "components.Blacklist.mediaName": "Name",
+  "components.Blacklist.mediaTmdbId": "tmdb Id",
+  "components.Blacklist.mediaType": "Type",
   "components.CollectionDetails.numberofmovies": "{count} Movies",
   "components.CollectionDetails.overview": "Overview",
   "components.CollectionDetails.requestcollection": "Request Collection",
@@ -200,6 +211,7 @@
   "components.LanguageSelector.originalLanguageDefault": "All Languages",
   "components.Layout.LanguagePicker.displaylanguage": "Display Language",
   "components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
+  "components.Layout.Sidebar.blacklist": "Blacklist",
   "components.Layout.Sidebar.browsemovies": "Movies",
   "components.Layout.Sidebar.browsetv": "Series",
   "components.Layout.Sidebar.dashboard": "Discover",
@@ -387,8 +399,12 @@
   "components.PermissionEdit.autorequestMoviesDescription": "Grant permission to automatically submit requests for non-4K movies via Plex Watchlist.",
   "components.PermissionEdit.autorequestSeries": "Auto-Request Series",
   "components.PermissionEdit.autorequestSeriesDescription": "Grant permission to automatically submit requests for non-4K series via Plex Watchlist.",
+  "components.PermissionEdit.blacklistedItems": "Blacklist media.",
+  "components.PermissionEdit.blacklistedItemsDescription": "Grant permission to blacklist media.",
   "components.PermissionEdit.createissues": "Report Issues",
   "components.PermissionEdit.createissuesDescription": "Grant permission to report media issues.",
+  "components.PermissionEdit.manageblacklist": "Manage Blacklist",
+  "components.PermissionEdit.manageblacklistDescription": "Grant permission to manage blacklisted media.",
   "components.PermissionEdit.manageissues": "Manage Issues",
   "components.PermissionEdit.manageissuesDescription": "Grant permission to manage media issues.",
   "components.PermissionEdit.managerequests": "Manage Requests",
@@ -407,6 +423,8 @@
   "components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
   "components.PermissionEdit.users": "Manage Users",
   "components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
+  "components.PermissionEdit.viewblacklistedItems": "View blacklisted media.",
+  "components.PermissionEdit.viewblacklistedItemsDescription": "Grant permission to view blacklisted media.",
   "components.PermissionEdit.viewissues": "View Issues",
   "components.PermissionEdit.viewissuesDescription": "Grant permission to view media issues reported by other users.",
   "components.PermissionEdit.viewrecent": "View Recently Added",
@@ -1299,6 +1317,11 @@
   "i18n.areyousure": "Are you sure?",
   "i18n.available": "Available",
   "i18n.back": "Back",
+  "i18n.blacklist": "Blacklist",
+  "i18n.blacklistDuplicateError": "<strong>{title}</strong> has already been blacklisted.",
+  "i18n.blacklistError": "Something went wrong try again.",
+  "i18n.blacklistSuccess": "<strong>{title}</strong> was successfully blacklisted.",
+  "i18n.blacklisted": "Blacklisted",
   "i18n.cancel": "Cancel",
   "i18n.canceling": "Canceling…",
   "i18n.close": "Close",
@@ -1324,6 +1347,8 @@
   "i18n.pending": "Pending",
   "i18n.previous": "Previous",
   "i18n.processing": "Processing",
+  "i18n.removeFromBlacklistSuccess": "<strong>{title}</strong> was successfully removed from the Blacklist.",
+  "i18n.removefromBlacklist": "Remove from Blacklist",
   "i18n.request": "Request",
   "i18n.request4k": "Request in 4K",
   "i18n.requested": "Requested",
diff --git a/src/pages/blacklist/index.tsx b/src/pages/blacklist/index.tsx
new file mode 100644
index 000000000..e7e3903b0
--- /dev/null
+++ b/src/pages/blacklist/index.tsx
@@ -0,0 +1,13 @@
+import Blacklist from '@app/components/Blacklist';
+import useRouteGuard from '@app/hooks/useRouteGuard';
+import { Permission } from '@server/lib/permissions';
+import type { NextPage } from 'next';
+
+const BlacklistPage: NextPage = () => {
+  useRouteGuard([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
+    type: 'or',
+  });
+  return <Blacklist />;
+};
+
+export default BlacklistPage;