diff --git a/README.md b/README.md index cf6d8e9f0f..3d1e8ab979 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 973f4c591c..949351c378 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -18,8 +18,8 @@ "discoverRegion": "", "streamingRegion": "", "originalLanguage": "", - "blacklistedTags": "", - "blacklistedTagsLimit": 50, + "blocklistedTags": "", + "blocklistedTagsLimit": 50, "trustProxy": false, "mediaServerType": 1, "partialRequestsEnabled": true, diff --git a/docs/README.md b/docs/README.md index 560dfa56f7..c035ddba34 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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! diff --git a/docs/using-seerr/backups.md b/docs/using-seerr/backups.md index e798abebbe..49d079f592 100644 --- a/docs/using-seerr/backups.md +++ b/docs/using-seerr/backups.md @@ -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 diff --git a/docs/using-seerr/settings/general.md b/docs/using-seerr/settings/general.md index a64c57633b..18fd32765f 100644 --- a/docs/using-seerr/settings/general.md +++ b/docs/using-seerr/settings/general.md @@ -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. diff --git a/gen-docs/blog/2026-02-10/seerr-release.md b/gen-docs/blog/2026-02-10/seerr-release.md index 9a83e66133..fda587b337 100644 --- a/gen-docs/blog/2026-02-10/seerr-release.md +++ b/gen-docs/blog/2026-02-10/seerr-release.md @@ -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. diff --git a/seerr-api.yml b/seerr-api.yml index bf9d882712..aaaf30bf4a 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -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,123 @@ paths: restricted: type: boolean example: false + /blocklist: + get: + summary: Returns blocklisted items + description: Returns list of all blocklisted media + 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 + 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 + /blocklist/{tmdbId}: + get: + summary: Get media from blocklist + 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 + tags: + - 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 blacklisted items - description: Returns list of all blacklisted media + summary: Returns blocklisted items + description: | + **DEPRECATED**: Use `/blocklist` instead. This endpoint will be deprecated soon. + deprecated: true tags: - - settings + - blocklist parameters: - in: query name: take @@ -4558,11 +4669,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 +4704,31 @@ paths: type: number example: 438631 post: - summary: Add media to blacklist + summary: Add media to blocklist + description: | + **DEPRECATED**: Use `/blocklist` instead. This endpoint will be deprecated soon. + deprecated: true 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 + description: Item has already been blocklisted /blacklist/{tmdbId}: get: - summary: Get media from blacklist + summary: Get media from blocklist + description: | + **DEPRECATED**: Use `/blocklist/{tmdbId}` instead. This endpoint will be deprecated soon. + deprecated: true tags: - - blacklist + - blocklist parameters: - in: path name: tmdbId @@ -4622,11 +4739,14 @@ 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 + description: | + **DEPRECATED**: Use `/blocklist/{tmdbId}` instead. This endpoint will be deprecated soon. + deprecated: true tags: - - blacklist + - blocklist parameters: - in: path name: tmdbId diff --git a/server/constants/media.ts b/server/constants/media.ts index 4bac7c0385..170109fb5c 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -17,6 +17,6 @@ export enum MediaStatus { PROCESSING, PARTIALLY_AVAILABLE, AVAILABLE, - BLACKLISTED, + BLOCKLISTED, DELETED, } diff --git a/server/entity/Blacklist.ts b/server/entity/Blocklist.ts similarity index 64% rename from server/entity/Blacklist.ts rename to server/entity/Blocklist.ts index 64c44e50d2..2f65e12859 100644 --- a/server/entity/Blacklist.ts +++ b/server/entity/Blocklist.ts @@ -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) { + constructor(init?: Partial) { Object.assign(this, init); } - public static async addToBlacklist( + public static async addToBlocklist( { - blacklistRequest, + blocklistRequest, }: { - blacklistRequest: { + blocklistRequest: { mediaType: MediaType; title?: ZodOptional['_output']; tmdbId: ZodNumber['_output']; - blacklistedTags?: string; + blocklistedTags?: string; }; }, entityManager?: EntityManager ): Promise { 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); } diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 89889c17cb..25609c58b0 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -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; + @OneToOne(() => Blocklist, (blocklist) => blocklist.media) + public blocklist: Promise; @DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) public createdAt: Date; diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 912d5284d1..e5524d99ba 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -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) { diff --git a/server/interfaces/api/blacklistInterfaces.ts b/server/interfaces/api/blocklistInterfaces.ts similarity index 60% rename from server/interfaces/api/blacklistInterfaces.ts rename to server/interfaces/api/blocklistInterfaces.ts index 0cf4646ea4..58e3ebe474 100644 --- a/server/interfaces/api/blacklistInterfaces.ts +++ b/server/interfaces/api/blocklistInterfaces.ts @@ -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[]; } diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 5e058eccda..ea08d4e61d 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -30,7 +30,7 @@ export interface PublicSettingsResponse { applicationTitle: string; applicationUrl: string; hideAvailable: boolean; - hideBlacklisted: boolean; + hideBlocklisted: boolean; localLogin: boolean; mediaServerLogin: boolean; movie4kEnabled: boolean; diff --git a/server/job/blacklistedTagsProcessor.ts b/server/job/blocklistedTagsProcessor.ts similarity index 67% rename from server/job/blacklistedTagsProcessor.ts rename to server/job/blocklistedTagsProcessor.ts index f7ca4f0f22..d6bf7509c3 100644 --- a/server/job/blacklistedTagsProcessor.ts +++ b/server/job/blocklistedTagsProcessor.ts @@ -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 { +class BlocklistedTagProcessor implements RunnableScanner { private running = false; private progress = 0; private total = 0; @@ -30,12 +30,12 @@ class BlacklistedTagProcessor implements RunnableScanner { 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 { 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(); - 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 { 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 { } 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 { 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 { } } - 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; diff --git a/server/job/schedule.ts b/server/job/schedule.ts index c740dbaec5..6ce8f14b1c 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -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' }); diff --git a/server/lib/permissions.ts b/server/lib/permissions.ts index bc477169c0..edc9f7e183 100644 --- a/server/lib/permissions.ts +++ b/server/lib/permissions.ts @@ -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 { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index e37eccc948..0aa9da1596 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -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, diff --git a/server/lib/settings/migrations/0008_migrate_blacklist_to_blocklist.ts b/server/lib/settings/migrations/0008_migrate_blacklist_to_blocklist.ts new file mode 100644 index 0000000000..c1f2ad1093 --- /dev/null +++ b/server/lib/settings/migrations/0008_migrate_blacklist_to_blocklist.ts @@ -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; diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index d53ff1dcec..be73a0dfd5 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -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', { diff --git a/server/middleware/deprecation.ts b/server/middleware/deprecation.ts new file mode 100644 index 0000000000..a35839180d --- /dev/null +++ b/server/middleware/deprecation.ts @@ -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; diff --git a/server/migration/postgres/1746900000000-RenameBlacklistToBlocklist.ts b/server/migration/postgres/1746900000000-RenameBlacklistToBlocklist.ts new file mode 100644 index 0000000000..670e915d4c --- /dev/null +++ b/server/migration/postgres/1746900000000-RenameBlacklistToBlocklist.ts @@ -0,0 +1,19 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface { + name = 'RenameBlacklistToBlocklist1746900000000'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query( + `ALTER TABLE "blocklist" RENAME COLUMN "blocklistedTags" TO "blacklistedTags"` + ); + await queryRunner.query(`ALTER TABLE "blocklist" RENAME TO "blacklist"`); + } +} diff --git a/server/migration/sqlite/1746900000000-RenameBlacklistToBlocklist.ts b/server/migration/sqlite/1746900000000-RenameBlacklistToBlocklist.ts new file mode 100644 index 0000000000..49a5d48591 --- /dev/null +++ b/server/migration/sqlite/1746900000000-RenameBlacklistToBlocklist.ts @@ -0,0 +1,66 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface { + name = 'RenameBlacklistToBlocklist1746900000000'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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")` + ); + } +} diff --git a/server/routes/blacklist.ts b/server/routes/blocklist.ts similarity index 60% rename from server/routes/blacklist.ts rename to server/routes/blocklist.ts index e0540c4896..8a322f3eef 100644 --- a/server/routes/blacklist.ts +++ b/server/routes/blocklist.ts @@ -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; diff --git a/server/routes/index.ts b/server/routes/index.ts index b3f1cebb76..f701acf968 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -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); diff --git a/server/routes/request.ts b/server/routes/request.ts index 412f4eb923..608de8e72b 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -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( 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 }); diff --git a/src/components/Blacklist/index.tsx b/src/components/Blocklist/index.tsx similarity index 89% rename from src/components/Blacklist/index.tsx rename to src/components/Blocklist/index.tsx index 02a5f449fe..63c95a2451 100644 --- a/src/components/Blacklist/index.tsx +++ b/src/components/Blocklist/index.tsx @@ -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: '{title} is not blacklisted.', + blocklistdate: 'date', + blocklistedby: '{date} by {user}', + blocklistNotFoundError: '{title} 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(10); const [searchFilter, debouncedSearchFilter, setSearchFilter] = useDebouncedState(''); @@ -75,8 +75,8 @@ const Blacklist = () => { data, error, mutate: revalidate, - } = useSWR( - `/api/v1/blacklist/?take=${currentPageSize}&skip=${ + } = useSWR( + `/api/v1/blocklist/?take=${currentPageSize}&skip=${ pageIndex * currentPageSize }&filter=${currentFilter}${ debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : '' @@ -107,9 +107,9 @@ const Blacklist = () => { return ( <> - +
-
{intl.formatMessage(globalMessages.blacklist)}
+
{intl.formatMessage(globalMessages.blocklist)}
@@ -137,8 +137,8 @@ const Blacklist = () => { -
@@ -170,16 +170,16 @@ const Blacklist = () => { buttonType="primary" onClick={() => setCurrentFilter(Filter.ALL)} > - {intl.formatMessage(messages.showAllBlacklisted)} + {intl.formatMessage(messages.showAllBlocklisted)}
)}
) : ( - data.results.map((item: BlacklistItem) => { + data.results.map((item: BlocklistItem) => { return (
- +
); }) @@ -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(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( - {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { + {intl.formatMessage(globalMessages.removeFromBlocklistSuccess, { title, strong: (msg: React.ReactNode) => {msg}, })} @@ -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) => {
Status - {intl.formatMessage(globalMessages.blacklisted)} + {intl.formatMessage(globalMessages.blocklisted)}
{item.createdAt && (
- {intl.formatMessage(globalMessages.blacklisted)} + {intl.formatMessage(globalMessages.blocklisted)} - {intl.formatMessage(messages.blacklistedby, { + {intl.formatMessage(messages.blocklistedby, { date: ( { - ) : item.blacklistedTags ? ( + ) : item.blocklistedTags ? ( - + ) : ( @@ -457,10 +457,10 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
- {hasPermission(Permission.MANAGE_BLACKLIST) && ( + {hasPermission(Permission.MANAGE_BLOCKLIST) && ( - removeFromBlacklist( + removeFromBlocklist( item.tmdbId, title && (isMovie(title) ? title.title : title.name) ) @@ -474,7 +474,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => { > - {intl.formatMessage(globalMessages.removefromBlacklist)} + {intl.formatMessage(globalMessages.removefromBlocklist)} )} diff --git a/src/components/BlacklistBlock/index.tsx b/src/components/BlocklistBlock/index.tsx similarity index 77% rename from src/components/BlacklistBlock/index.tsx rename to src/components/BlocklistBlock/index.tsx index 1e8f1fb682..2eb48e2353 100644 --- a/src/components/BlacklistBlock/index.tsx +++ b/src/components/BlocklistBlock/index.tsx @@ -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(`/api/v1/blacklist/${tmdbId}`); + const { data } = useSWR(`/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( - {intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { + {intl.formatMessage(globalMessages.removeFromBlocklistSuccess, { title, strong: (msg: React.ReactNode) => {msg}, })} @@ -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 = ({
{data.user ? ( <> - + @@ -97,23 +97,23 @@ const BlacklistBlock = ({ - ) : data.blacklistedTags ? ( + ) : data.blocklistedTags ? ( <> - {intl.formatMessage(messages.blacklistedby)}:  + {intl.formatMessage(messages.blocklistedby)}:  - + ) : null}