refactor: rename blacklist to blocklist (#2157)

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
Co-authored-by: 0xsysr3ll <0xsysr3ll@pm.me>
Co-authored-by: gauthier-th <mail@gauthierth.fr>
This commit is contained in:
Conlan Kreher
2026-02-14 08:31:45 -05:00
committed by GitHub
parent 15be3d7475
commit 33a5d9a9ac
55 changed files with 842 additions and 535 deletions

View File

@@ -26,7 +26,7 @@
- Granular permission system.
- Support for various notification agents.
- Mobile-friendly design, for when you need to approve requests on the go!
- Support for watchlisting & blacklisting media.
- Support for watchlisting & blocklisting media.
With more features on the way! Check out our [issue tracker](/../../issues) to see the features which have already been requested.

View File

@@ -18,8 +18,8 @@
"discoverRegion": "",
"streamingRegion": "",
"originalLanguage": "",
"blacklistedTags": "",
"blacklistedTagsLimit": 50,
"blocklistedTags": "",
"blocklistedTagsLimit": 50,
"trustProxy": false,
"mediaServerType": 1,
"partialRequestsEnabled": true,

View File

@@ -23,7 +23,7 @@ Welcome to the Seerr Documentation.
- Localization into other languages.
- Support for **PostgreSQL** and **SQLite** databases.
- Support for various notification agents.
- Easily **Watchlist** or **Blacklist** media.
- Easily **Watchlist** or **Blocklist** media.
- More features to come!
## We need your help!

View File

@@ -13,7 +13,7 @@ These settings are stored in the `settings.json` file located in the Seerr data
## User Data
Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL).
Apart from the settings, all other data—including user accounts, media requests, blocklist etc. are stored in the database (either SQLite or PostgreSQL).
# Backup

View File

@@ -62,13 +62,13 @@ Set the default display language for Seerr. Users can override this setting in t
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
## Blacklist Content with Tags and Limit Content Blacklisted per Tag
## Blocklist Content with Tags and Limit Content Blocklisted per Tag
These settings blacklist any TV shows or movies that have one of the entered tags. The "Process Blacklisted Tags" job adds entries to the blacklist based on the configured blacklisted tags. If a blacklisted tag is removed, any media blacklisted under that tag will be removed from the blacklist when the "Process Blacklisted Tags" job runs.
These settings blocklist any TV shows or movies that have one of the entered tags. The "Process Blocklisted Tags" job adds entries to the blocklist based on the configured blocklisted tags. If a blocklisted tag is removed, any media blocklisted under that tag will be removed from the blocklist when the "Process Blocklisted Tags" job runs.
The limit setting determines how many pages per tag the job will process, with each page containing 20 entries. The job cycles through all 16 available discovery sort options, querying the defined number of pages to blacklist media that is most likely to appear at the top of each sort. Higher limits will create a more accurate blacklist, but will require more storage.
The limit setting determines how many pages per tag the job will process, with each page containing 20 entries. The job cycles through all 16 available discovery sort options, querying the defined number of pages to blocklist media that is most likely to appear at the top of each sort. Higher limits will create a more accurate blocklist, but will require more storage.
Blacklisted tags are disabled until at least one tag is entered. These settings cannot be overridden in user settings.
Blocklisted tags are disabled until at least one tag is entered. These settings cannot be overridden in user settings.
## Hide Available Media
@@ -78,9 +78,9 @@ Available media will still appear in search results, however, so it is possible
This setting is **disabled** by default.
## Hide Blacklisted Items
## Hide Blocklisted Items
When enabled, media that has been blacklisted will not appear on the "Discover" home page, for all administrators. This can be useful to hide content that you don't want to see, such as content with specific tags or content that has been manually blacklisted when you have the "Manage Blacklist" permission.
When enabled, media that has been blocklisted will not appear on the "Discover" home page, for all administrators. This can be useful to hide content that you don't want to see, such as content with specific tags or content that has been manually blocklisted when you have the "Manage Blocklist" permission.
This setting is **disabled** by default.

View File

@@ -21,7 +21,7 @@ Seerr brings several features that were previously available in Jellyseerr but m
* **Alternative media solution:** Added support for Jellyfin and Emby in addition to the existing Plex integration.
* **PostgreSQL support**: In addition to SQLite, you can now opt in to using a PostgreSQL database.
* **Blacklist for movies, series, and tags**: Allows permitted users to hide movies, series, or tags from regular users.
* **Blocklist for movies, series, and tags**: Allows permitted users to hide movies, series, or tags from regular users.
* **Override rules**: Adjust default request settings based on conditions such as user, tag, or other criteria.
* **TVDB metadata**: Option to use TheTVDB metadata for series (as in Sonarr) instead of TMDB.
* **DNS caching**: Reduces lookup times and external requests, especially useful when using systems like Pi-Hole/Adguard Home.

View File

@@ -38,8 +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.
- name: blocklist
description: Blocklisted media from discovery page.
servers:
- url: '{server}/api/v1'
variables:
@@ -48,7 +48,7 @@ servers:
components:
schemas:
Blacklist:
Blocklist:
type: object
properties:
tmdbId:
@@ -4529,12 +4529,12 @@ paths:
restricted:
type: boolean
example: false
/blacklist:
/blocklist:
get:
summary: Returns blacklisted items
description: Returns list of all blacklisted media
summary: Returns blocklisted items
description: Returns list of all blocklisted media
tags:
- settings
- blocklist
parameters:
- in: query
name: take
@@ -4558,11 +4558,11 @@ paths:
name: filter
schema:
type: string
enum: [all, manual, blacklistedTags]
enum: [all, manual, blocklistedTags]
default: manual
responses:
'200':
description: Blacklisted items returned
description: Blocklisted items returned
content:
application/json:
schema:
@@ -4593,25 +4593,25 @@ paths:
type: number
example: 438631
post:
summary: Add media to blacklist
summary: Add media to blocklist
tags:
- blacklist
- blocklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Blacklist'
$ref: '#/components/schemas/Blocklist'
responses:
'201':
description: Item succesfully blacklisted
description: Item succesfully blocklisted
'412':
description: Item has already been blacklisted
/blacklist/{tmdbId}:
description: Item has already been blocklisted
/blocklist/{tmdbId}:
get:
summary: Get media from blacklist
summary: Get media from blocklist
tags:
- blacklist
- blocklist
parameters:
- in: path
name: tmdbId
@@ -4622,11 +4622,131 @@ paths:
type: string
responses:
'200':
description: Blacklist details in JSON
description: Blocklist details in JSON
delete:
summary: Remove media from blacklist
summary: Remove media from blocklist
tags:
- blacklist
- blocklist
parameters:
- in: path
name: tmdbId
description: tmdbId ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed media item
/blacklist:
get:
summary: Returns blocklisted items
description: |
**DEPRECATED**: Use `/blocklist` instead. This endpoint will be deprecated soon.
deprecated: true
tags:
- blocklist
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
- in: query
name: filter
schema:
type: string
enum: [all, manual, blocklistedTags]
default: manual
responses:
'200':
description: Blocklisted 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 blocklist
description: |
**DEPRECATED**: Use `/blocklist` instead. This endpoint will be deprecated soon.
deprecated: true
tags:
- blocklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Blocklist'
responses:
'201':
description: Item succesfully blocklisted
'412':
description: Item has already been blocklisted
/blacklist/{tmdbId}:
get:
summary: Get media from blocklist
description: |
**DEPRECATED**: Use `/blocklist/{tmdbId}` instead. This endpoint will be deprecated soon.
deprecated: true
tags:
- blocklist
parameters:
- in: path
name: tmdbId
description: tmdbId ID
required: true
example: '1'
schema:
type: string
responses:
'200':
description: Blocklist details in JSON
delete:
summary: Remove media from blocklist
description: |
**DEPRECATED**: Use `/blocklist/{tmdbId}` instead. This endpoint will be deprecated soon.
deprecated: true
tags:
- blocklist
parameters:
- in: path
name: tmdbId

View File

@@ -17,6 +17,6 @@ export enum MediaStatus {
PROCESSING,
PARTIALLY_AVAILABLE,
AVAILABLE,
BLACKLISTED,
BLOCKLISTED,
DELETED,
}

View File

