Compare commits

...

7 Commits

Author SHA1 Message Date
0xsysr3ll
eb08eb024f fix(plexapi): temporary remove unused PlexLibraryResponse interface 2026-02-16 21:53:14 +01:00
0xsysr3ll
b5c8e21104 feat(plexapi): add debug logs for Plex API response 2026-02-16 21:48:46 +01:00
0xsysr3ll
54baa53aac feat(plexapi): add debug logs for Plex API response 2026-02-16 21:47:01 +01:00
Gauthier
bcd8002887 fix: run the blocklist migration last (#2417) 2026-02-14 16:26:03 +01:00
Conlan Kreher
33a5d9a9ac 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>
2026-02-14 14:31:45 +01:00
fallenbagel
15be3d7475 fix(base-scanner): derive media availability from actual season state… (#2412) 2026-02-13 23:05:59 +05:00
Gauthier
55c2d541e6 feat(ui): rebrand Jellyseerr logos to Seerr (#2406) 2026-02-13 17:37:00 +01:00
114 changed files with 1023 additions and 1352 deletions

View File

@@ -26,7 +26,7 @@
- Granular permission system. - Granular permission system.
- Support for various notification agents. - Support for various notification agents.
- Mobile-friendly design, for when you need to approve requests on the go! - 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. 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": "", "discoverRegion": "",
"streamingRegion": "", "streamingRegion": "",
"originalLanguage": "", "originalLanguage": "",
"blacklistedTags": "", "blocklistedTags": "",
"blacklistedTagsLimit": 50, "blocklistedTagsLimit": 50,
"trustProxy": false, "trustProxy": false,
"mediaServerType": 1, "mediaServerType": 1,
"partialRequestsEnabled": true, "partialRequestsEnabled": true,

View File

@@ -23,7 +23,7 @@ Welcome to the Seerr Documentation.
- Localization into other languages. - Localization into other languages.
- Support for **PostgreSQL** and **SQLite** databases. - Support for **PostgreSQL** and **SQLite** databases.
- Support for various notification agents. - Support for various notification agents.
- Easily **Watchlist** or **Blacklist** media. - Easily **Watchlist** or **Blocklist** media.
- More features to come! - More features to come!
## We need your help! ## 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 ## 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 # 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. 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 ## 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. 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. 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. * **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. * **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. * **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. * **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. * **DNS caching**: Reduces lookup times and external requests, especially useful when using systems like Pi-Hole/Adguard Home.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -38,8 +38,8 @@ tags:
description: Endpoints related to getting service (Radarr/Sonarr) details. description: Endpoints related to getting service (Radarr/Sonarr) details.
- name: watchlist - name: watchlist
description: Collection of media to watch later description: Collection of media to watch later
- name: blacklist - name: blocklist
description: Blacklisted media from discovery page. description: Blocklisted media from discovery page.
servers: servers:
- url: '{server}/api/v1' - url: '{server}/api/v1'
variables: variables:
@@ -48,7 +48,7 @@ servers:
components: components:
schemas: schemas:
Blacklist: Blocklist:
type: object type: object
properties: properties:
tmdbId: tmdbId:
@@ -4529,12 +4529,12 @@ paths:
restricted: restricted:
type: boolean type: boolean
example: false example: false
/blacklist: /blocklist:
get: get:
summary: Returns blacklisted items summary: Returns blocklisted items
description: Returns list of all blacklisted media description: Returns list of all blocklisted media
tags: tags:
- settings - blocklist
parameters: parameters:
- in: query - in: query
name: take name: take
@@ -4558,11 +4558,11 @@ paths:
name: filter name: filter
schema: schema:
type: string type: string
enum: [all, manual, blacklistedTags] enum: [all, manual, blocklistedTags]
default: manual default: manual
responses: responses:
'200': '200':
description: Blacklisted items returned description: Blocklisted items returned
content: content:
application/json: application/json:
schema: schema:
@@ -4593,25 +4593,25 @@ paths:
type: number type: number
example: 438631 example: 438631
post: post:
summary: Add media to blacklist summary: Add media to blocklist
tags: tags:
- blacklist - blocklist
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Blacklist' $ref: '#/components/schemas/Blocklist'
responses: responses:
'201': '201':
description: Item succesfully blacklisted description: Item succesfully blocklisted
'412': '412':
description: Item has already been blacklisted description: Item has already been blocklisted
/blacklist/{tmdbId}: /blocklist/{tmdbId}:
get: get:
summary: Get media from blacklist summary: Get media from blocklist
tags: tags:
- blacklist - blocklist
parameters: parameters:
- in: path - in: path
name: tmdbId name: tmdbId
@@ -4622,11 +4622,131 @@ paths:
type: string type: string
responses: responses:
'200': '200':
description: Blacklist details in JSON description: Blocklist details in JSON
delete: delete:
summary: Remove media from blacklist summary: Remove media from blocklist
tags: 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: parameters:
- in: path - in: path
name: tmdbId name: tmdbId

View File

@@ -27,10 +27,13 @@ export interface PlexLibraryItem {
Media: Media[]; Media: Media[];
} }
interface PlexLibraryResponse { interface PlexLibraryResponseRaw {
MediaContainer: { MediaContainer: {
totalSize: number; totalSize?: number;
Metadata: PlexLibraryItem[]; size?: number;
Metadata?: PlexLibraryItem[];
Directory?: PlexLibraryItem[];
[key: string]: unknown;
}; };
} }
@@ -177,7 +180,7 @@ class PlexAPI extends ExternalAPI {
id: string, id: string,
{ offset = 0, size = 50 }: { offset?: number; size?: number } = {} { offset = 0, size = 50 }: { offset?: number; size?: number } = {}
): Promise<{ totalSize: number; items: PlexLibraryItem[] }> { ): Promise<{ totalSize: number; items: PlexLibraryItem[] }> {
const response = await this.get<PlexLibraryResponse>( const response = await this.get<PlexLibraryResponseRaw>(
`/library/sections/${id}/all?includeGuids=1`, `/library/sections/${id}/all?includeGuids=1`,
{ {
headers: { headers: {
@@ -187,9 +190,26 @@ class PlexAPI extends ExternalAPI {
} }
); );
const container = response.MediaContainer;
const metadataLength = container.Metadata?.length ?? 0;
const directoryLength = container.Directory?.length ?? 0;
logger.debug('Plex getLibraryContents raw response', {
label: 'Plex API',
libraryId: id,
offset,
metadataLength,
directoryLength,
totalSize: container.totalSize,
size: container.size,
keys: Object.keys(container).filter((k) =>
['Metadata', 'Directory', 'totalSize', 'size'].includes(k)
),
});
return { return {
totalSize: response.MediaContainer.totalSize, totalSize: container.totalSize ?? 0,
items: response.MediaContainer.Metadata ?? [], items: container.Metadata ?? [],
}; };
} }
@@ -221,7 +241,7 @@ class PlexAPI extends ExternalAPI {
}, },
mediaType: 'movie' | 'show' mediaType: 'movie' | 'show'
): Promise<PlexLibraryItem[]> { ): Promise<PlexLibraryItem[]> {
const response = await this.get<PlexLibraryResponse>( const response = await this.get<PlexLibraryResponseRaw>(
`/library/sections/${id}/all?type=${ `/library/sections/${id}/all?type=${
mediaType === 'show' ? '4' : '1' mediaType === 'show' ? '4' : '1'
}&sort=addedAt%3Adesc&addedAt>>=${Math.floor(options.addedAt / 1000)}`, }&sort=addedAt%3Adesc&addedAt>>=${Math.floor(options.addedAt / 1000)}`,
@@ -233,7 +253,21 @@ class PlexAPI extends ExternalAPI {
} }
); );
return response.MediaContainer.Metadata; const container = response.MediaContainer;
const items = container.Metadata ?? [];
logger.debug('Plex getRecentlyAdded raw response', {
label: 'Plex API',
libraryId: id,
mediaType,
addedAtFilter: options.addedAt,
addedAtFilterDate: new Date(options.addedAt).toISOString(),
metadataLength: container.Metadata?.length ?? 0,
directoryLength: container.Directory?.length ?? 0,
itemsReturned: items.length,
});
return items;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -385,26 +385,6 @@ class BaseScanner<T> {
} }
} }
// We want to skip specials when checking if a show is available
const isAllStandardSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes === season.totalEpisodes && season.episodes > 0
);
const isAll4kSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes4k === season.totalEpisodes &&
season.episodes4k > 0
);
if (media) { if (media) {
media.seasons = [...media.seasons, ...newSeasons]; media.seasons = [...media.seasons, ...newSeasons];
@@ -464,26 +444,21 @@ class BaseScanner<T> {
externalServiceSlug; externalServiceSlug;
} }
// If the show is already available, and there are no new seasons, dont adjust const nonSpecialSeasons = media.seasons.filter(
// the status. Skip specials when performing availability check (s) => s.seasonNumber !== 0
const shouldStayAvailable = );
media.status === MediaStatus.AVAILABLE &&
newSeasons.filter( // Check the actual season objects instead scanner input
(season) => // to determine overall availability status
season.status !== MediaStatus.UNKNOWN && const isAllStandardSeasonsAvailable =
season.status !== MediaStatus.DELETED && nonSpecialSeasons.length > 0 &&
season.seasonNumber !== 0 nonSpecialSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
).length === 0;
const shouldStayAvailable4k = const isAll4kSeasonsAvailable =
media.status4k === MediaStatus.AVAILABLE && nonSpecialSeasons.length > 0 &&
newSeasons.filter( nonSpecialSeasons.every((s) => s.status4k === MediaStatus.AVAILABLE);
(season) =>
season.status4k !== MediaStatus.UNKNOWN && media.status = isAllStandardSeasonsAvailable
season.status4k !== MediaStatus.DELETED &&
season.seasonNumber !== 0
).length === 0;
media.status =
isAllStandardSeasons || shouldStayAvailable
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: media.seasons.some( : media.seasons.some(
(season) => (season) =>
@@ -500,7 +475,7 @@ class BaseScanner<T> {
? MediaStatus.DELETED ? MediaStatus.DELETED
: MediaStatus.UNKNOWN; : MediaStatus.UNKNOWN;
media.status4k = media.status4k =
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow isAll4kSeasonsAvailable && this.enable4kShow
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: this.enable4kShow && : this.enable4kShow &&
media.seasons.some( media.seasons.some(
@@ -520,6 +495,22 @@ class BaseScanner<T> {
await mediaRepository.save(media); await mediaRepository.save(media);
this.log(`Updating existing title: ${title}`); this.log(`Updating existing title: ${title}`);
} else { } else {
// For new media, check actual newSeasons objects instead of scanner
// input to determine overall availability status
const nonSpecialNewSeasons = newSeasons.filter(
(s) => s.seasonNumber !== 0
);
const isAllStandardSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
const isAll4kSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every(
(s) => s.status4k === MediaStatus.AVAILABLE
);
const newMedia = new Media({ const newMedia = new Media({
mediaType: MediaType.TV, mediaType: MediaType.TV,
seasons: newSeasons, seasons: newSeasons,
@@ -564,7 +555,7 @@ class BaseScanner<T> {
) )
? jellyfinMediaId ? jellyfinMediaId
: undefined, : undefined,
status: isAllStandardSeasons status: isAllStandardSeasonsAvailable
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: newSeasons.some( : newSeasons.some(
(season) => (season) =>
@@ -578,7 +569,7 @@ class BaseScanner<T> {
? MediaStatus.PROCESSING ? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN, : MediaStatus.UNKNOWN,
status4k: status4k:
isAll4kSeasons && this.enable4kShow isAll4kSeasonsAvailable && this.enable4kShow
? MediaStatus.AVAILABLE ? MediaStatus.AVAILABLE
: this.enable4kShow && : this.enable4kShow &&
newSeasons.some( newSeasons.some(

View File

@@ -90,22 +90,41 @@ class PlexScanner
if (this.isRecentOnly) { if (this.isRecentOnly) {
for (const library of this.libraries) { for (const library of this.libraries) {
this.currentLibrary = library; this.currentLibrary = library;
this.log( const addedAtOpt = library.lastScan
`Beginning to process recently added for library: ${library.name}`,
'info',
{ lastScan: library.lastScan }
);
const libraryItems = await this.plexClient.getRecentlyAdded(
library.id,
library.lastScan
? { ? {
// We remove 10 minutes from the last scan as a buffer // We remove 10 minutes from the last scan as a buffer
addedAt: library.lastScan - 1000 * 60 * 10, addedAt: library.lastScan - 1000 * 60 * 10,
} }
: undefined;
this.log(
`Beginning to process recently added for library: ${library.name}`,
'info',
{
lastScan: library.lastScan,
addedAtFilter: addedAtOpt?.addedAt,
addedAtFilterDate: addedAtOpt
? new Date(addedAtOpt.addedAt).toISOString()
: undefined, : undefined,
}
);
const libraryItems = await this.plexClient.getRecentlyAdded(
library.id,
addedAtOpt ?? {
addedAt: Date.now() - 1000 * 60 * 60,
},
library.type library.type
); );
this.log(
`Recently added fetched ${libraryItems.length} items for library: ${library.name}`,
'debug',
{
libraryId: library.id,
itemCount: libraryItems.length,
lastScanWillUpdateTo: Date.now(),
}
);
// Bundle items up by rating keys // Bundle items up by rating keys
this.items = uniqWith(libraryItems, (mediaA, mediaB) => { this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) { if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) {

View File

@@ -132,15 +132,15 @@ export interface MainSettings {
tv: Quota; tv: Quota;
}; };
hideAvailable: boolean; hideAvailable: boolean;
hideBlacklisted: boolean; hideBlocklisted: boolean;
localLogin: boolean; localLogin: boolean;
mediaServerLogin: boolean; mediaServerLogin: boolean;
newPlexLogin: boolean; newPlexLogin: boolean;
discoverRegion: string; discoverRegion: string;
streamingRegion: string; streamingRegion: string;
originalLanguage: string; originalLanguage: string;
blacklistedTags: string; blocklistedTags: string;
blacklistedTagsLimit: number; blocklistedTagsLimit: number;
mediaServerType: number; mediaServerType: number;
partialRequestsEnabled: boolean; partialRequestsEnabled: boolean;
enableSpecialEpisodes: boolean; enableSpecialEpisodes: boolean;
@@ -181,7 +181,7 @@ interface FullPublicSettings extends PublicSettings {
applicationTitle: string; applicationTitle: string;
applicationUrl: string; applicationUrl: string;
hideAvailable: boolean; hideAvailable: boolean;
hideBlacklisted: boolean; hideBlocklisted: boolean;
localLogin: boolean; localLogin: boolean;
mediaServerLogin: boolean; mediaServerLogin: boolean;
movie4kEnabled: boolean; movie4kEnabled: boolean;
@@ -346,7 +346,7 @@ export type JobId =
| 'jellyfin-full-scan' | 'jellyfin-full-scan'
| 'image-cache-cleanup' | 'image-cache-cleanup'
| 'availability-sync' | 'availability-sync'
| 'process-blacklisted-tags'; | 'process-blocklisted-tags';
export interface AllSettings { export interface AllSettings {
clientId: string; clientId: string;
@@ -389,15 +389,15 @@ class Settings {
tv: {}, tv: {},
}, },
hideAvailable: false, hideAvailable: false,
hideBlacklisted: false, hideBlocklisted: false,
localLogin: true, localLogin: true,
mediaServerLogin: true, mediaServerLogin: true,
newPlexLogin: true, newPlexLogin: true,
discoverRegion: '', discoverRegion: '',
streamingRegion: '', streamingRegion: '',
originalLanguage: '', originalLanguage: '',
blacklistedTags: '', blocklistedTags: '',
blacklistedTagsLimit: 50, blocklistedTagsLimit: 50,
mediaServerType: MediaServerType.NOT_CONFIGURED, mediaServerType: MediaServerType.NOT_CONFIGURED,
partialRequestsEnabled: true, partialRequestsEnabled: true,
enableSpecialEpisodes: false, enableSpecialEpisodes: false,
@@ -570,7 +570,7 @@ class Settings {
'image-cache-cleanup': { 'image-cache-cleanup': {
schedule: '0 0 5 * * *', schedule: '0 0 5 * * *',
}, },
'process-blacklisted-tags': { 'process-blocklisted-tags': {
schedule: '0 30 1 */7 * *', schedule: '0 30 1 */7 * *',
}, },
}, },
@@ -671,7 +671,7 @@ class Settings {
applicationTitle: this.data.main.applicationTitle, applicationTitle: this.data.main.applicationTitle,
applicationUrl: this.data.main.applicationUrl, applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable, hideAvailable: this.data.main.hideAvailable,
hideBlacklisted: this.data.main.hideBlacklisted, hideBlocklisted: this.data.main.hideBlocklisted,
localLogin: this.data.main.localLogin, localLogin: this.data.main.localLogin,
mediaServerLogin: this.data.main.mediaServerLogin, mediaServerLogin: this.data.main.mediaServerLogin,
jellyfinExternalHost: this.data.jellyfin.externalHostname, 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 { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import { import {
BlacklistedMediaError, BlocklistedMediaError,
DuplicateMediaRequestError, DuplicateMediaRequestError,
MediaRequest, MediaRequest,
NoSeasonsAvailableError, NoSeasonsAvailableError,
@@ -145,8 +145,8 @@ class WatchlistSync {
errorMessage: e.message, errorMessage: e.message,
}); });
break; break;
// Blacklisted media should be silently ignored during watchlist sync to avoid spam // Blocklisted media should be silently ignored during watchlist sync to avoid spam
case BlacklistedMediaError: case BlocklistedMediaError:
break; break;
default: default:
logger.error('Failed to create media request from watchlist', { 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 RenameBlacklistToBlocklist1771080196816 implements MigrationInterface {
name = 'RenameBlacklistToBlocklist1771080196816';
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 RenameBlacklistToBlocklist1771080196816 implements MigrationInterface {
name = 'RenameBlacklistToBlocklist1771080196816';
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 { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist'; import { Blocklist } from '@server/entity/Blocklist';
import Media from '@server/entity/Media'; 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 { Permission } from '@server/lib/permissions';
import logger from '@server/logger'; import logger from '@server/logger';
import { isAuthenticated } from '@server/middleware/auth'; import { isAuthenticated } from '@server/middleware/auth';
@@ -10,53 +10,53 @@ import { Router } from 'express';
import { EntityNotFoundError, QueryFailedError } from 'typeorm'; import { EntityNotFoundError, QueryFailedError } from 'typeorm';
import { z } from 'zod'; import { z } from 'zod';
const blacklistRoutes = Router(); const blocklistRoutes = Router();
export const blacklistAdd = z.object({ export const blocklistAdd = z.object({
tmdbId: z.coerce.number(), tmdbId: z.coerce.number(),
mediaType: z.nativeEnum(MediaType), mediaType: z.nativeEnum(MediaType),
title: z.coerce.string().optional(), title: z.coerce.string().optional(),
user: z.coerce.number(), user: z.coerce.number(),
}); });
const blacklistGet = z.object({ const blocklistGet = z.object({
take: z.coerce.number().int().positive().default(25), take: z.coerce.number().int().positive().default(25),
skip: z.coerce.number().int().nonnegative().default(0), skip: z.coerce.number().int().nonnegative().default(0),
search: z.string().optional(), 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', type: 'or',
}), }),
async (req, res, next) => { async (req, res, next) => {
const { take, skip, search, filter } = blacklistGet.parse(req.query); const { take, skip, search, filter } = blocklistGet.parse(req.query);
try { try {
let query = getRepository(Blacklist) let query = getRepository(Blocklist)
.createQueryBuilder('blacklist') .createQueryBuilder('blocklist')
.leftJoinAndSelect('blacklist.user', 'user') .leftJoinAndSelect('blocklist.user', 'user')
.where('1 = 1'); // Allow use of andWhere later .where('1 = 1'); // Allow use of andWhere later
switch (filter) { switch (filter) {
case 'manual': case 'manual':
query = query.andWhere('blacklist.blacklistedTags IS NULL'); query = query.andWhere('blocklist.blocklistedTags IS NULL');
break; break;
case 'blacklistedTags': case 'blocklistedTags':
query = query.andWhere('blacklist.blacklistedTags IS NOT NULL'); query = query.andWhere('blocklist.blocklistedTags IS NOT NULL');
break; break;
} }
if (search) { if (search) {
query = query.andWhere('blacklist.title like :title', { query = query.andWhere('blocklist.title like :title', {
title: `%${search}%`, title: `%${search}%`,
}); });
} }
const [blacklistedItems, itemsCount] = await query const [blocklistedItems, itemsCount] = await query
.orderBy('blacklist.createdAt', 'DESC') .orderBy('blocklist.createdAt', 'DESC')
.take(take) .take(take)
.skip(skip) .skip(skip)
.getManyAndCount(); .getManyAndCount();
@@ -68,35 +68,35 @@ blacklistRoutes.get(
results: itemsCount, results: itemsCount,
page: Math.ceil(skip / take) + 1, page: Math.ceil(skip / take) + 1,
}, },
results: blacklistedItems, results: blocklistedItems,
} as BlacklistResultsResponse); } as BlocklistResultsResponse);
} catch (error) { } catch (error) {
logger.error('Something went wrong while retrieving blacklisted items', { logger.error('Something went wrong while retrieving blocklisted items', {
label: 'Blacklist', label: 'Blocklist',
errorMessage: error.message, errorMessage: error.message,
}); });
return next({ return next({
status: 500, status: 500,
message: 'Unable to retrieve blacklisted items.', message: 'Unable to retrieve blocklisted items.',
}); });
} }
} }
); );
blacklistRoutes.get( blocklistRoutes.get(
'/:id', '/:id',
isAuthenticated([Permission.MANAGE_BLACKLIST], { isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or', type: 'or',
}), }),
async (req, res, next) => { async (req, res, next) => {
try { 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) }, where: { tmdbId: Number(req.params.id) },
}); });
return res.status(200).send(blacklistItem); return res.status(200).send(blocklistItem);
} catch (e) { } catch (e) {
if (e instanceof EntityNotFoundError) { if (e instanceof EntityNotFoundError) {
return next({ return next({
@@ -109,17 +109,17 @@ blacklistRoutes.get(
} }
); );
blacklistRoutes.post( blocklistRoutes.post(
'/', '/',
isAuthenticated([Permission.MANAGE_BLACKLIST], { isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or', type: 'or',
}), }),
async (req, res, next) => { async (req, res, next) => {
try { try {
const values = blacklistAdd.parse(req.body); const values = blocklistAdd.parse(req.body);
await Blacklist.addToBlacklist({ await Blocklist.addToBlocklist({
blacklistRequest: values, blocklistRequest: values,
}); });
return res.status(201).send(); return res.status(201).send();
@@ -131,12 +131,12 @@ blacklistRoutes.post(
if (error instanceof QueryFailedError) { if (error instanceof QueryFailedError) {
switch (error.driverError.errno) { switch (error.driverError.errno) {
case 19: case 19:
return next({ status: 412, message: 'Item already blacklisted' }); return next({ status: 412, message: 'Item already blocklisted' });
default: default:
logger.warn('Something wrong with data blacklist', { logger.warn('Something wrong with data blocklist', {
tmdbId: req.body.tmdbId, tmdbId: req.body.tmdbId,
mediaType: req.body.mediaType, mediaType: req.body.mediaType,
label: 'Blacklist', label: 'Blocklist',
}); });
return next({ status: 409, message: 'Something wrong' }); return next({ status: 409, message: 'Something wrong' });
} }
@@ -147,20 +147,20 @@ blacklistRoutes.post(
} }
); );
blacklistRoutes.delete( blocklistRoutes.delete(
'/:id', '/:id',
isAuthenticated([Permission.MANAGE_BLACKLIST], { isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or', type: 'or',
}), }),
async (req, res, next) => { async (req, res, next) => {
try { 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) }, where: { tmdbId: Number(req.params.id) },
}); });
await blacklisteRepository.remove(blacklistItem); await blocklisteRepository.remove(blocklistItem);
const mediaRepository = getRepository(Media); 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 { getSettings } from '@server/lib/settings';
import logger from '@server/logger'; import logger from '@server/logger';
import { checkUser, isAuthenticated } from '@server/middleware/auth'; import { checkUser, isAuthenticated } from '@server/middleware/auth';
import deprecatedRoute from '@server/middleware/deprecation';
import { mapProductionCompany } from '@server/models/Movie'; import { mapProductionCompany } from '@server/models/Movie';
import { mapNetwork } from '@server/models/Tv'; import { mapNetwork } from '@server/models/Tv';
import { mapWatchProviderDetails } from '@server/models/common'; import { mapWatchProviderDetails } from '@server/models/common';
@@ -28,7 +29,7 @@ import restartFlag from '@server/utils/restartFlag';
import { isPerson } from '@server/utils/typeHelpers'; import { isPerson } from '@server/utils/typeHelpers';
import { Router } from 'express'; import { Router } from 'express';
import authRoutes from './auth'; import authRoutes from './auth';
import blacklistRoutes from './blacklist'; import blocklistRoutes from './blocklist';
import collectionRoutes from './collection'; import collectionRoutes from './collection';
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover'; import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
import issueRoutes from './issue'; import issueRoutes from './issue';
@@ -151,7 +152,17 @@ router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/discover', isAuthenticated(), discoverRoutes);
router.use('/request', isAuthenticated(), requestRoutes); router.use('/request', isAuthenticated(), requestRoutes);
router.use('/watchlist', isAuthenticated(), watchlistRoutes); 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('/movie', isAuthenticated(), movieRoutes);
router.use('/tv', isAuthenticated(), tvRoutes); router.use('/tv', isAuthenticated(), tvRoutes);
router.use('/media', isAuthenticated(), mediaRoutes); router.use('/media', isAuthenticated(), mediaRoutes);

View File

@@ -8,7 +8,7 @@ import {
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import { import {
BlacklistedMediaError, BlocklistedMediaError,
DuplicateMediaRequestError, DuplicateMediaRequestError,
MediaRequest, MediaRequest,
NoSeasonsAvailableError, NoSeasonsAvailableError,
@@ -326,7 +326,7 @@ requestRoutes.post<never, MediaRequest, MediaRequestBody>(
return next({ status: 409, message: error.message }); return next({ status: 409, message: error.message });
case NoSeasonsAvailableError: case NoSeasonsAvailableError:
return next({ status: 202, message: error.message }); return next({ status: 202, message: error.message });
case BlacklistedMediaError: case BlocklistedMediaError:
return next({ status: 403, message: error.message }); return next({ status: 403, message: error.message });
default: default:
return next({ status: 500, message: error.message }); 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 Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
@@ -20,9 +20,9 @@ import {
TrashIcon, TrashIcon,
} from '@heroicons/react/24/solid'; } from '@heroicons/react/24/solid';
import type { import type {
BlacklistItem, BlocklistItem,
BlacklistResultsResponse, BlocklistResultsResponse,
} from '@server/interfaces/api/blacklistInterfaces'; } from '@server/interfaces/api/blocklistInterfaces';
import type { MovieDetails } from '@server/models/Movie'; import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv'; import type { TvDetails } from '@server/models/Tv';
import axios from 'axios'; import axios from 'axios';
@@ -35,31 +35,31 @@ import { FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages('components.Blacklist', { const messages = defineMessages('components.Blocklist', {
blacklistsettings: 'Blacklist Settings', blocklistsettings: 'Blocklist Settings',
blacklistSettingsDescription: 'Manage blacklisted media.', blocklistSettingsDescription: 'Manage blocklisted media.',
mediaName: 'Name', mediaName: 'Name',
mediaType: 'Type', mediaType: 'Type',
mediaTmdbId: 'tmdb Id', mediaTmdbId: 'tmdb Id',
blacklistdate: 'date', blocklistdate: 'date',
blacklistedby: '{date} by {user}', blocklistedby: '{date} by {user}',
blacklistNotFoundError: '<strong>{title}</strong> is not blacklisted.', blocklistNotFoundError: '<strong>{title}</strong> is not blocklisted.',
filterManual: 'Manual', filterManual: 'Manual',
filterBlacklistedTags: 'Blacklisted Tags', filterBlocklistedTags: 'Blocklisted Tags',
showAllBlacklisted: 'Show All Blacklisted Media', showAllBlocklisted: 'Show All Blocklisted Media',
}); });
enum Filter { enum Filter {
ALL = 'all', ALL = 'all',
MANUAL = 'manual', MANUAL = 'manual',
BLACKLISTEDTAGS = 'blacklistedTags', BLOCKLISTEDTAGS = 'blocklistedTags',
} }
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined; return (movie as MovieDetails).title !== undefined;
}; };
const Blacklist = () => { const Blocklist = () => {
const [currentPageSize, setCurrentPageSize] = useState<number>(10); const [currentPageSize, setCurrentPageSize] = useState<number>(10);
const [searchFilter, debouncedSearchFilter, setSearchFilter] = const [searchFilter, debouncedSearchFilter, setSearchFilter] =
useDebouncedState(''); useDebouncedState('');
@@ -75,8 +75,8 @@ const Blacklist = () => {
data, data,
error, error,
mutate: revalidate, mutate: revalidate,
} = useSWR<BlacklistResultsResponse>( } = useSWR<BlocklistResultsResponse>(
`/api/v1/blacklist/?take=${currentPageSize}&skip=${ `/api/v1/blocklist/?take=${currentPageSize}&skip=${
pageIndex * currentPageSize pageIndex * currentPageSize
}&filter=${currentFilter}${ }&filter=${currentFilter}${
debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : '' debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
@@ -107,9 +107,9 @@ const Blacklist = () => {
return ( 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"> <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="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"> <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"> <option value="manual">
{intl.formatMessage(messages.filterManual)} {intl.formatMessage(messages.filterManual)}
</option> </option>
<option value="blacklistedTags"> <option value="blocklistedTags">
{intl.formatMessage(messages.filterBlacklistedTags)} {intl.formatMessage(messages.filterBlocklistedTags)}
</option> </option>
</select> </select>
</div> </div>
@@ -170,16 +170,16 @@ const Blacklist = () => {
buttonType="primary" buttonType="primary"
onClick={() => setCurrentFilter(Filter.ALL)} onClick={() => setCurrentFilter(Filter.ALL)}
> >
{intl.formatMessage(messages.showAllBlacklisted)} {intl.formatMessage(messages.showAllBlocklisted)}
</Button> </Button>
</div> </div>
)} )}
</div> </div>
) : ( ) : (
data.results.map((item: BlacklistItem) => { data.results.map((item: BlocklistItem) => {
return ( return (
<div className="py-2" key={`request-list-${item.tmdbId}`}> <div className="py-2" key={`request-list-${item.tmdbId}`}>
<BlacklistedItem item={item} revalidateList={revalidate} /> <BlocklistedItem item={item} revalidateList={revalidate} />
</div> </div>
); );
}) })
@@ -260,14 +260,14 @@ const Blacklist = () => {
); );
}; };
export default Blacklist; export default Blocklist;
interface BlacklistedItemProps { interface BlocklistedItemProps {
item: BlacklistItem; item: BlocklistItem;
revalidateList: () => void; revalidateList: () => void;
} }
const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => { const BlocklistedItem = ({ item, revalidateList }: BlocklistedItemProps) => {
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
const { addToast } = useToasts(); const { addToast } = useToasts();
const { ref, inView } = useInView({ 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); setIsUpdating(true);
try { try {
await axios.delete(`/api/v1/blacklist/${tmdbId}`); await axios.delete(`/api/v1/blocklist/${tmdbId}`);
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { {intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title, title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})} })}
@@ -309,7 +309,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
} catch { } catch {
addToast(intl.formatMessage(globalMessages.blacklistError), { addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,
}); });
@@ -389,17 +389,17 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
<div className="card-field"> <div className="card-field">
<span className="card-field-name">Status</span> <span className="card-field-name">Status</span>
<Badge badgeType="danger"> <Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)} {intl.formatMessage(globalMessages.blocklisted)}
</Badge> </Badge>
</div> </div>
{item.createdAt && ( {item.createdAt && (
<div className="card-field"> <div className="card-field">
<span className="card-field-name"> <span className="card-field-name">
{intl.formatMessage(globalMessages.blacklisted)} {intl.formatMessage(globalMessages.blocklisted)}
</span> </span>
<span className="flex truncate text-sm text-gray-300"> <span className="flex truncate text-sm text-gray-300">
{intl.formatMessage(messages.blacklistedby, { {intl.formatMessage(messages.blocklistedby, {
date: ( date: (
<FormattedRelativeTime <FormattedRelativeTime
value={Math.floor( value={Math.floor(
@@ -426,9 +426,9 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
</span> </span>
</span> </span>
</Link> </Link>
) : item.blacklistedTags ? ( ) : item.blocklistedTags ? (
<span className="ml-1"> <span className="ml-1">
<BlacklistedTagsBadge data={item} /> <BlocklistedTagsBadge data={item} />
</span> </span>
) : ( ) : (
<span className="ml-1 truncate text-sm font-semibold"> <span className="ml-1 truncate text-sm font-semibold">
@@ -457,10 +457,10 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
</div> </div>
</div> </div>
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0"> <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 <ConfirmButton
onClick={() => onClick={() =>
removeFromBlacklist( removeFromBlocklist(
item.tmdbId, item.tmdbId,
title && (isMovie(title) ? title.title : title.name) title && (isMovie(title) ? title.title : title.name)
) )
@@ -474,7 +474,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
> >
<TrashIcon /> <TrashIcon />
<span> <span>
{intl.formatMessage(globalMessages.removefromBlacklist)} {intl.formatMessage(globalMessages.removefromBlocklist)}
</span> </span>
</ConfirmButton> </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 Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button'; import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import LoadingSpinner from '@app/components/Common/LoadingSpinner';
@@ -7,7 +7,7 @@ import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages'; import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid'; 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 axios from 'axios';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
@@ -15,37 +15,37 @@ import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
const messages = defineMessages('component.BlacklistBlock', { const messages = defineMessages('component.BlocklistBlock', {
blacklistedby: 'Blacklisted By', blocklistedby: 'Blocklisted By',
blacklistdate: 'Blacklisted date', blocklistdate: 'Blocklisted date',
}); });
interface BlacklistBlockProps { interface BlocklistBlockProps {
tmdbId: number; tmdbId: number;
onUpdate?: () => void; onUpdate?: () => void;
onDelete?: () => void; onDelete?: () => void;
} }
const BlacklistBlock = ({ const BlocklistBlock = ({
tmdbId, tmdbId,
onUpdate, onUpdate,
onDelete, onDelete,
}: BlacklistBlockProps) => { }: BlocklistBlockProps) => {
const { user } = useUser(); const { user } = useUser();
const intl = useIntl(); const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const { addToast } = useToasts(); 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); setIsUpdating(true);
try { try {
await axios.delete('/api/v1/blacklist/' + tmdbId); await axios.delete('/api/v1/blocklist/' + tmdbId);
addToast( addToast(
<span> <span>
{intl.formatMessage(globalMessages.removeFromBlacklistSuccess, { {intl.formatMessage(globalMessages.removeFromBlocklistSuccess, {
title, title,
strong: (msg: React.ReactNode) => <strong>{msg}</strong>, strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
})} })}
@@ -53,7 +53,7 @@ const BlacklistBlock = ({
{ appearance: 'success', autoDismiss: true } { appearance: 'success', autoDismiss: true }
); );
} catch { } catch {
addToast(intl.formatMessage(globalMessages.blacklistError), { addToast(intl.formatMessage(globalMessages.blocklistError), {
appearance: 'error', appearance: 'error',
autoDismiss: true, autoDismiss: true,
}); });
@@ -80,7 +80,7 @@ const BlacklistBlock = ({
<div className="white mb-1 flex flex-nowrap"> <div className="white mb-1 flex flex-nowrap">
{data.user ? ( {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" /> <UserIcon className="mr-1.5 h-5 w-5 min-w-0 flex-shrink-0" />
</Tooltip> </Tooltip>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
@@ -97,23 +97,23 @@ const BlacklistBlock = ({
</Link> </Link>
</span> </span>
</> </>
) : data.blacklistedTags ? ( ) : data.blocklistedTags ? (
<> <>
<span className="w-40 truncate md:w-auto"> <span className="w-40 truncate md:w-auto">
{intl.formatMessage(messages.blacklistedby)}:&nbsp; {intl.formatMessage(messages.blocklistedby)}:&nbsp;
</span> </span>
<BlacklistedTagsBadge data={data} /> <BlocklistedTagsBadge data={data} />
</> </>
) : null} ) : null}
</div> </div>
</div> </div>
<div className="ml-2 flex flex-shrink-0 flex-wrap"> <div className="ml-2 flex flex-shrink-0 flex-wrap">
<Tooltip <Tooltip
content={intl.formatMessage(globalMessages.removefromBlacklist)} content={intl.formatMessage(globalMessages.removefromBlocklist)}
> >
<Button <Button
buttonType="danger" buttonType="danger"
onClick={() => removeFromBlacklist(data.tmdbId, data.title)} onClick={() => removeFromBlocklist(data.tmdbId, data.title)}
disabled={isUpdating} disabled={isUpdating}
> >
<TrashIcon className="icon-sm" /> <TrashIcon className="icon-sm" />
@@ -125,12 +125,12 @@ const BlacklistBlock = ({
<div className="sm:flex"> <div className="sm:flex">
<div className="mr-6 flex items-center text-sm leading-5"> <div className="mr-6 flex items-center text-sm leading-5">
<Badge badgeType="danger"> <Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)} {intl.formatMessage(globalMessages.blocklisted)}
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0"> <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" /> <CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip> </Tooltip>
<span> <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 { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
interface BlacklistModalProps { interface BlocklistModalProps {
tmdbId: number; tmdbId: number;
type: 'movie' | 'tv' | 'collection'; type: 'movie' | 'tv' | 'collection';
show: boolean; show: boolean;
@@ -17,8 +17,8 @@ interface BlacklistModalProps {
isUpdating?: boolean; isUpdating?: boolean;
} }
const messages = defineMessages('component.BlacklistModal', { const messages = defineMessages('component.BlocklistModal', {
blacklisting: 'Blacklisting', blocklisting: 'Blocklisting',
}); });
const isMovie = ( const isMovie = (
@@ -28,14 +28,14 @@ const isMovie = (
return (movie as MovieDetails).title !== undefined; return (movie as MovieDetails).title !== undefined;
}; };
const BlacklistModal = ({ const BlocklistModal = ({
tmdbId, tmdbId,
type, type,
show, show,
onComplete, onComplete,
onCancel, onCancel,
isUpdating, isUpdating,
}: BlacklistModalProps) => { }: BlocklistModalProps) => {
const intl = useIntl(); const intl = useIntl();
const [data, setData] = useState<TvDetails | MovieDetails | null>(null); const [data, setData] = useState<TvDetails | MovieDetails | null>(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -67,7 +67,7 @@ const BlacklistModal = ({
<Modal <Modal
loading={!data && !error} loading={!data && !error}
backgroundClickable backgroundClickable
title={`${intl.formatMessage(globalMessages.blacklist)} ${ title={`${intl.formatMessage(globalMessages.blocklist)} ${
isMovie(data) isMovie(data)
? intl.formatMessage(globalMessages.movie) ? intl.formatMessage(globalMessages.movie)
: intl.formatMessage(globalMessages.tvshow) : intl.formatMessage(globalMessages.tvshow)
@@ -77,8 +77,8 @@ const BlacklistModal = ({
onOk={onComplete} onOk={onComplete}
okText={ okText={
isUpdating isUpdating
? intl.formatMessage(messages.blacklisting) ? intl.formatMessage(messages.blocklisting)
: intl.formatMessage(globalMessages.blacklist) : intl.formatMessage(globalMessages.blocklist)
} }
okButtonType="danger" okButtonType="danger"
okDisabled={isUpdating} 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 Tooltip from '@app/components/Common/Tooltip';
import defineMessages from '@app/utils/defineMessages'; import defineMessages from '@app/utils/defineMessages';
import { TagIcon } from '@heroicons/react/20/solid'; 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 type { Keyword } from '@server/models/common';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
const messages = defineMessages('components.Settings', { const messages = defineMessages('components.Settings', {
blacklistedTagsText: 'Blacklisted Tags', blocklistedTagsText: 'Blocklisted Tags',
}); });
interface BlacklistedTagsBadgeProps { interface BlocklistedTagsBadgeProps {
data: BlacklistItem; data: BlocklistItem;
} }
const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => { const BlocklistedTagsBadge = ({ data }: BlocklistedTagsBadgeProps) => {
const [tagNamesBlacklistedFor, setTagNamesBlacklistedFor] = const [tagNamesBlocklistedFor, setTagNamesBlocklistedFor] =
useState<string>('Loading...'); useState<string>('Loading...');
const intl = useIntl(); const intl = useIntl();
useEffect(() => { useEffect(() => {
if (!data.blacklistedTags) { if (!data.blocklistedTags) {
return; return;
} }
const keywordIds = data.blacklistedTags.slice(1, -1).split(','); const keywordIds = data.blocklistedTags.slice(1, -1).split(',');
Promise.all( Promise.all(
keywordIds.map(async (keywordId) => { keywordIds.map(async (keywordId) => {
const { data } = await axios.get<Keyword | null>( const { data } = await axios.get<Keyword | null>(
@@ -35,13 +35,13 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
return data?.name || `[Invalid: ${keywordId}]`; return data?.name || `[Invalid: ${keywordId}]`;
}) })
).then((keywords) => { ).then((keywords) => {
setTagNamesBlacklistedFor(keywords.join(', ')); setTagNamesBlocklistedFor(keywords.join(', '));
}); });
}, [data.blacklistedTags]); }, [data.blocklistedTags]);
return ( return (
<Tooltip <Tooltip
content={tagNamesBlacklistedFor} content={tagNamesBlocklistedFor}
tooltipConfig={{ followCursor: false }} tooltipConfig={{ followCursor: false }}
> >
<Badge <Badge
@@ -49,10 +49,10 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => {
className="items-center border border-red-500 !text-red-400" className="items-center border border-red-500 !text-red-400"
> >
<TagIcon className="mr-1 h-4" /> <TagIcon className="mr-1 h-4" />
{intl.formatMessage(messages.blacklistedTagsText)} {intl.formatMessage(messages.blocklistedTagsText)}
</Badge> </Badge>
</Tooltip> </Tooltip>
); );
}; };
export default BlacklistedTagsBadge; export default BlocklistedTagsBadge;

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ export const menuMessages = defineMessages('components.Layout.Sidebar', {
browsemovies: 'Movies', browsemovies: 'Movies',
browsetv: 'Series', browsetv: 'Series',
requests: 'Requests', requests: 'Requests',
blacklist: 'Blacklist', blocklist: 'Blocklist',
issues: 'Issues', issues: 'Issues',
users: 'Users', users: 'Users',
settings: 'Settings', settings: 'Settings',
@@ -79,13 +79,13 @@ const SidebarLinks: SidebarLinkProps[] = [
activeRegExp: /^\/requests/, activeRegExp: /^\/requests/,
}, },
{ {
href: '/blacklist', href: '/blocklist',
messagesKey: 'blacklist', messagesKey: 'blocklist',
svgIcon: <EyeSlashIcon className="mr-3 h-6 w-6" />, svgIcon: <EyeSlashIcon className="mr-3 h-6 w-6" />,
activeRegExp: /^\/blacklist/, activeRegExp: /^\/blocklist/,
requiredPermission: [ requiredPermission: [
Permission.MANAGE_BLACKLIST, Permission.MANAGE_BLOCKLIST,
Permission.VIEW_BLACKLIST, Permission.VIEW_BLOCKLIST,
], ],
permissionType: 'or', 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 Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage'; import CachedImage from '@app/components/Common/CachedImage';
import ConfirmButton from '@app/components/Common/ConfirmButton'; import ConfirmButton from '@app/components/Common/ConfirmButton';
@@ -314,13 +314,13 @@ const ManageSlideOver = ({
</div> </div>
</div> </div>
)} )}
{data.mediaInfo?.status === MediaStatus.BLACKLISTED && ( {data.mediaInfo?.status === MediaStatus.BLOCKLISTED && (
<div> <div>
<h3 className="mb-2 text-xl font-bold"> <h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(globalMessages.blacklist)} {intl.formatMessage(globalMessages.blocklist)}
</h3> </h3>
<div className="overflow-hidden rounded-md border border-gray-700 shadow"> <div className="overflow-hidden rounded-md border border-gray-700 shadow">
<BlacklistBlock <BlocklistBlock
tmdbId={data.mediaInfo.tmdbId} tmdbId={data.mediaInfo.tmdbId}
onUpdate={() => revalidate()} onUpdate={() => revalidate()}
onDelete={() => onClose()} onDelete={() => onClose()}
@@ -651,7 +651,7 @@ const ManageSlideOver = ({
)} )}
{hasPermission(Permission.ADMIN) && {hasPermission(Permission.ADMIN) &&
data?.mediaInfo && data?.mediaInfo &&
data.mediaInfo.status !== MediaStatus.BLACKLISTED && ( data.mediaInfo.status !== MediaStatus.BLOCKLISTED && (
<div> <div>
<h3 className="mb-2 text-xl font-bold"> <h3 className="mb-2 text-xl font-bold">
{intl.formatMessage(messages.manageModalAdvanced)} {intl.formatMessage(messages.manageModalAdvanced)}

View File

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

View File

@@ -78,13 +78,13 @@ export const messages = defineMessages('components.PermissionEdit', {
viewwatchlists: 'View {mediaServerName} Watchlists', viewwatchlists: 'View {mediaServerName} Watchlists',
viewwatchlistsDescription: viewwatchlistsDescription:
"Grant permission to view other users' {mediaServerName} Watchlists.", "Grant permission to view other users' {mediaServerName} Watchlists.",
manageblacklist: 'Manage Blacklist', manageblocklist: 'Manage Blocklist',
manageblacklistDescription: 'Grant permission to manage blacklisted media.', manageblocklistDescription: 'Grant permission to manage blocklisted media.',
blacklistedItems: 'Blacklist media.', blocklistedItems: 'Blocklist media.',
blacklistedItemsDescription: 'Grant permission to blacklist media.', blocklistedItemsDescription: 'Grant permission to blocklist media.',
viewblacklistedItems: 'View blacklisted media.', viewblocklistedItems: 'View blocklisted media.',
viewblacklistedItemsDescription: viewblocklistedItemsDescription:
'Grant permission to view blacklisted media.', 'Grant permission to view blocklisted media.',
}); });
interface PermissionEditProps { interface PermissionEditProps {
@@ -340,18 +340,18 @@ export const PermissionEdit = ({
], ],
}, },
{ {
id: 'manageblacklist', id: 'manageblocklist',
name: intl.formatMessage(messages.manageblacklist), name: intl.formatMessage(messages.manageblocklist),
description: intl.formatMessage(messages.manageblacklistDescription), description: intl.formatMessage(messages.manageblocklistDescription),
permission: Permission.MANAGE_BLACKLIST, permission: Permission.MANAGE_BLOCKLIST,
children: [ children: [
{ {
id: 'viewblacklisteditems', id: 'viewblocklisteditems',
name: intl.formatMessage(messages.viewblacklistedItems), name: intl.formatMessage(messages.viewblocklistedItems),
description: intl.formatMessage( 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', type: 'or',
}) && }) &&
media && media &&
media.status !== MediaStatus.BLACKLISTED && media.status !== MediaStatus.BLOCKLISTED &&
!isShowComplete !isShowComplete
) { ) {
buttons.push({ buttons.push({
@@ -345,7 +345,7 @@ const RequestButton = ({
type: 'or', type: 'or',
}) && }) &&
media && media &&
media.status4k !== MediaStatus.BLACKLISTED && media.status4k !== MediaStatus.BLOCKLISTED &&
!is4kShowComplete && !is4kShowComplete &&
settings.currentSettings.series4kEnabled settings.currentSettings.series4kEnabled
) { ) {

View File

@@ -68,7 +68,7 @@ const CollectionRequestModal = ({
const getAllParts = (): number[] => { const getAllParts = (): number[] => {
return (data?.parts ?? []) return (data?.parts ?? [])
.filter((part) => part.mediaInfo?.status !== MediaStatus.BLACKLISTED) .filter((part) => part.mediaInfo?.status !== MediaStatus.BLOCKLISTED)
.map((part) => part.id); .map((part) => part.id);
}; };
@@ -257,8 +257,8 @@ const CollectionRequestModal = ({
{ type: 'or' } { type: 'or' }
); );
const blacklistVisibility = hasPermission( const blocklistVisibility = hasPermission(
[Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], [Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST],
{ type: 'or' } { type: 'or' }
); );
@@ -360,9 +360,9 @@ const CollectionRequestModal = ({
<tbody className="divide-y divide-gray-700"> <tbody className="divide-y divide-gray-700">
{data?.parts {data?.parts
.filter((part) => { .filter((part) => {
if (!blacklistVisibility) if (!blocklistVisibility)
return ( return (
part.mediaInfo?.status !== MediaStatus.BLACKLISTED part.mediaInfo?.status !== MediaStatus.BLOCKLISTED
); );
return part; return part;
}) })
@@ -381,7 +381,7 @@ const CollectionRequestModal = ({
<tr key={`part-${part.id}`}> <tr key={`part-${part.id}`}>
<td <td
className={`whitespace-nowrap px-4 py-4 text-sm font-medium leading-5 text-gray-100 ${ 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' 'pointer-events-none opacity-50'
}`} }`}
> >
@@ -391,7 +391,7 @@ const CollectionRequestModal = ({
aria-checked={ aria-checked={
(!!partMedia && (!!partMedia &&
partMedia.status !== partMedia.status !==
MediaStatus.BLACKLISTED) || MediaStatus.BLOCKLISTED) ||
isSelectedPart(part.id) isSelectedPart(part.id)
} }
onClick={() => togglePart(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 ${ className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none ${
(!!partMedia && (!!partMedia &&
partMedia.status !== partMedia.status !==
MediaStatus.BLACKLISTED) || MediaStatus.BLOCKLISTED) ||
partRequest || partRequest ||
(quota?.movie.limit && (quota?.movie.limit &&
currentlyRemaining <= 0 && currentlyRemaining <= 0 &&
@@ -417,7 +417,7 @@ const CollectionRequestModal = ({
className={`${ className={`${
(!!partMedia && (!!partMedia &&
partMedia.status !== partMedia.status !==
MediaStatus.BLACKLISTED) || MediaStatus.BLOCKLISTED) ||
partRequest || partRequest ||
isSelectedPart(part.id) isSelectedPart(part.id)
? 'bg-indigo-500' ? 'bg-indigo-500'
@@ -429,7 +429,7 @@ const CollectionRequestModal = ({
className={`${ className={`${
(!!partMedia && (!!partMedia &&
partMedia.status !== partMedia.status !==
MediaStatus.BLACKLISTED) || MediaStatus.BLOCKLISTED) ||
partRequest || partRequest ||
isSelectedPart(part.id) isSelectedPart(part.id)
? 'translate-x-5' ? 'translate-x-5'
@@ -440,7 +440,7 @@ const CollectionRequestModal = ({
</td> </td>
<td <td
className={`flex items-center px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6 ${ 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' 'pointer-events-none opacity-50'
}`} }`}
> >
@@ -502,9 +502,9 @@ const CollectionRequestModal = ({
{intl.formatMessage(globalMessages.available)} {intl.formatMessage(globalMessages.available)}
</Badge> </Badge>
)} )}
{partMedia?.status === MediaStatus.BLACKLISTED && ( {partMedia?.status === MediaStatus.BLOCKLISTED && (
<Badge badgeType="danger"> <Badge badgeType="danger">
{intl.formatMessage(globalMessages.blacklisted)} {intl.formatMessage(globalMessages.blocklisted)}
</Badge> </Badge>
)} )}
</td> </td>

Some files were not shown because too many files have changed in this diff Show More