@@ -2,7 +2,7 @@ import { MediaStatus, type MediaType } from '@server/constants/media';
import dataSource from '@server/datasource';
import Media from '@server/entity/Media';
import { User } from '@server/entity/User';
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
import type { BlocklistItem } from '@server/interfaces/api/blocklistInterfaces';
import { DbAwareColumn } from '@server/utils/DbColumnHelper';
import type { EntityManager } from 'typeorm';
import {
@@ -19,7 +19,7 @@ import type { ZodNumber, ZodOptional, ZodString } from 'zod';
@Entity()
@Unique(['tmdbId'])
export class Blacklist implements BlacklistItem {
export class Blocklist implements BlocklistItem {
@PrimaryGeneratedColumn()
public id: number;
@@ -38,65 +38,65 @@ export class Blacklist implements BlacklistItem {
})
user?: User;
@OneToOne(() => Media, (media) => media.blacklist, {
@OneToOne(() => Media, (media) => media.blocklist, {
onDelete: 'CASCADE',
})
@JoinColumn()
public media: Media;
@Column({ nullable: true, type: 'varchar' })
public blacklistedTags?: string;
public blocklistedTags?: string;
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
public createdAt: Date;
constructor(init?: Partial<Blacklist>) {
constructor(init?: Partial<Blocklist>) {
Object.assign(this, init);
}
public static async addToBlacklist(
public static async addToBlocklist(
{
blacklistRequest,
blocklistRequest,
}: {
blacklistRequest: {
blocklistRequest: {
mediaType: MediaType;
title?: ZodOptional<ZodString>['_output'];
tmdbId: ZodNumber['_output'];
blacklistedTags?: string;
blocklistedTags?: string;
};
},
entityManager?: EntityManager
): Promise<void> {
const em = entityManager ?? dataSource;
const blacklist = new this({
...blacklistRequest,
const blocklist = new this({
...blocklistRequest,
});
const mediaRepository = em.getRepository(Media);
let media = await mediaRepository.findOne({
where: {
tmdbId: blacklistRequest.tmdbId,
tmdbId: blocklistRequest.tmdbId,
},
});
const blacklistRepository = em.getRepository(this);
const blocklistRepository = em.getRepository(this);
await blacklistRepository.save(blacklist);
await blocklistRepository.save(blocklist);
if (!media) {
media = new Media({
tmdbId: blacklistRequest.tmdbId,
status: MediaStatus.BLACKLISTED,
status4k: MediaStatus.BLACKLISTED,
mediaType: blacklistRequest.mediaType,
blacklist: Promise.resolve(blacklist),
tmdbId: blocklistRequest.tmdbId,
status: MediaStatus.BLOCKLISTED,
status4k: MediaStatus.BLOCKLISTED,
mediaType: blocklistRequest.mediaType,
blocklist: Promise.resolve(blocklist),
});
await mediaRepository.save(media);
} else {
media.blacklist = Promise.resolve(blacklist);
media.status = MediaStatus.BLACKLISTED;
media.status4k = MediaStatus.BLACKLISTED;
media.blocklist = Promise.resolve(blocklist);
media.status = MediaStatus.BLOCKLISTED;
media.status4k = MediaStatus.BLOCKLISTED;
await mediaRepository.save(media);
}

View File

@@ -3,7 +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 { Blocklist } from '@server/entity/Blocklist';
import type { User } from '@server/entity/User';
import { Watchlist } from '@server/entity/Watchlist';
import type { DownloadingItem } from '@server/lib/downloadtracker';
@@ -126,8 +126,8 @@ class Media {
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
public issues: Issue[];
@OneToOne(() => Blacklist, (blacklist) => blacklist.media)
public blacklist: Promise<Blacklist>;
@OneToOne(() => Blocklist, (blocklist) => blocklist.media)
public blocklist: Promise<Blocklist>;
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
public createdAt: Date;

View File

@@ -35,7 +35,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 {}
export class BlocklistedMediaError extends Error {}
type MediaRequestOptions = {
isAutoRequest?: boolean;
@@ -140,14 +140,14 @@ export class MediaRequest {
mediaType: requestBody.mediaType,
});
} else {
if (media.status === MediaStatus.BLACKLISTED) {
logger.warn('Request for media blocked due to being blacklisted', {
if (media.status === MediaStatus.BLOCKLISTED) {
logger.warn('Request for media blocked due to being blocklisted', {
tmdbId: tmdbMedia.id,
mediaType: requestBody.mediaType,
label: 'Media Request',
});
throw new BlacklistedMediaError('This media is blacklisted.');
throw new BlocklistedMediaError('This media is blocklisted.');
}
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {

View File

@@ -1,15 +1,15 @@
import type { User } from '@server/entity/User';
import type { PaginatedResponse } from '@server/interfaces/api/common';
export interface BlacklistItem {
export interface BlocklistItem {
tmdbId: number;
mediaType: 'movie' | 'tv';
title?: string;
createdAt?: Date;
user?: User;
blacklistedTags?: string;
blocklistedTags?: string;
}
export interface BlacklistResultsResponse extends PaginatedResponse {
results: BlacklistItem[];
export interface BlocklistResultsResponse extends PaginatedResponse {
results: BlocklistItem[];
}

View File

@@ -30,7 +30,7 @@ export interface PublicSettingsResponse {
applicationTitle: string;
applicationUrl: string;
hideAvailable: boolean;
hideBlacklisted: boolean;
hideBlocklisted: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;

View File

@@ -6,7 +6,7 @@ import type {
} from '@server/api/themoviedb/interfaces';
import { MediaType } from '@server/constants/media';
import dataSource from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist';
import { Blocklist } from '@server/entity/Blocklist';
import Media from '@server/entity/Media';
import type {
RunnableScanner,
@@ -20,7 +20,7 @@ import type { EntityManager } from 'typeorm';
const TMDB_API_DELAY_MS = 250;
class AbortTransaction extends Error {}
class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
class BlocklistedTagProcessor implements RunnableScanner<StatusBase> {
private running = false;
private progress = 0;
private total = 0;
@@ -30,12 +30,12 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
try {
await dataSource.transaction(async (em) => {
await this.cleanBlacklist(em);
await this.createBlacklistEntries(em);
await this.cleanBlocklist(em);
await this.createBlocklistEntries(em);
});
} catch (err) {
if (err instanceof AbortTransaction) {
logger.info('Aborting job: Process Blacklisted Tags', {
logger.info('Aborting job: Process Blocklisted Tags', {
label: 'Jobs',
});
} else {
@@ -64,37 +64,37 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
this.cancel();
}
private async createBlacklistEntries(em: EntityManager) {
private async createBlocklistEntries(em: EntityManager) {
const tmdb = createTmdbWithRegionLanguage();
const settings = getSettings();
const blacklistedTags = settings.main.blacklistedTags;
const blacklistedTagsArr = blacklistedTags.split(',');
const blocklistedTags = settings.main.blocklistedTags;
const blocklistedTagsArr = blocklistedTags.split(',');
const pageLimit = settings.main.blacklistedTagsLimit;
const pageLimit = settings.main.blocklistedTagsLimit;
const invalidKeywords = new Set<string>();
if (blacklistedTags.length === 0) {
if (blocklistedTags.length === 0) {
return;
}
// The maximum number of queries we're expected to execute
this.total =
2 * blacklistedTagsArr.length * pageLimit * SortOptionsIterable.length;
2 * blocklistedTagsArr.length * pageLimit * SortOptionsIterable.length;
for (const type of [MediaType.MOVIE, MediaType.TV]) {
const getDiscover =
type === MediaType.MOVIE ? tmdb.getDiscoverMovies : tmdb.getDiscoverTv;
// Iterate for each tag
for (const tag of blacklistedTagsArr) {
for (const tag of blocklistedTagsArr) {
const keywordDetails = await tmdb.getKeywordDetails({
keywordId: Number(tag),
});
if (keywordDetails === null) {
logger.warn('Skipping invalid keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
logger.warn('Skipping invalid keyword in blocklisted tags', {
label: 'Blocklisted Tags Processor',
keywordId: tag,
});
invalidKeywords.add(tag);
@@ -134,8 +134,8 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
queryMax = response.total_pages;
}
} catch (error) {
logger.error('Error processing keyword in blacklisted tags', {
label: 'Blacklisted Tags Processor',
logger.error('Error processing keyword in blocklisted tags', {
label: 'Blocklisted Tags Processor',
keywordId: tag,
errorMessage: error.message,
});
@@ -145,19 +145,19 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
}
if (invalidKeywords.size > 0) {
const currentTags = blacklistedTagsArr.filter(
const currentTags = blocklistedTagsArr.filter(
(tag) => !invalidKeywords.has(tag)
);
const cleanedTags = currentTags.join(',');
if (cleanedTags !== blacklistedTags) {
settings.main.blacklistedTags = cleanedTags;
if (cleanedTags !== blocklistedTags) {
settings.main.blocklistedTags = cleanedTags;
await settings.save();
logger.info('Cleaned up invalid keywords from settings', {
label: 'Blacklisted Tags Processor',
label: 'Blocklisted Tags Processor',
removedKeywords: Array.from(invalidKeywords),
newBlacklistedTags: cleanedTags,
newBlocklistedTags: cleanedTags,
});
}
}
@@ -169,33 +169,33 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
mediaType: MediaType,
em: EntityManager
) {
const blacklistRepository = em.getRepository(Blacklist);
const blocklistRepository = em.getRepository(Blocklist);
for (const entry of response.results) {
const blacklistEntry = await blacklistRepository.findOne({
const blocklistEntry = await blocklistRepository.findOne({
where: { tmdbId: entry.id },
});
if (blacklistEntry) {
// Don't mark manual blacklists with tags
// If media wasn't previously blacklisted for this tag, add the tag to the media's blacklist
if (blocklistEntry) {
// Don't mark manual blocklists with tags
// If media wasn't previously blocklisted for this tag, add the tag to the media's blocklist
if (
blacklistEntry.blacklistedTags &&
!blacklistEntry.blacklistedTags.includes(`,${keywordId},`)
blocklistEntry.blocklistedTags &&
!blocklistEntry.blocklistedTags.includes(`,${keywordId},`)
) {
await blacklistRepository.update(blacklistEntry.id, {
blacklistedTags: `${blacklistEntry.blacklistedTags}${keywordId},`,
await blocklistRepository.update(blocklistEntry.id, {
blocklistedTags: `${blocklistEntry.blocklistedTags}${keywordId},`,
});
}
} else {
// Media wasn't previously blacklisted, add it to the blacklist
await Blacklist.addToBlacklist(
// Media wasn't previously blocklisted, add it to the blocklist
await Blocklist.addToBlocklist(
{
blacklistRequest: {
blocklistRequest: {
mediaType,
title: 'title' in entry ? entry.title : entry.name,
tmdbId: entry.id,
blacklistedTags: `,${keywordId},`,
blocklistedTags: `,${keywordId},`,
},
},
em
@@ -204,22 +204,22 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
}
}
private async cleanBlacklist(em: EntityManager) {
// Remove blacklist and media entries blacklisted by tags
private async cleanBlocklist(em: EntityManager) {
// Remove blocklist and media entries blocklisted by tags
const mediaRepository = em.getRepository(Media);
const mediaToRemove = await mediaRepository
.createQueryBuilder('media')
.innerJoinAndSelect(Blacklist, 'blist', 'blist.tmdbId = media.tmdbId')
.where(`blist.blacklistedTags IS NOT NULL`)
.innerJoinAndSelect(Blocklist, 'blist', 'blist.tmdbId = media.tmdbId')
.where(`blist.blocklistedTags IS NOT NULL`)
.getMany();
// Batch removes so the query doesn't get too large
for (let i = 0; i < mediaToRemove.length; i += 500) {
await mediaRepository.remove(mediaToRemove.slice(i, i + 500)); // This also deletes the blacklist entries via cascading
await mediaRepository.remove(mediaToRemove.slice(i, i + 500)); // This also deletes the blocklist entries via cascading
}
}
}
const blacklistedTagsProcessor = new BlacklistedTagProcessor();
const blocklistedTagsProcessor = new BlocklistedTagProcessor();
export default blacklistedTagsProcessor;
export default blocklistedTagsProcessor;

View File

@@ -1,5 +1,5 @@
import { MediaServerType } from '@server/constants/server';
import blacklistedTagsProcessor from '@server/job/blacklistedTagsProcessor';
import blocklistedTagsProcessor from '@server/job/blocklistedTagsProcessor';
import availabilitySync from '@server/lib/availabilitySync';
import downloadTracker from '@server/lib/downloadtracker';
import ImageProxy from '@server/lib/imageproxy';
@@ -239,19 +239,19 @@ export const startJobs = (): void => {
});
scheduledJobs.push({
id: 'process-blacklisted-tags',
name: 'Process Blacklisted Tags',
id: 'process-blocklisted-tags',
name: 'Process Blocklisted Tags',
type: 'process',
interval: 'days',
cronSchedule: jobs['process-blacklisted-tags'].schedule,
job: schedule.scheduleJob(jobs['process-blacklisted-tags'].schedule, () => {
logger.info('Starting scheduled job: Process Blacklisted Tags', {
cronSchedule: jobs['process-blocklisted-tags'].schedule,
job: schedule.scheduleJob(jobs['process-blocklisted-tags'].schedule, () => {
logger.info('Starting scheduled job: Process Blocklisted Tags', {
label: 'Jobs',
});
blacklistedTagsProcessor.run();
blocklistedTagsProcessor.run();
}),
running: () => blacklistedTagsProcessor.status().running,
cancelFn: () => blacklistedTagsProcessor.cancel(),
running: () => blocklistedTagsProcessor.status().running,
cancelFn: () => blocklistedTagsProcessor.cancel(),
});
logger.info('Scheduled jobs loaded', { label: 'Jobs' });

View File

@@ -27,8 +27,8 @@ export enum Permission {
AUTO_REQUEST_TV = 33554432,
RECENT_VIEW = 67108864,
WATCHLIST_VIEW = 134217728,
MANAGE_BLACKLIST = 268435456,
VIEW_BLACKLIST = 1073741824,
MANAGE_BLOCKLIST = 268435456,
VIEW_BLOCKLIST = 1073741824,
}
export interface PermissionCheckOptions {

View File

@@ -132,15 +132,15 @@ export interface MainSettings {
tv: Quota;
};
hideAvailable: boolean;
hideBlacklisted: boolean;
hideBlocklisted: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
newPlexLogin: boolean;
discoverRegion: string;
streamingRegion: string;
originalLanguage: string;
blacklistedTags: string;
blacklistedTagsLimit: number;
blocklistedTags: string;
blocklistedTagsLimit: number;
mediaServerType: number;
partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean;
@@ -181,7 +181,7 @@ interface FullPublicSettings extends PublicSettings {
applicationTitle: string;
applicationUrl: string;
hideAvailable: boolean;
hideBlacklisted: boolean;
hideBlocklisted: boolean;
localLogin: boolean;
mediaServerLogin: boolean;
movie4kEnabled: boolean;
@@ -346,7 +346,7 @@ export type JobId =
| 'jellyfin-full-scan'
| 'image-cache-cleanup'
| 'availability-sync'
| 'process-blacklisted-tags';
| 'process-blocklisted-tags';
export interface AllSettings {
clientId: string;
@@ -389,15 +389,15 @@ class Settings {
tv: {},
},
hideAvailable: false,
hideBlacklisted: false,
hideBlocklisted: false,
localLogin: true,
mediaServerLogin: true,
newPlexLogin: true,
discoverRegion: '',
streamingRegion: '',
originalLanguage: '',
blacklistedTags: '',
blacklistedTagsLimit: 50,
blocklistedTags: '',
blocklistedTagsLimit: 50,
mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true,
enableSpecialEpisodes: false,
@@ -570,7 +570,7 @@ class Settings {
'image-cache-cleanup': {
schedule: '0 0 5 * * *',
},
'process-blacklisted-tags': {
'process-blocklisted-tags': {
schedule: '0 30 1 */7 * *',
},
},
@@ -671,7 +671,7 @@ class Settings {
applicationTitle: this.data.main.applicationTitle,
applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable,
hideBlacklisted: this.data.main.hideBlacklisted,
hideBlocklisted: this.data.main.hideBlocklisted,
localLogin: this.data.main.localLogin,
mediaServerLogin: this.data.main.mediaServerLogin,
jellyfinExternalHost: this.data.jellyfin.externalHostname,

View File

@@ -0,0 +1,40 @@
import type { AllSettings } from '@server/lib/settings';
const migrateBlacklistToBlocklist = (settings: any): AllSettings => {
if (
Array.isArray(settings.migrations) &&
settings.migrations.includes('0008_migrate_blacklist_to_blocklist')
) {
return settings;
}
if (settings.main?.hideBlacklisted !== undefined) {
settings.main.hideBlocklisted = settings.main.hideBlacklisted;
delete settings.main.hideBlacklisted;
}
if (settings.main?.blacklistedTags !== undefined) {
settings.main.blocklistedTags = settings.main.blacklistedTags;
delete settings.main.blacklistedTags;
}
if (settings.main?.blacklistedTagsLimit !== undefined) {
settings.main.blocklistedTagsLimit = settings.main.blacklistedTagsLimit;
delete settings.main.blacklistedTagsLimit;
}
if (settings.jobs?.['process-blacklisted-tags']) {
settings.jobs['process-blocklisted-tags'] =
settings.jobs['process-blacklisted-tags'];
delete settings.jobs['process-blacklisted-tags'];
}
if (!Array.isArray(settings.migrations)) {
settings.migrations = [];
}
settings.migrations.push('0008_migrate_blacklist_to_blocklist');
return settings;
};
export default migrateBlacklistToBlocklist;

View File

@@ -3,7 +3,7 @@ import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import {
BlacklistedMediaError,
BlocklistedMediaError,
DuplicateMediaRequestError,
MediaRequest,
NoSeasonsAvailableError,
@@ -145,8 +145,8 @@ class WatchlistSync {
errorMessage: e.message,
});
break;
// Blacklisted media should be silently ignored during watchlist sync to avoid spam
case BlacklistedMediaError:
// Blocklisted media should be silently ignored during watchlist sync to avoid spam
case BlocklistedMediaError:
break;
default:
logger.error('Failed to create media request from watchlist', {

View File

@@ -0,0 +1,49 @@
import logger from '@server/logger';
import type { NextFunction, Request, Response } from 'express';
interface DeprecationOptions {
oldPath: string;
newPath: string;
sunsetDate?: string;
documentationUrl?: string;
}
/**
* Mark an API route as deprecated.
* @see https://datatracker.ietf.org/doc/html/rfc8594
*/
export const deprecatedRoute = ({
oldPath,
newPath,
sunsetDate,
documentationUrl,
}: DeprecationOptions) => {
return (req: Request, res: Response, next: NextFunction) => {
logger.warn(
`Deprecated API endpoint accessed: ${oldPath} → use ${newPath} instead`,
{
label: 'API Deprecation',
ip: req.ip,
userAgent: req.get('User-Agent'),
method: req.method,
path: req.originalUrl,
}
);
res.setHeader('Deprecation', 'true');
const links: string[] = [`<${newPath}>; rel="successor-version"`];
if (documentationUrl) {
links.push(`<${documentationUrl}>; rel="deprecation"`);
}
res.setHeader('Link', links.join(', '));
if (sunsetDate) {
res.setHeader('Sunset', new Date(sunsetDate).toUTCString());
}
next();
};
};
export default deprecatedRoute;

View File

@@ -0,0 +1,19 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface {
name = 'RenameBlacklistToBlocklist1746900000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "blacklist" RENAME TO "blocklist"`);
await queryRunner.query(
`ALTER TABLE "blocklist" RENAME COLUMN "blacklistedTags" TO "blocklistedTags"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "blocklist" RENAME COLUMN "blocklistedTags" TO "blacklistedTags"`
);
await queryRunner.query(`ALTER TABLE "blocklist" RENAME TO "blacklist"`);
}
}

View File

@@ -0,0 +1,66 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface {
name = 'RenameBlacklistToBlocklist1746900000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "temporary_blocklist" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"mediaType" varchar NOT NULL,
"title" varchar,
"tmdbId" integer NOT NULL,
"blocklistedTags" varchar,
"createdAt" datetime NOT NULL DEFAULT (datetime('now')),
"userId" integer,
"mediaId" integer,
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"),
CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"),
CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
)
`);
await queryRunner.query(`
INSERT INTO "temporary_blocklist" ("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId")
SELECT "id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId" FROM "blacklist"
`);
await queryRunner.query(`DROP TABLE "blacklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blocklist" RENAME TO "blocklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blocklist" ("tmdbId")`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "blocklist" RENAME TO "blacklist"`);
await queryRunner.query(`
CREATE TABLE "temporary_blacklist" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"mediaType" varchar NOT NULL,
"title" varchar,
"tmdbId" integer NOT NULL,
"blacklistedTags" varchar,
"createdAt" datetime NOT NULL DEFAULT (datetime('now')),
"userId" integer,
"mediaId" integer,
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"),
CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"),
CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
)
`);
await queryRunner.query(`
INSERT INTO "temporary_blacklist" ("id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId")
SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "blacklist"
`);
await queryRunner.query(`DROP TABLE "blacklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId")`
);
}
}

View File

@@ -1,8 +1,8 @@
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist';
import { Blocklist } from '@server/entity/Blocklist';
import Media from '@server/entity/Media';
import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces';
import type { BlocklistResultsResponse } from '@server/interfaces/api/blocklistInterfaces';
import { Permission } from '@server/lib/permissions';
import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth';
@@ -10,53 +10,53 @@ import { Router } from 'express';
import { EntityNotFoundError, QueryFailedError } from 'typeorm';
import { z } from 'zod';
const blacklistRoutes = Router();
const blocklistRoutes = Router();
export const blacklistAdd = z.object({
export const blocklistAdd = z.object({
tmdbId: z.coerce.number(),
mediaType: z.nativeEnum(MediaType),
title: z.coerce.string().optional(),
user: z.coerce.number(),
});
const blacklistGet = z.object({
const blocklistGet = z.object({
take: z.coerce.number().int().positive().default(25),
skip: z.coerce.number().int().nonnegative().default(0),
search: z.string().optional(),
filter: z.enum(['all', 'manual', 'blacklistedTags']).optional(),
filter: z.enum(['all', 'manual', 'blocklistedTags']).optional(),
});
blacklistRoutes.get(
blocklistRoutes.get(
'/',
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
isAuthenticated([Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
const { take, skip, search, filter } = blacklistGet.parse(req.query);
const { take, skip, search, filter } = blocklistGet.parse(req.query);
try {
let query = getRepository(Blacklist)
.createQueryBuilder('blacklist')
.leftJoinAndSelect('blacklist.user', 'user')
let query = getRepository(Blocklist)
.createQueryBuilder('blocklist')
.leftJoinAndSelect('blocklist.user', 'user')
.where('1 = 1'); // Allow use of andWhere later
switch (filter) {
case 'manual':
query = query.andWhere('blacklist.blacklistedTags IS NULL');
query = query.andWhere('blocklist.blocklistedTags IS NULL');
break;
case 'blacklistedTags':
query = query.andWhere('blacklist.blacklistedTags IS NOT NULL');
case 'blocklistedTags':
query = query.andWhere('blocklist.blocklistedTags IS NOT NULL');
break;
}
if (search) {
query = query.andWhere('blacklist.title like :title', {
query = query.andWhere('blocklist.title like :title', {
title: `%${search}%`,
});
}
const [blacklistedItems, itemsCount] = await query
.orderBy('blacklist.createdAt', 'DESC')
const [blocklistedItems, itemsCount] = await query
.orderBy('blocklist.createdAt', 'DESC')
.take(take)
.skip(skip)
.getManyAndCount();
@@ -68,35 +68,35 @@ blacklistRoutes.get(
results: itemsCount,
page: Math.ceil(skip / take) + 1,
},
results: blacklistedItems,
} as BlacklistResultsResponse);
results: blocklistedItems,
} as BlocklistResultsResponse);
} catch (error) {
logger.error('Something went wrong while retrieving blacklisted items', {
label: 'Blacklist',
logger.error('Something went wrong while retrieving blocklisted items', {
label: 'Blocklist',
errorMessage: error.message,
});
return next({
status: 500,
message: 'Unable to retrieve blacklisted items.',
message: 'Unable to retrieve blocklisted items.',
});
}
}
);
blacklistRoutes.get(
blocklistRoutes.get(
'/:id',
isAuthenticated([Permission.MANAGE_BLACKLIST], {
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const blacklisteRepository = getRepository(Blacklist);
const blocklisteRepository = getRepository(Blocklist);
const blacklistItem = await blacklisteRepository.findOneOrFail({
const blocklistItem = await blocklisteRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
});
return res.status(200).send(blacklistItem);
return res.status(200).send(blocklistItem);
} catch (e) {
if (e instanceof EntityNotFoundError) {
return next({
@@ -109,17 +109,17 @@ blacklistRoutes.get(
}
);
blacklistRoutes.post(
blocklistRoutes.post(
'/',
isAuthenticated([Permission.MANAGE_BLACKLIST], {
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const values = blacklistAdd.parse(req.body);
const values = blocklistAdd.parse(req.body);
await Blacklist.addToBlacklist({
blacklistRequest: values,
await Blocklist.addToBlocklist({
blocklistRequest: values,
});
return res.status(201).send();
@@ -131,12 +131,12 @@ blacklistRoutes.post(
if (error instanceof QueryFailedError) {
switch (error.driverError.errno) {
case 19:
return next({ status: 412, message: 'Item already blacklisted' });
return next({ status: 412, message: 'Item already blocklisted' });
default:
logger.warn('Something wrong with data blacklist', {
logger.warn('Something wrong with data blocklist', {
tmdbId: req.body.tmdbId,
mediaType: req.body.mediaType,
label: 'Blacklist',
label: 'Blocklist',
});
return next({ status: 409, message: 'Something wrong' });
}
@@ -147,20 +147,20 @@ blacklistRoutes.post(
}
);
blacklistRoutes.delete(
blocklistRoutes.delete(
'/:id',
isAuthenticated([Permission.MANAGE_BLACKLIST], {
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const blacklisteRepository = getRepository(Blacklist);
const blocklisteRepository = getRepository(Blocklist);
const blacklistItem = await blacklisteRepository.findOneOrFail({
const blocklistItem = await blocklisteRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
});
await blacklisteRepository.remove(blacklistItem);
await blocklisteRepository.remove(blocklistItem);
const mediaRepository = getRepository(Media);
@@ -183,4 +183,4 @@ blacklistRoutes.delete(
}
);
export default blacklistRoutes;
export default blocklistRoutes;

View File

@@ -12,6 +12,7 @@ import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { checkUser, isAuthenticated } from '@server/middleware/auth';
import deprecatedRoute from '@server/middleware/deprecation';
import { mapProductionCompany } from '@server/models/Movie';
import { mapNetwork } from '@server/models/Tv';
import { mapWatchProviderDetails } from '@server/models/common';
@@ -28,7 +29,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 blocklistRoutes from './blocklist';
import collectionRoutes from './collection';
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
import issueRoutes from './issue';
@@ -151,7 +152,17 @@ 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('/blocklist', isAuthenticated(), blocklistRoutes);
router.use(
'/blacklist',
isAuthenticated(),
deprecatedRoute({
oldPath: '/api/v1/blacklist',
newPath: '/api/v1/blocklist',
sunsetDate: '2026-06-01',
}),
blocklistRoutes
);
router.use('/movie', isAuthenticated(), movieRoutes);
router.use('/tv', isAuthenticated(), tvRoutes);
router.use('/media', isAuthenticated(), mediaRoutes);

View File

@@ -8,7 +8,7 @@ import {
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import {
BlacklistedMediaError,
BlocklistedMediaError,
DuplicateMediaRequestError,
MediaRequest,
NoSeasonsAvailableError,
@@ -326,7 +326,7 @@ requestRoutes.post<never, MediaRequest, MediaRequestBody>(
return next({ status: 409, message: error.message });
case NoSeasonsAvailableError:
return next({ status: 202, message: error.message });
case BlacklistedMediaError:
case BlocklistedMediaError:
return next({ status: 403, message: error.message });
default:
return next({ status: 500, message: error.message });

View File

@@ -1,4 +1,4 @@
import BlacklistedTagsBadge from '@app/components/BlacklistedTagsBadge';
import BlocklistedTagsBadge from '@app/components/BlocklistedTagsBadge';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
@@ -20,9 +20,9 @@ import {
TrashIcon,
} from '@heroicons/react/24/solid';
import type {
BlacklistItem,
BlacklistResultsResponse,
} from '@server/interfaces/api/blacklistInterfaces';
BlocklistItem,
BlocklistResultsResponse,
} from '@server/interfaces/api/blocklistInterfaces';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
@@ -35,31 +35,31 @@ 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.',
const messages = defineMessages('components.Blocklist', {
blocklistsettings: 'Blocklist Settings',
blocklistSettingsDescription: 'Manage blocklisted media.',
mediaName: 'Name',
mediaType: 'Type',
mediaTmdbId: 'tmdb Id',
blacklistdate: 'date',
blacklistedby: '{date} by {user}',
blacklistNotFoundError: '<strong>{title}</strong> is not blacklisted.',
blocklistdate: 'date',
blocklistedby: '{date} by {user}',
blocklistNotFoundError: '<strong>{title}</strong> is not blocklisted.',
filterManual: 'Manual',
filterBlacklistedTags: 'Blacklisted Tags',
showAllBlacklisted: 'Show All Blacklisted Media',
filterBlocklistedTags: 'Blocklisted Tags',
showAllBlocklisted: 'Show All Blocklisted Media',
});
enum Filter {
ALL = 'all',
MANUAL = 'manual',
BLACKLISTEDTAGS = 'blacklistedTags',
BLOCKLISTEDTAGS = 'blocklistedTags',
}
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
const Blacklist = () => {
const Blocklist = () => {
const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const [searchFilter, debouncedSearchFilter, setSearchFilter] =
useDebouncedState('');
@@ -75,8 +75,8 @@ const Blacklist = () => {
data,
error,
mutate: revalidate,
} = useSWR<BlacklistResultsResponse>(
`/api/v1/blacklist/?take=${currentPageSize}&skip=${
} = useSWR<BlocklistResultsResponse>(
`/api/v1/blocklist/?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
}&filter=${currentFilter}${
debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
@@ -107,9 +107,9 @@ const Blacklist = () => {
return (
<>
<PageTitle title={[intl.formatMessage(globalMessages.blacklist)]} />
<PageTitle title={[intl.formatMessage(globalMessages.blocklist)]} />
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
<Header>{intl.formatMessage(globalMessages.blacklist)}</Header>
<Header>{intl.formatMessage(globalMessages.blocklist)}</Header>
<div className="mt-2 flex flex-grow flex-col sm:flex-row lg:flex-grow-0">
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 lg:flex-grow-0">
@@ -137,8 +137,8 @@ const Blacklist = () => {
<option value="manual">
{intl.formatMessage(messages.filterManual)}
</option>
<option value="blacklistedTags">
{intl.formatMessage(messages.filterBlacklistedTags)}
<option value="blocklistedTags">
{intl.formatMessage(messages.filterBlocklistedTags)}
</option>
</select>
</div>
@@ -170,16 +170,16 @@ const Blacklist = () => {
buttonType="primary"
onClick={() => setCurrentFilter(Filter.ALL)}
>
{intl.formatMessage(messages.showAllBlacklisted)}
{intl.formatMessage(messages.showAllBlocklisted)}
</Button>
</div>
)}
</div>
) : (
data.results.map((item: BlacklistItem) => {
data.results.map((item: BlocklistItem) => {
return (
<div className="py-2" key={`request-list-${item.tmdbId}`}>
<BlacklistedItem item={item} revalidateList={revalidate} />
<BlocklistedItem item={item} revalidateList={revalidate} />
</div>
);
})
@@ -260,14 +260,14 @@ const Blacklist = () => {
);
};
export default Blacklist;
export default Blocklist;
interface BlacklistedItemProps {
item: BlacklistItem;
interface BlocklistedItemProps {
item: BlocklistItem;
revalidateList: () => void;
}
const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => {
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const { addToast } = useToasts();
const { ref, inView } = useInView({
@@ -293,15 +293,15 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
);
}
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
const removeFromBlocklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
try {
await axios.delete(`/api/v1/blacklist/${tmdbId}`);
await axios.delete(`/api/v1/blocklist/${tmdbId}`);
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -309,7 +309,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
{ appearance: 'success', autoDismiss: true }
);
} catch {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
@@ -389,17 +389,17 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
<div className="card-field">
<span className="card-field-name">Status</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
{intl.formatMessage(globalMessages.blocklisted)}
</Badge>
</div>
{item.createdAt && (
<div className="card-field">
<span className="card-field-name">
{intl.formatMessage(globalMessages.blacklisted)}
{intl.formatMessage(globalMessages.blocklisted)}
</span>
<span className="flex truncate text-sm text-gray-300">
{intl.formatMessage(messages.blacklistedby, {
{intl.formatMessage(messages.blocklistedby, {
date: (
<FormattedRelativeTime
value={Math.floor(
@@ -426,9 +426,9 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
</span>
</span>
</Link>
) : item.blacklistedTags ? (
) : item.blocklistedTags ? (
<span className="ml-1">
<BlacklistedTagsBadge data={item} />
<BlocklistedTagsBadge data={item} />
</span>
) : (
<span className="ml-1 truncate text-sm font-semibold">
@@ -457,10 +457,10 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
</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) && (
{hasPermission(Permission.MANAGE_BLOCKLIST) && (
<ConfirmButton
onClick={() =>
removeFromBlacklist(
removeFromBlocklist(
item.tmdbId,
title && (isMovie(title) ? title.title : title.name)
)
@@ -474,7 +474,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
>
<TrashIcon />
<span>
{intl.formatMessage(globalMessages.removefromBlacklist)}
{intl.formatMessage(globalMessages.removefromBlocklist)}
</span>
</ConfirmButton>
)}

View File

@@ -1,4 +1,4 @@
import BlacklistedTagsBadge from '@app/components/BlacklistedTagsBadge';
import BlocklistedTagsBadge from '@app/components/BlocklistedTagsBadge';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
@@ -7,7 +7,7 @@ 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 type { Blocklist } from '@server/entity/Blocklist';
import axios from 'axios';
import Link from 'next/link';
import { useState } from 'react';
@@ -15,37 +15,37 @@ import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
const messages = defineMessages('component.BlacklistBlock', {
blacklistedby: 'Blacklisted By',
blacklistdate: 'Blacklisted date',
const messages = defineMessages('component.BlocklistBlock', {
blocklistedby: 'Blocklisted By',
blocklistdate: 'Blocklisted date',
});
interface BlacklistBlockProps {
interface BlocklistBlockProps {
tmdbId: number;
onUpdate?: () => void;
onDelete?: () => void;
}
const BlacklistBlock = ({
const BlocklistBlock = ({
tmdbId,
onUpdate,
onDelete,
}: BlacklistBlockProps) => {
}: BlocklistBlockProps) => {
const { user } = useUser();
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const { addToast } = useToasts();
const { data } = useSWR<Blacklist>(`/api/v1/blacklist/${tmdbId}`);
const { data } = useSWR<Blocklist>(`/api/v1/blocklist/${tmdbId}`);
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
const removeFromBlocklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
try {
await axios.delete('/api/v1/blacklist/' + tmdbId);
await axios.delete('/api/v1/blocklist/' + tmdbId);
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -53,7 +53,7 @@ const BlacklistBlock = ({
{ appearance: 'success', autoDismiss: true }
);
} catch {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
@@ -80,7 +80,7 @@ const BlacklistBlock = ({
<div className="white mb-1 flex flex-nowrap">
{data.user ? (
<>
<Tooltip content={intl.formatMessage(messages.blacklistedby)}>
<Tooltip content={intl.formatMessage(messages.blocklistedby)}>
<UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip>
<span className="w-40 truncate md:w-auto">
@@ -97,23 +97,23 @@ const BlacklistBlock = ({
</Link>
</span>
</>
) : data.blacklistedTags ? (
) : data.blocklistedTags ? (
<>
<span className="w-40 truncate md:w-auto">
{intl.formatMessage(messages.blacklistedby)}:&nbsp;
{intl.formatMessage(messages.blocklistedby)}:&nbsp;
</span>
<BlacklistedTagsBadge data={data} />
<BlocklistedTagsBadge data={data} />
</>
) : null}
</div>
</div>
<div className="ml-2 flex flex-shrink-0 flex-wrap">
<Tooltip
content={intl.formatMessage(globalMessages.removefromBlacklist)}
content={intl.formatMessage(globalMessages.removefromBlocklist)}
>
<Button
buttonType="danger"
onClick={() => removeFromBlacklist(data.tmdbId, data.title)}
onClick={() => removeFromBlocklist(data.tmdbId, data.title)}
disabled={isUpdating}
>
<TrashIcon className="icon-sm" />
@@ -125,12 +125,12 @@ const BlacklistBlock = ({
<div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5">
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
{intl.formatMessage(globalMessages.blocklisted)}
</Badge>
</div>
</div>
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
<Tooltip content={intl.formatMessage(messages.blacklistdate)}>
<Tooltip content={intl.formatMessage(messages.blocklistdate)}>
<CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span>
@@ -146,4 +146,4 @@ const BlacklistBlock = ({
);
};
export default BlacklistBlock;
export default BlocklistBlock;

View File

@@ -8,7 +8,7 @@ import axios from 'axios';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
interface BlacklistModalProps {
interface BlocklistModalProps {
tmdbId: number;
type: 'movie' | 'tv' | 'collection';
show: boolean;
@@ -17,8 +17,8 @@ interface BlacklistModalProps {
isUpdating?: boolean;
}
const messages = defineMessages('component.BlacklistModal', {
blacklisting: 'Blacklisting',
const messages = defineMessages('component.BlocklistModal', {
blocklisting: 'Blocklisting',
});
const isMovie = (
@@ -28,14 +28,14 @@ const isMovie = (
return (movie as MovieDetails).title !== undefined;
};
const BlacklistModal = ({
const BlocklistModal = ({
tmdbId,
type,
show,
onComplete,
onCancel,
isUpdating,
}: BlacklistModalProps) => {
}: BlocklistModalProps) => {
const intl = useIntl();
const [data, setData] = useState<TvDetails | MovieDetails | null>(null);
const [error, setError] = useState(null);
@@ -67,7 +67,7 @@ const BlacklistModal = ({
<Modal
loading={!data && !error}
backgroundClickable
title={`${intl.formatMessage(globalMessages.blacklist)} ${
title={`${intl.formatMessage(globalMessages.blocklist)} ${
isMovie(data)
? intl.formatMessage(globalMessages.movie)
: intl.formatMessage(globalMessages.tvshow)
@@ -77,8 +77,8 @@ const BlacklistModal = ({
onOk={onComplete}
okText={
isUpdating
? intl.formatMessage(messages.blacklisting)
: intl.formatMessage(globalMessages.blacklist)
? intl.formatMessage(messages.blocklisting)
: intl.formatMessage(globalMessages.blocklist)
}
okButtonType="danger"
okDisabled={isUpdating}
@@ -88,4 +88,4 @@ const BlacklistModal = ({
);
};
export default BlacklistModal;
export default BlocklistModal;

View File

@@ -2,31 +2,31 @@ import Badge from '@app/components/Common/Badge';
import Tooltip from '@app/components/Common/Tooltip';
import defineMessages from '@app/utils/defineMessages';
import { TagIcon } from '@heroicons/react/20/solid';
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
import type { BlocklistItem } from '@server/interfaces/api/blocklistInterfaces';
import type { Keyword } from '@server/models/common';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const messages = defineMessages('components.Settings', {
blacklistedTagsText: 'Blacklisted Tags',
blocklistedTagsText: 'Blocklisted Tags',
});
interface BlacklistedTagsBadgeProps {
data: BlacklistItem;
interface BlocklistedTagsBadgeProps {
data: BlocklistItem;
}
const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
const [tagNamesBlacklistedFor, setTagNamesBlacklistedFor] =
const BlocklistedTagsBadge = ({ data }: BlocklistedTagsBadgeProps) => {
const [tagNamesBlocklistedFor, setTagNamesBlocklistedFor] =
useState<string>('Loading...');
const intl = useIntl();
useEffect(() => {
if (!data.blacklistedTags) {
if (!data.blocklistedTags) {
return;
}
const keywordIds = data.blacklistedTags.slice(1, -1).split(',');
const keywordIds = data.blocklistedTags.slice(1, -1).split(',');
Promise.all(
keywordIds.map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>(
@@ -35,13 +35,13 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
return data?.name || `[Invalid: ${keywordId}]`;
})
).then((keywords) => {
setTagNamesBlacklistedFor(keywords.join(', '));
setTagNamesBlocklistedFor(keywords.join(', '));
});
}, [data.blacklistedTags]);
}, [data.blocklistedTags]);
return (
<Tooltip
content={tagNamesBlacklistedFor}
content={tagNamesBlocklistedFor}
tooltipConfig={{ followCursor: false }}
>
<Badge
@@ -49,10 +49,10 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
className="items-center border border-red-500 !text-red-400"
>
<TagIcon className="mr-1 h-4" />
{intl.formatMessage(messages.blacklistedTagsText)}
{intl.formatMessage(messages.blocklistedTagsText)}
</Badge>
</Tooltip>
);
};
export default BlacklistedTagsBadge;
export default BlocklistedTagsBadge;

View File

@@ -26,19 +26,19 @@ import { components } from 'react-select';
import AsyncSelect from 'react-select/async';
const messages = defineMessages('components.Settings', {
copyBlacklistedTags: 'Copied blacklisted tags to clipboard.',
copyBlacklistedTagsTip: 'Copy blacklisted tag configuration',
copyBlacklistedTagsEmpty: 'Nothing to copy',
importBlacklistedTagsTip: 'Import blacklisted tag configuration',
clearBlacklistedTagsConfirm:
'Are you sure you want to clear the blacklisted tags?',
copyBlocklistedTags: 'Copied blocklisted tags to clipboard.',
copyBlocklistedTagsTip: 'Copy blocklisted tag configuration',
copyBlocklistedTagsEmpty: 'Nothing to copy',
importBlocklistedTagsTip: 'Import blocklisted tag configuration',
clearBlocklistedTagsConfirm:
'Are you sure you want to clear the blocklisted tags?',
yes: 'Yes',
no: 'No',
searchKeywords: 'Search keywords…',
starttyping: 'Starting typing to search.',
nooptions: 'No results.',
blacklistedTagImportTitle: 'Import Blacklisted Tag Configuration',
blacklistedTagImportInstructions: 'Paste blacklist tag configuration below.',
blocklistedTagImportTitle: 'Import Blocklisted Tag Configuration',
blocklistedTagImportInstructions: 'Paste blocklist tag configuration below.',
valueRequired: 'You must provide a value.',
noSpecialCharacters:
'Configuration must be a comma delimited list of TMDB keyword ids, and must not start or end with a comma.',
@@ -50,13 +50,13 @@ type SingleVal = {
value: number;
};
type BlacklistedTagsSelectorProps = {
type BlocklistedTagsSelectorProps = {
defaultValue?: string;
};
const BlacklistedTagsSelector = ({
const BlocklistedTagsSelector = ({
defaultValue,
}: BlacklistedTagsSelectorProps) => {
}: BlocklistedTagsSelectorProps) => {
const { setFieldValue } = useFormikContext();
const [value, setValue] = useState<string | undefined>(defaultValue);
const intl = useIntl();
@@ -68,7 +68,7 @@ const BlacklistedTagsSelector = ({
const strVal = value?.map((v) => v.value).join(',');
setSelectorValue(value);
setValue(strVal);
setFieldValue('blacklistedTags', strVal);
setFieldValue('blocklistedTags', strVal);
},
[setSelectorValue, setValue, setFieldValue]
);
@@ -91,15 +91,15 @@ const BlacklistedTagsSelector = ({
<CopyButton
textToCopy={value ?? ''}
disabled={copyDisabled}
toastMessage={intl.formatMessage(messages.copyBlacklistedTags)}
toastMessage={intl.formatMessage(messages.copyBlocklistedTags)}
tooltipContent={intl.formatMessage(
copyDisabled
? messages.copyBlacklistedTagsEmpty
: messages.copyBlacklistedTagsTip
? messages.copyBlocklistedTagsEmpty
: messages.copyBlocklistedTagsTip
)}
tooltipConfig={{ followCursor: false }}
/>
<BlacklistedTagsImportButton setSelector={update} />
<BlocklistedTagsImportButton setSelector={update} />
</>
);
};
@@ -162,7 +162,7 @@ const ControlledKeywordSelector = ({
return (
<AsyncSelect
key={`keyword-select-blacklistedTags`}
key={`keyword-select-blocklistedTags`}
inputId="data"
isMulti
className="react-select-container"
@@ -181,13 +181,13 @@ const ControlledKeywordSelector = ({
);
};
type BlacklistedTagsImportButtonProps = {
type BlocklistedTagsImportButtonProps = {
setSelector: (value: MultiValue<SingleVal>) => void;
};
const BlacklistedTagsImportButton = ({
const BlocklistedTagsImportButton = ({
setSelector,
}: BlacklistedTagsImportButtonProps) => {
}: BlocklistedTagsImportButtonProps) => {
const [show, setShow] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const intl = useIntl();
@@ -218,17 +218,17 @@ const BlacklistedTagsImportButton = ({
show={show}
>
<Modal
title={intl.formatMessage(messages.blacklistedTagImportTitle)}
title={intl.formatMessage(messages.blocklistedTagImportTitle)}
okText="Confirm"
onOk={onConfirm}
onCancel={() => setShow(false)}
>
<BlacklistedTagImportForm ref={formRef} setSelector={setSelector} />
<BlocklistedTagImportForm ref={formRef} setSelector={setSelector} />
</Modal>
</Transition>
<Tooltip
content={intl.formatMessage(messages.importBlacklistedTagsTip)}
content={intl.formatMessage(messages.importBlocklistedTagsTip)}
tooltipConfig={{ followCursor: false }}
>
<button className="input-action" onClick={onClick} type="button">
@@ -239,11 +239,11 @@ const BlacklistedTagsImportButton = ({
);
};
type BlacklistedTagImportFormProps = BlacklistedTagsImportButtonProps;
type BlocklistedTagImportFormProps = BlocklistedTagsImportButtonProps;
const BlacklistedTagImportForm = forwardRef<
const BlocklistedTagImportForm = forwardRef<
Partial<HTMLFormElement>,
BlacklistedTagImportFormProps
BlocklistedTagImportFormProps
>((props, ref) => {
const { setSelector } = props;
const intl = useIntl();
@@ -306,7 +306,7 @@ const BlacklistedTagImportForm = forwardRef<
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="value">
{intl.formatMessage(messages.blacklistedTagImportInstructions)}
{intl.formatMessage(messages.blocklistedTagImportInstructions)}
</label>
<textarea
id="value"
@@ -392,7 +392,7 @@ const VerifyClearIndicator = <
show={show}
>
<Modal
subTitle={intl.formatMessage(messages.clearBlacklistedTagsConfirm)}
subTitle={intl.formatMessage(messages.clearBlocklistedTagsConfirm)}
okText={intl.formatMessage(messages.yes)}
cancelText={intl.formatMessage(messages.no)}
onOk={clearValue}
@@ -406,4 +406,4 @@ const VerifyClearIndicator = <
);
};
export default BlacklistedTagsSelector;
export default BlocklistedTagsSelector;

View File

@@ -188,8 +188,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
);
}
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
@@ -349,8 +349,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
isEmpty={data.parts.length === 0}
items={data.parts
.filter((title) => {
if (!blacklistVisibility)
return title.mediaInfo?.status !== MediaStatus.BLACKLISTED;
if (!blocklistVisibility)
return title.mediaInfo?.status !== MediaStatus.BLOCKLISTED;
return title;
})
.map((title) => (

View File

@@ -37,8 +37,8 @@ const ListView = ({
const { hasPermission } = useUser();
useVerticalScroll(onScrollBottom, !isLoading && !isEmpty && !isReachingEnd);
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
@@ -66,10 +66,10 @@ const ListView = ({
})}
{items
?.filter((title) => {
if (!blacklistVisibility)
if (!blocklistVisibility)
return (
(title as TvResult | MovieResult).mediaInfo?.status !==
MediaStatus.BLACKLISTED
MediaStatus.BLOCKLISTED
);
return title;
})

View File

@@ -50,7 +50,7 @@ const StatusBadgeMini = ({
);
indicatorIcon = <BellIcon />;
break;
case MediaStatus.BLACKLISTED:
case MediaStatus.BLOCKLISTED:
badgeStyle.push('bg-red-500 border-white-400 ring-white-400 text-white');
indicatorIcon = <EyeSlashIcon />;
break;

View File

@@ -100,14 +100,14 @@ const MobileMenu = ({
activeRegExp: /^\/requests/,
},
{
href: '/blacklist',
content: intl.formatMessage(menuMessages.blacklist),
href: '/blocklist',
content: intl.formatMessage(menuMessages.blocklist),
svgIcon: <EyeSlashIcon className="h-6 w-6" />,
svgIconSelected: <FilledEyeSlashIcon className="h-6 w-6" />,
activeRegExp: /^\/blacklist/,
activeRegExp: /^\/blocklist/,
requiredPermission: [
Permission.MANAGE_BLACKLIST,
Permission.VIEW_BLACKLIST,
Permission.MANAGE_BLOCKLIST,
Permission.VIEW_BLOCKLIST,
],
permissionType: 'or',
},

View File

@@ -27,7 +27,7 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
browsemovies: 'Movies',
browsetv: 'Series',
requests: 'Requests',
blacklist: 'Blacklist',
blocklist: 'Blocklist',
issues: 'Issues',
users: 'Users',
settings: 'Settings',
@@ -79,13 +79,13 @@ const SidebarLinks: SidebarLinkProps[] = [
activeRegExp: /^\/requests/,
},
{
href: '/blacklist',
messagesKey: 'blacklist',
href: '/blocklist',
messagesKey: 'blocklist',
svgIcon: <EyeSlashIcon className="mr-3 h-6 w-6" />,
activeRegExp: /^\/blacklist/,
activeRegExp: /^\/blocklist/,
requiredPermission: [
Permission.MANAGE_BLACKLIST,
Permission.VIEW_BLACKLIST,
Permission.MANAGE_BLOCKLIST,
Permission.VIEW_BLOCKLIST,
],
permissionType: 'or',
},

View File

@@ -1,4 +1,4 @@
import BlacklistBlock from '@app/components/BlacklistBlock';
import BlocklistBlock from '@app/components/BlocklistBlock';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton';
@@ -314,13 +314,13 @@ const ManageSlideOver = ({
</div>
</div>
)}
{data.mediaInfo?.status === MediaStatus.BLACKLISTED && (
{data.mediaInfo?.status === MediaStatus.BLOCKLISTED && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(globalMessages.blacklist)}
{intl.formatMessage(globalMessages.blocklist)}
</h3>
<div className="overflow-hidden rounded-md border border-gray-700 shadow">
<BlacklistBlock
<BlocklistBlock
tmdbId={data.mediaInfo.tmdbId}
onUpdate={() => revalidate()}
onDelete={() => onClose()}
@@ -651,7 +651,7 @@ const ManageSlideOver = ({
)}
{hasPermission(Permission.ADMIN) &&
data?.mediaInfo &&
data.mediaInfo.status !== MediaStatus.BLACKLISTED && (
data.mediaInfo.status !== MediaStatus.BLOCKLISTED && (
<div>
<h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalAdvanced)}

View File

@@ -74,11 +74,11 @@ const MediaSlider = ({
);
}
if (settings.currentSettings.hideBlacklisted) {
if (settings.currentSettings.hideBlocklisted) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.BLACKLISTED
i.mediaInfo?.status !== MediaStatus.BLOCKLISTED
);
}
@@ -102,18 +102,18 @@ const MediaSlider = ({
return null;
}
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
const finalTitles = titles
.slice(0, 20)
.filter((title) => {
if (!blacklistVisibility)
if (!blocklistVisibility)
return (
(title as TvResult | MovieResult).mediaInfo?.status !==
MediaStatus.BLACKLISTED
MediaStatus.BLOCKLISTED
);
return title;
})

View File

@@ -5,7 +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 BlocklistModal from '@app/components/BlocklistModal';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
@@ -128,9 +128,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!movie?.onUserWatchlist
);
const [isBlacklistUpdating, setIsBlacklistUpdating] =
const [isBlocklistUpdating, setIsBlocklistUpdating] =
useState<boolean>(false);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const [showBlocklistModal, setShowBlocklistModal] = useState(false);
const { addToast } = useToasts();
const {
@@ -161,8 +161,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
setShowManager(router.query.manage == '1' ? true : false);
}, [router.query.manage]);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
const closeBlocklistModal = useCallback(
() => setShowBlocklistModal(false),
[]
);
@@ -381,10 +381,10 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
};
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true);
setIsBlocklistUpdating(true);
try {
await axios.post('/api/v1/blacklist', {
await axios.post('/api/v1/blocklist', {
tmdbId: movie?.id,
mediaType: 'movie',
title: movie?.title,
@@ -393,7 +393,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
{intl.formatMessage(globalMessages.blocklistSuccess, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -406,7 +406,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
if (e?.response?.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
{intl.formatMessage(globalMessages.blocklistDuplicateError, {
title: movie?.title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -414,18 +414,18 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
}
setIsBlacklistUpdating(false);
closeBlacklistModal();
setIsBlocklistUpdating(false);
closeBlocklistModal();
};
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
const showHideButton = hasPermission([Permission.MANAGE_BLOCKLIST], {
type: 'or',
});
@@ -475,13 +475,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
revalidate={() => revalidate()}
show={showManager}
/>
<BlacklistModal
<BlocklistModal
tmdbId={data.id}
type="movie"
show={showBlacklistModal}
onCancel={closeBlacklistModal}
show={showBlocklistModal}
onCancel={closeBlocklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isBlacklistUpdating}
isUpdating={isBlocklistUpdating}
/>
<div className="media-header">
<div className="media-poster">
@@ -565,21 +565,21 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PENDING &&
data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
data?.mediaInfo?.status !== MediaStatus.BLOCKLISTED && (
<Tooltip
content={intl.formatMessage(globalMessages.addToBlacklist)}
content={intl.formatMessage(globalMessages.addToBlocklist)}
>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={() => setShowBlacklistModal(true)}
onClick={() => setShowBlocklistModal(true)}
>
<EyeSlashIcon />
</Button>
</Tooltip>
)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED &&
{data?.mediaInfo?.status !== MediaStatus.BLOCKLISTED &&
user?.userType !== UserType.PLEX && (
<>
{toggleWatchlist ? (

View File

@@ -78,13 +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.',
manageblocklist: 'Manage Blocklist',
manageblocklistDescription: 'Grant permission to manage blocklisted media.',
blocklistedItems: 'Blocklist media.',
blocklistedItemsDescription: 'Grant permission to blocklist media.',
viewblocklistedItems: 'View blocklisted media.',
viewblocklistedItemsDescription:
'Grant permission to view blocklisted media.',
});
interface PermissionEditProps {
@@ -340,18 +340,18 @@ export const PermissionEdit = ({
],
},
{
id: 'manageblacklist',
name: intl.formatMessage(messages.manageblacklist),
description: intl.formatMessage(messages.manageblacklistDescription),
permission: Permission.MANAGE_BLACKLIST,
id: 'manageblocklist',
name: intl.formatMessage(messages.manageblocklist),
description: intl.formatMessage(messages.manageblocklistDescription),
permission: Permission.MANAGE_BLOCKLIST,
children: [
{
id: 'viewblacklisteditems',
name: intl.formatMessage(messages.viewblacklistedItems),
id: 'viewblocklisteditems',
name: intl.formatMessage(messages.viewblocklistedItems),
description: intl.formatMessage(
messages.viewblacklistedItemsDescription
messages.viewblocklistedItemsDescription
),
permission: Permission.VIEW_BLACKLIST,
permission: Permission.VIEW_BLOCKLIST,
},
],
},

View File

@@ -298,7 +298,7 @@ const RequestButton = ({
type: 'or',
}) &&
media &&
media.status !== MediaStatus.BLACKLISTED &&
media.status !== MediaStatus.BLOCKLISTED &&
!isShowComplete
) {
buttons.push({
@@ -345,7 +345,7 @@ const RequestButton = ({
type: 'or',
}) &&
media &&
media.status4k !== MediaStatus.BLACKLISTED &&
media.status4k !== MediaStatus.BLOCKLISTED &&
!is4kShowComplete &&
settings.currentSettings.series4kEnabled
) {

View File

@@ -68,7 +68,7 @@ const CollectionRequestModal = ({
const getAllParts = (): number[] => {
return (data?.parts ?? [])
.filter((part) => part.mediaInfo?.status !== MediaStatus.BLACKLISTED)
.filter((part) => part.mediaInfo?.status !== MediaStatus.BLOCKLISTED)
.map((part) => part.id);
};
@@ -257,8 +257,8 @@ const CollectionRequestModal = ({
{ type: 'or' }
);
const blacklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST],
const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' }
);
@@ -360,9 +360,9 @@ const CollectionRequestModal = ({
<tbody className="divide-y divide-gray-700">
{data?.parts
.filter((part) => {
if (!blacklistVisibility)
if (!blocklistVisibility)
return (
part.mediaInfo?.status !== MediaStatus.BLACKLISTED
part.mediaInfo?.status !== MediaStatus.BLOCKLISTED
);
return part;
})
@@ -381,7 +381,7 @@ const CollectionRequestModal = ({
<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 &&
partMedia?.status === MediaStatus.BLOCKLISTED &&
'pointer-events-none opacity-50'
}`}
>
@@ -391,7 +391,7 @@ const CollectionRequestModal = ({
aria-checked={
(!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
MediaStatus.BLOCKLISTED) ||
isSelectedPart(part.id)
}
onClick={() => togglePart(part.id)}
@@ -403,7 +403,7 @@ const CollectionRequestModal = ({
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) ||
MediaStatus.BLOCKLISTED) ||
partRequest ||
(quota?.movie.limit &&
currentlyRemaining <= 0 &&
@@ -417,7 +417,7 @@ const CollectionRequestModal = ({
className={`${
(!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
MediaStatus.BLOCKLISTED) ||
partRequest ||
isSelectedPart(part.id)
? 'bg-indigo-500'
@@ -429,7 +429,7 @@ const CollectionRequestModal = ({
className={`${
(!!partMedia &&
partMedia.status !==
MediaStatus.BLACKLISTED) ||
MediaStatus.BLOCKLISTED) ||
partRequest ||
isSelectedPart(part.id)
? 'translate-x-5'
@@ -440,7 +440,7 @@ const CollectionRequestModal = ({
</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 &&
partMedia?.status === MediaStatus.BLOCKLISTED &&
'pointer-events-none opacity-50'
}`}
>
@@ -502,9 +502,9 @@ const CollectionRequestModal = ({
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{partMedia?.status === MediaStatus.BLACKLISTED && (
{partMedia?.status === MediaStatus.BLOCKLISTED && (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)}
{intl.formatMessage(globalMessages.blocklisted)}
</Badge>
)}
</td>

View File

@@ -34,7 +34,7 @@ const Search = () => {
{
query: router.query.query,
},
{ hideAvailable: false, hideBlacklisted: false }
{ hideAvailable: false, hideBlocklisted: false }
);
if (error) {

View File

@@ -89,7 +89,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
'download-sync': 'Download Sync',
'download-sync-reset': 'Download Sync Reset',
'image-cache-cleanup': 'Image Cache Cleanup',
'process-blacklisted-tags': 'Process Blacklisted Tags',
'process-blocklisted-tags': 'Process Blocklisted Tags',
editJobSchedule: 'Modify Job',
jobScheduleEditSaved: 'Job edited successfully!',
jobScheduleEditFailed: 'Something went wrong while saving the job.',

View File

@@ -1,4 +1,4 @@
import BlacklistedTagsSelector from '@app/components/BlacklistedTagsSelector';
import BlocklistedTagsSelector from '@app/components/BlocklistedTagsSelector';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
@@ -38,17 +38,17 @@ const messages = defineMessages('components.Settings.SettingsMain', {
discoverRegionTip: 'Filter content by regional availability',
originallanguage: 'Discover Language',
originallanguageTip: 'Filter content by original language',
blacklistedTags: 'Blacklist Content with Tags',
blacklistedTagsTip:
'Automatically add content with tags to the blacklist using the "Process Blacklisted Tags" job',
blacklistedTagsLimit: 'Limit Content Blacklisted per Tag',
blacklistedTagsLimitTip:
'The "Process Blacklisted Tags" job will blacklist this many pages into each sort. Larger numbers will create a more accurate blacklist, but use more space.',
blocklistedTags: 'Blocklist Content with Tags',
blocklistedTagsTip:
'Automatically add content with tags to the blocklist using the "Process Blocklisted Tags" job',
blocklistedTagsLimit: 'Limit Content Blocklisted per Tag',
blocklistedTagsLimitTip:
'The "Process Blocklisted Tags" job will blocklist this many pages into each sort. Larger numbers will create a more accurate blocklist, but use more space.',
streamingRegion: 'Streaming Region',
streamingRegionTip: 'Show streaming sites by regional availability',
hideBlacklisted: 'Hide Blacklisted Items',
hideBlacklistedTip:
'Hide blacklisted items from discover pages for all users with the "Manage Blacklist" permission',
hideBlocklisted: 'Hide Blocklisted Items',
hideBlocklistedTip:
'Hide blocklisted items from discover pages for all users with the "Manage Blocklist" permission',
toastApiKeySuccess: 'New API key generated successfully!',
toastApiKeyFailure: 'Something went wrong while generating a new API key.',
toastSettingsSuccess: 'Settings saved successfully!',
@@ -101,7 +101,7 @@ const SettingsMain = () => {
intl.formatMessage(messages.validationApplicationUrlTrailingSlash),
(value) => !value || !value.endsWith('/')
),
blacklistedTagsLimit: Yup.number()
blocklistedTagsLimit: Yup.number()
.test(
'positive',
'Number must be greater than 0.',
@@ -164,13 +164,13 @@ const SettingsMain = () => {
applicationTitle: data?.applicationTitle,
applicationUrl: data?.applicationUrl,
hideAvailable: data?.hideAvailable,
hideBlacklisted: data?.hideBlacklisted,
hideBlocklisted: data?.hideBlocklisted,
locale: data?.locale ?? 'en',
discoverRegion: data?.discoverRegion,
originalLanguage: data?.originalLanguage,
streamingRegion: data?.streamingRegion || 'US',
blacklistedTags: data?.blacklistedTags,
blacklistedTagsLimit: data?.blacklistedTagsLimit || 50,
blocklistedTags: data?.blocklistedTags,
blocklistedTagsLimit: data?.blocklistedTagsLimit || 50,
partialRequestsEnabled: data?.partialRequestsEnabled,
enableSpecialEpisodes: data?.enableSpecialEpisodes,
cacheImages: data?.cacheImages,
@@ -184,13 +184,13 @@ const SettingsMain = () => {
applicationTitle: values.applicationTitle,
applicationUrl: values.applicationUrl,
hideAvailable: values.hideAvailable,
hideBlacklisted: values.hideBlacklisted,
hideBlocklisted: values.hideBlocklisted,
locale: values.locale,
discoverRegion: values.discoverRegion,
streamingRegion: values.streamingRegion,
originalLanguage: values.originalLanguage,
blacklistedTags: values.blacklistedTags,
blacklistedTagsLimit: values.blacklistedTagsLimit,
blocklistedTags: values.blocklistedTags,
blocklistedTagsLimit: values.blocklistedTagsLimit,
partialRequestsEnabled: values.partialRequestsEnabled,
enableSpecialEpisodes: values.enableSpecialEpisodes,
cacheImages: values.cacheImages,
@@ -403,44 +403,44 @@ const SettingsMain = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="blacklistedTags" className="text-label">
<span>{intl.formatMessage(messages.blacklistedTags)}</span>
<label htmlFor="blocklistedTags" className="text-label">
<span>{intl.formatMessage(messages.blocklistedTags)}</span>
<span className="label-tip">
{intl.formatMessage(messages.blacklistedTagsTip)}
{intl.formatMessage(messages.blocklistedTagsTip)}
</span>
</label>
<div className="form-input-area">
<div className="form-input-field relative z-10">
<BlacklistedTagsSelector
defaultValue={values.blacklistedTags}
<BlocklistedTagsSelector
defaultValue={values.blocklistedTags}
/>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="blacklistedTagsLimit" className="text-label">
<label htmlFor="blocklistedTagsLimit" className="text-label">
<span className="mr-2">
{intl.formatMessage(messages.blacklistedTagsLimit)}
{intl.formatMessage(messages.blocklistedTagsLimit)}
</span>
<SettingsBadge badgeType="advanced" />
<span className="label-tip">
{intl.formatMessage(messages.blacklistedTagsLimitTip)}
{intl.formatMessage(messages.blocklistedTagsLimitTip)}
</span>
</label>
<div className="form-input-area">
<Field
id="blacklistedTagsLimit"
name="blacklistedTagsLimit"
id="blocklistedTagsLimit"
name="blocklistedTagsLimit"
type="text"
inputMode="numeric"
className="short"
placeholder={50}
/>
{errors.blacklistedTagsLimit &&
touched.blacklistedTagsLimit &&
typeof errors.blacklistedTagsLimit === 'string' && (
{errors.blocklistedTagsLimit &&
touched.blocklistedTagsLimit &&
typeof errors.blocklistedTagsLimit === 'string' && (
<div className="error">
{errors.blacklistedTagsLimit}
{errors.blocklistedTagsLimit}
</div>
)}
</div>
@@ -467,23 +467,23 @@ const SettingsMain = () => {
</div>
</div>
<div className="form-row">
<label htmlFor="hideBlacklisted" className="checkbox-label">
<label htmlFor="hideBlocklisted" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.hideBlacklisted)}
{intl.formatMessage(messages.hideBlocklisted)}
</span>
<span className="label-tip">
{intl.formatMessage(messages.hideBlacklistedTip)}
{intl.formatMessage(messages.hideBlocklistedTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="hideBlacklisted"
name="hideBlacklisted"
id="hideBlocklisted"
name="hideBlocklisted"
onChange={() => {
setFieldValue(
'hideBlacklisted',
!values.hideBlacklisted
'hideBlocklisted',
!values.hideBlocklisted
);
}}
/>

View File

@@ -19,7 +19,9 @@ const SetupSteps = ({
<li className="relative md:flex md:flex-1">
<div className="flex items-center space-x-4 px-6 py-4 text-sm font-medium leading-5">
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center border-2 ${active ? 'border-indigo-600' : 'border-white'} ${completed ? 'border-indigo-600 bg-indigo-600' : ''} rounded-full`}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center border-2 ${
active ? 'border-indigo-600' : 'border-white'
} ${completed ? 'border-indigo-600 bg-indigo-600' : ''} rounded-full`}
>
{completed && <CheckIcon className="h-6 w-6 text-white" />}
{!completed && (

View File

@@ -362,12 +362,12 @@ const StatusBadge = ({
</Tooltip>
);
case MediaStatus.BLACKLISTED:
case MediaStatus.BLOCKLISTED:
return (
<Tooltip content={mediaLinkDescription}>
<Badge badgeType="danger" href={mediaLink}>
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
status: intl.formatMessage(globalMessages.blacklisted),
status: intl.formatMessage(globalMessages.blocklisted),
})}
</Badge>
</Tooltip>

View File

@@ -1,5 +1,5 @@
import Spinner from '@app/assets/spinner.svg';
import BlacklistModal from '@app/components/BlacklistModal';
import BlocklistModal from '@app/components/BlocklistModal';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import StatusBadgeMini from '@app/components/Common/StatusBadgeMini';
@@ -78,7 +78,7 @@ const TitleCard = ({
const { addToast } = useToasts();
const [toggleWatchlist, setToggleWatchlist] =
useState<boolean>(!isAddedToWatchlist);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const [showBlocklistModal, setShowBlocklistModal] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
// Just to get the year from the date
@@ -100,8 +100,8 @@ const TitleCard = ({
[]
);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
const closeBlocklistModal = useCallback(
() => setShowBlocklistModal(false),
[]
);
@@ -173,7 +173,7 @@ const TitleCard = ({
if (topNode) {
try {
await axios.post('/api/v1/blacklist', {
await axios.post('/api/v1/blocklist', {
tmdbId: id,
mediaType,
title,
@@ -181,19 +181,19 @@ const TitleCard = ({
});
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
{intl.formatMessage(globalMessages.blocklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
</span>,
{ appearance: 'success', autoDismiss: true }
);
setCurrentStatus(MediaStatus.BLACKLISTED);
setCurrentStatus(MediaStatus.BLOCKLISTED);
} catch (e) {
if (e?.response?.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
{intl.formatMessage(globalMessages.blocklistDuplicateError, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -201,7 +201,7 @@ const TitleCard = ({
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
@@ -209,26 +209,26 @@ const TitleCard = ({
}
setIsUpdating(false);
closeBlacklistModal();
closeBlocklistModal();
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
};
const onClickShowBlacklistBtn = async (): Promise<void> => {
const onClickShowBlocklistBtn = async (): Promise<void> => {
setIsUpdating(true);
const topNode = cardRef.current;
if (topNode) {
const res = await axios.delete('/api/v1/blacklist/' + id);
const res = await axios.delete('/api/v1/blocklist/' + id);
if (res.status === 204) {
addToast(
<span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, {
{intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -237,13 +237,13 @@ const TitleCard = ({
);
setCurrentStatus(MediaStatus.UNKNOWN);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
@@ -264,7 +264,7 @@ const TitleCard = ({
{ type: 'or' }
);
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
const showHideButton = hasPermission([Permission.MANAGE_BLOCKLIST], {
type: 'or',
});
@@ -288,7 +288,7 @@ const TitleCard = ({
onUpdating={requestUpdating}
onCancel={closeModal}
/>
<BlacklistModal
<BlocklistModal
tmdbId={id}
type={
mediaType === 'movie'
@@ -297,8 +297,8 @@ const TitleCard = ({
? 'collection'
: 'tv'
}
show={showBlacklistModal}
onCancel={closeBlacklistModal}
show={showBlocklistModal}
onCancel={closeBlocklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isUpdating}
/>
@@ -355,7 +355,7 @@ const TitleCard = ({
: intl.formatMessage(globalMessages.tvshow)}
</div>
</div>
{showDetail && currentStatus !== MediaStatus.BLACKLISTED && (
{showDetail && currentStatus !== MediaStatus.BLOCKLISTED && (
<div className="flex flex-col gap-1">
{user?.userType !== UserType.PLEX &&
(toggleWatchlist ? (
@@ -385,7 +385,7 @@ const TitleCard = ({
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => setShowBlacklistModal(true)}
onClick={() => setShowBlocklistModal(true)}
>
<EyeSlashIcon className={'h-3'} />
</Button>
@@ -394,17 +394,17 @@ const TitleCard = ({
)}
{showDetail &&
showHideButton &&
currentStatus == MediaStatus.BLACKLISTED && (
currentStatus == MediaStatus.BLOCKLISTED && (
<Tooltip
content={intl.formatMessage(
globalMessages.removefromBlacklist
globalMessages.removefromBlocklist
)}
>
<Button
buttonType={'ghost'}
className="z-40"
buttonSize={'sm'}
onClick={() => onClickShowBlacklistBtn()}
onClick={() => onClickShowBlocklistBtn()}
>
<EyeIcon className={'h-3'} />
</Button>

View File

@@ -4,7 +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 BlocklistModal from '@app/components/BlocklistModal';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
@@ -124,9 +124,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
const [toggleWatchlist, setToggleWatchlist] = useState<boolean>(
!tv?.onUserWatchlist
);
const [isBlacklistUpdating, setIsBlacklistUpdating] =
const [isBlocklistUpdating, setIsBlocklistUpdating] =
useState<boolean>(false);
const [showBlacklistModal, setShowBlacklistModal] = useState(false);
const [showBlocklistModal, setShowBlocklistModal] = useState(false);
const { addToast } = useToasts();
const {
@@ -157,8 +157,8 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
setShowManager(router.query.manage == '1');
}, [router.query.manage]);
const closeBlacklistModal = useCallback(
() => setShowBlacklistModal(false),
const closeBlocklistModal = useCallback(
() => setShowBlocklistModal(false),
[]
);
@@ -411,10 +411,10 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
};
const onClickHideItemBtn = async (): Promise<void> => {
setIsBlacklistUpdating(true);
setIsBlocklistUpdating(true);
try {
const res = await axios.post('/api/v1/blacklist', {
const res = await axios.post('/api/v1/blocklist', {
tmdbId: tv?.id,
mediaType: 'tv',
title: tv?.name,
@@ -424,7 +424,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
if (res.status === 201) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistSuccess, {
{intl.formatMessage(globalMessages.blocklistSuccess, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -438,7 +438,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
if (e?.response?.status === 412) {
addToast(
<span>
{intl.formatMessage(globalMessages.blacklistDuplicateError, {
{intl.formatMessage(globalMessages.blocklistDuplicateError, {
title: tv?.name,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})}
@@ -446,18 +446,18 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
{ appearance: 'info', autoDismiss: true }
);
} else {
addToast(intl.formatMessage(globalMessages.blacklistError), {
addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error',
autoDismiss: true,
});
}
}
setIsBlacklistUpdating(false);
closeBlacklistModal();
setIsBlocklistUpdating(false);
closeBlocklistModal();
};
const showHideButton = hasPermission([Permission.MANAGE_BLACKLIST], {
const showHideButton = hasPermission([Permission.MANAGE_BLOCKLIST], {
type: 'or',
});
@@ -488,13 +488,13 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
</div>
)}
<PageTitle title={data.name} />
<BlacklistModal
<BlocklistModal
tmdbId={data.id}
type="tv"
show={showBlacklistModal}
onCancel={closeBlacklistModal}
show={showBlocklistModal}
onCancel={closeBlocklistModal}
onComplete={onClickHideItemBtn}
isUpdating={isBlacklistUpdating}
isUpdating={isBlocklistUpdating}
/>
<IssueModal
onCancel={() => setShowIssueModal(false)}
@@ -607,21 +607,21 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
data?.mediaInfo?.status !== MediaStatus.AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE &&
data?.mediaInfo?.status !== MediaStatus.PENDING &&
data?.mediaInfo?.status !== MediaStatus.BLACKLISTED && (
data?.mediaInfo?.status !== MediaStatus.BLOCKLISTED && (
<Tooltip
content={intl.formatMessage(globalMessages.addToBlacklist)}
content={intl.formatMessage(globalMessages.addToBlocklist)}
>
<Button
buttonType={'ghost'}
className="z-40 mr-2"
buttonSize={'md'}
onClick={() => setShowBlacklistModal(true)}
onClick={() => setShowBlocklistModal(true)}
>
<EyeSlashIcon />
</Button>
</Tooltip>
)}
{data?.mediaInfo?.status !== MediaStatus.BLACKLISTED &&
{data?.mediaInfo?.status !== MediaStatus.BLOCKLISTED &&
user?.userType !== UserType.PLEX && (
<>
{toggleWatchlist ? (

View File

@@ -13,7 +13,7 @@ const defaultSettings = {
applicationTitle: 'Seerr',
applicationUrl: '',
hideAvailable: false,
hideBlacklisted: false,
hideBlocklisted: false,
localLogin: true,
mediaServerLogin: true,
movie4kEnabled: false,

View File

@@ -54,7 +54,7 @@ const useDiscover = <
>(
endpoint: string,
options?: O,
{ hideAvailable = true, hideBlacklisted = true } = {}
{ hideAvailable = true, hideBlocklisted = true } = {}
): DiscoverResult<T, S> => {
const settings = useSettings();
const { hasPermission } = useUser();
@@ -125,14 +125,14 @@ const useDiscover = <
}
if (
settings.currentSettings.hideBlacklisted &&
hideBlacklisted &&
hasPermission(Permission.MANAGE_BLACKLIST)
settings.currentSettings.hideBlocklisted &&
hideBlocklisted &&
hasPermission(Permission.MANAGE_BLOCKLIST)
) {
titles = titles.filter(
(i) =>
(i.mediaType === 'movie' || i.mediaType === 'tv') &&
i.mediaInfo?.status !== MediaStatus.BLACKLISTED
i.mediaInfo?.status !== MediaStatus.BLOCKLISTED
);
}

View File

@@ -57,16 +57,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. Please 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',
blocklist: 'Blocklist',
blocklisted: 'Blocklisted',
blocklistSuccess: '<strong>{title}</strong> was successfully blocklisted.',
blocklistError: 'Something went wrong. Please try again.',
blocklistDuplicateError:
'<strong>{title}</strong> has already been blocklisted.',
removeFromBlocklistSuccess:
'<strong>{title}</strong> was successfully removed from the Blocklist.',
addToBlocklist: 'Add to Blocklist',
removefromBlocklist: 'Remove from Blocklist',
specials: 'Specials',
});

View File

@@ -1,21 +1,21 @@
{
"component.BlacklistBlock.blacklistdate": "Blacklisted date",
"component.BlacklistBlock.blacklistedby": "Blacklisted By",
"component.BlacklistModal.blacklisting": "Blacklisting",
"component.BlocklistBlock.blocklistdate": "Blocklisted date",
"component.BlocklistBlock.blocklistedby": "Blocklisted By",
"component.BlocklistModal.blocklisting": "Blocklisting",
"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.filterBlacklistedTags": "Blacklisted Tags",
"components.Blacklist.filterManual": "Manual",
"components.Blacklist.mediaName": "Name",
"components.Blacklist.mediaTmdbId": "tmdb Id",
"components.Blacklist.mediaType": "Type",
"components.Blacklist.showAllBlacklisted": "Show All Blacklisted Media",
"components.Blocklist.blocklistNotFoundError": "<strong>{title}</strong> is not blocklisted.",
"components.Blocklist.blocklistSettingsDescription": "Manage blocklisted media.",
"components.Blocklist.blocklistdate": "date",
"components.Blocklist.blocklistedby": "{date} by {user}",
"components.Blocklist.blocklistsettings": "Blocklist Settings",
"components.Blocklist.filterBlocklistedTags": "Blocklisted Tags",
"components.Blocklist.filterManual": "Manual",
"components.Blocklist.mediaName": "Name",
"components.Blocklist.mediaTmdbId": "tmdb Id",
"components.Blocklist.mediaType": "Type",
"components.Blocklist.showAllBlocklisted": "Show All Blocklisted Media",
"components.CollectionDetails.numberofmovies": "{count} Movies",
"components.CollectionDetails.overview": "Overview",
"components.CollectionDetails.requestcollection": "Request Collection",
@@ -219,7 +219,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.blocklist": "Blocklist",
"components.Layout.Sidebar.browsemovies": "Movies",
"components.Layout.Sidebar.browsetv": "Series",
"components.Layout.Sidebar.dashboard": "Discover",
@@ -412,12 +412,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.blocklistedItems": "Blocklist media.",
"components.PermissionEdit.blocklistedItemsDescription": "Grant permission to blocklist 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.manageblocklist": "Manage Blocklist",
"components.PermissionEdit.manageblocklistDescription": "Grant permission to manage blocklisted media.",
"components.PermissionEdit.manageissues": "Manage Issues",
"components.PermissionEdit.manageissuesDescription": "Grant permission to manage media issues.",
"components.PermissionEdit.managerequests": "Manage Requests",
@@ -436,8 +436,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.viewblocklistedItems": "View blocklisted media.",
"components.PermissionEdit.viewblocklistedItemsDescription": "Grant permission to view blocklisted 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",
@@ -935,7 +935,7 @@
"components.Settings.SettingsJobsCache.plex-refresh-token": "Plex Refresh Token",
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist Sync",
"components.Settings.SettingsJobsCache.process": "Process",
"components.Settings.SettingsJobsCache.process-blacklisted-tags": "Process Blacklisted Tags",
"components.Settings.SettingsJobsCache.process-blocklisted-tags": "Process Blocklisted Tags",
"components.Settings.SettingsJobsCache.radarr-scan": "Radarr Scan",
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SettingsJobsCache.size": "Size",
@@ -964,10 +964,10 @@
"components.Settings.SettingsMain.apikeyCopied": "Copied API key to clipboard.",
"components.Settings.SettingsMain.applicationTitle": "Application Title",
"components.Settings.SettingsMain.applicationurl": "Application URL",
"components.Settings.SettingsMain.blacklistedTags": "Blacklist Content with Tags",
"components.Settings.SettingsMain.blacklistedTagsLimit": "Limit Content Blacklisted per Tag",
"components.Settings.SettingsMain.blacklistedTagsLimitTip": "The \"Process Blacklisted Tags\" job will blacklist this many pages into each sort. Larger numbers will create a more accurate blacklist, but use more space.",
"components.Settings.SettingsMain.blacklistedTagsTip": "Automatically add content with tags to the blacklist using the \"Process Blacklisted Tags\" job",
"components.Settings.SettingsMain.blocklistedTags": "Blocklist Content with Tags",
"components.Settings.SettingsMain.blocklistedTagsLimit": "Limit Content Blocklisted per Tag",
"components.Settings.SettingsMain.blocklistedTagsLimitTip": "The \"Process Blocklisted Tags\" job will blocklist this many pages into each sort. Larger numbers will create a more accurate blocklist, but use more space.",
"components.Settings.SettingsMain.blocklistedTagsTip": "Automatically add content with tags to the blocklist using the \"Process Blocklisted Tags\" job",
"components.Settings.SettingsMain.cacheImages": "Enable Image Caching",
"components.Settings.SettingsMain.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
"components.Settings.SettingsMain.discoverRegion": "Discover Region",
@@ -978,8 +978,8 @@
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Seerr.",
"components.Settings.SettingsMain.hideAvailable": "Hide Available Media",
"components.Settings.SettingsMain.hideAvailableTip": "Hide available media from the discover pages but not search results",
"components.Settings.SettingsMain.hideBlacklisted": "Hide Blacklisted Items",
"components.Settings.SettingsMain.hideBlacklistedTip": "Hide blacklisted items from discover pages for all users with the \"Manage Blacklist\" permission",
"components.Settings.SettingsMain.hideBlocklisted": "Hide Blocklisted Items",
"components.Settings.SettingsMain.hideBlocklistedTip": "Hide blocklisted items from discover pages for all users with the \"Manage Blocklist\" permission",
"components.Settings.SettingsMain.locale": "Display Language",
"components.Settings.SettingsMain.originallanguage": "Discover Language",
"components.Settings.SettingsMain.originallanguageTip": "Filter content by original language",
@@ -1112,17 +1112,17 @@
"components.Settings.allChosenProvidersAreOperational": "All chosen metadata providers are operational",
"components.Settings.animeMetadataProvider": "Anime metadata provider",
"components.Settings.apiKey": "API key",
"components.Settings.blacklistedTagImportInstructions": "Paste blacklist tag configuration below.",
"components.Settings.blacklistedTagImportTitle": "Import Blacklisted Tag Configuration",
"components.Settings.blacklistedTagsText": "Blacklisted Tags",
"components.Settings.blocklistedTagImportInstructions": "Paste blocklist tag configuration below.",
"components.Settings.blocklistedTagImportTitle": "Import Blocklisted Tag Configuration",
"components.Settings.blocklistedTagsText": "Blocklisted Tags",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.chooseProvider": "Choose metadata providers for different content types",
"components.Settings.clearBlacklistedTagsConfirm": "Are you sure you want to clear the blacklisted tags?",
"components.Settings.clearBlocklistedTagsConfirm": "Are you sure you want to clear the blocklisted tags?",
"components.Settings.clickTest": "Click on the \"Test\" button to check connectivity with metadata providers",
"components.Settings.connectionTestFailed": "Connection test failed",
"components.Settings.copyBlacklistedTags": "Copied blacklisted tags to clipboard.",
"components.Settings.copyBlacklistedTagsEmpty": "Nothing to copy",
"components.Settings.copyBlacklistedTagsTip": "Copy blacklisted tag configuration",
"components.Settings.copyBlocklistedTags": "Copied blocklisted tags to clipboard.",
"components.Settings.copyBlocklistedTagsEmpty": "Nothing to copy",
"components.Settings.copyBlocklistedTagsTip": "Copy blocklisted tag configuration",
"components.Settings.currentlibrary": "Current Library: {name}",
"components.Settings.default": "Default",
"components.Settings.default4k": "Default 4K",
@@ -1136,7 +1136,7 @@
"components.Settings.failedToSaveMetadataSettings": "Failed to save metadata provider settings",
"components.Settings.general": "General",
"components.Settings.hostname": "Hostname or IP Address",
"components.Settings.importBlacklistedTagsTip": "Import blacklisted tag configuration",
"components.Settings.importBlocklistedTagsTip": "Import blocklisted tag configuration",
"components.Settings.invalidKeyword": "{keywordId} is not a TMDB keyword.",
"components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.",
"components.Settings.is4k": "4K",
@@ -1539,7 +1539,7 @@
"components.UserProfile.seriesrequest": "Series Requests",
"components.UserProfile.totalrequests": "Total Requests",
"components.UserProfile.unlimited": "Unlimited",
"i18n.addToBlacklist": "Add to Blacklist",
"i18n.addToBlocklist": "Add to Blocklist",
"i18n.advanced": "Advanced",
"i18n.all": "All",
"i18n.approve": "Approve",
@@ -1547,11 +1547,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. Please try again.",
"i18n.blacklistSuccess": "<strong>{title}</strong> was successfully blacklisted.",
"i18n.blacklisted": "Blacklisted",
"i18n.blocklist": "Blocklist",
"i18n.blocklistDuplicateError": "<strong>{title}</strong> has already been blocklisted.",
"i18n.blocklistError": "Something went wrong. Please try again.",
"i18n.blocklistSuccess": "<strong>{title}</strong> was successfully blocklisted.",
"i18n.blocklisted": "Blocklisted",
"i18n.cancel": "Cancel",
"i18n.canceling": "Canceling…",
"i18n.close": "Close",
@@ -1579,8 +1579,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.removeFromBlocklistSuccess": "<strong>{title}</strong> was successfully removed from the Blocklist.",
"i18n.removefromBlocklist": "Remove from Blocklist",
"i18n.request": "Request",
"i18n.request4k": "Request in 4K",
"i18n.requested": "Requested",

View File

@@ -231,7 +231,7 @@ CoreApp.getInitialProps = async (initialProps) => {
applicationTitle: '',
applicationUrl: '',
hideAvailable: false,
hideBlacklisted: false,
hideBlocklisted: false,
movie4kEnabled: false,
series4kEnabled: false,
localLogin: true,

View File

@@ -1,13 +0,0 @@
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;

View File

@@ -0,0 +1,13 @@
import Blocklist from '@app/components/Blocklist';
import useRouteGuard from '@app/hooks/useRouteGuard';
import { Permission } from '@server/lib/permissions';
import type { NextPage } from 'next';
const BlocklistPage: NextPage = () => {
useRouteGuard([Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST], {
type: 'or',
});
return <Blocklist />;
};
export default BlocklistPage;