refactor: rename blacklist to blocklist (#2157)
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me> Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: 0xsysr3ll <0xsysr3ll@pm.me> Co-authored-by: gauthier-th <mail@gauthierth.fr>
This commit is contained in:
@@ -17,6 +17,6 @@ export enum MediaStatus {
|
||||
PROCESSING,
|
||||
PARTIALLY_AVAILABLE,
|
||||
AVAILABLE,
|
||||
BLACKLISTED,
|
||||
BLOCKLISTED,
|
||||
DELETED,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MediaStatus, type MediaType } from '@server/constants/media';
|
||||
import dataSource from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
|
||||
import type { BlocklistItem } from '@server/interfaces/api/blocklistInterfaces';
|
||||
import { DbAwareColumn } from '@server/utils/DbColumnHelper';
|
||||
import type { EntityManager } from 'typeorm';
|
||||
import {
|
||||
@@ -19,7 +19,7 @@ import type { ZodNumber, ZodOptional, ZodString } from 'zod';
|
||||
|
||||
@Entity()
|
||||
@Unique(['tmdbId'])
|
||||
export class Blacklist implements BlacklistItem {
|
||||
export class Blocklist implements BlocklistItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@@ -38,65 +38,65 @@ export class Blacklist implements BlacklistItem {
|
||||
})
|
||||
user?: User;
|
||||
|
||||
@OneToOne(() => Media, (media) => media.blacklist, {
|
||||
@OneToOne(() => Media, (media) => media.blocklist, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public media: Media;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
public blacklistedTags?: string;
|
||||
public blocklistedTags?: string;
|
||||
|
||||
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
public createdAt: Date;
|
||||
|
||||
constructor(init?: Partial<Blacklist>) {
|
||||
constructor(init?: Partial<Blocklist>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
public static async addToBlacklist(
|
||||
public static async addToBlocklist(
|
||||
{
|
||||
blacklistRequest,
|
||||
blocklistRequest,
|
||||
}: {
|
||||
blacklistRequest: {
|
||||
blocklistRequest: {
|
||||
mediaType: MediaType;
|
||||
title?: ZodOptional<ZodString>['_output'];
|
||||
tmdbId: ZodNumber['_output'];
|
||||
blacklistedTags?: string;
|
||||
blocklistedTags?: string;
|
||||
};
|
||||
},
|
||||
entityManager?: EntityManager
|
||||
): Promise<void> {
|
||||
const em = entityManager ?? dataSource;
|
||||
const blacklist = new this({
|
||||
...blacklistRequest,
|
||||
const blocklist = new this({
|
||||
...blocklistRequest,
|
||||
});
|
||||
|
||||
const mediaRepository = em.getRepository(Media);
|
||||
let media = await mediaRepository.findOne({
|
||||
where: {
|
||||
tmdbId: blacklistRequest.tmdbId,
|
||||
tmdbId: blocklistRequest.tmdbId,
|
||||
},
|
||||
});
|
||||
|
||||
const blacklistRepository = em.getRepository(this);
|
||||
const blocklistRepository = em.getRepository(this);
|
||||
|
||||
await blacklistRepository.save(blacklist);
|
||||
await blocklistRepository.save(blocklist);
|
||||
|
||||
if (!media) {
|
||||
media = new Media({
|
||||
tmdbId: blacklistRequest.tmdbId,
|
||||
status: MediaStatus.BLACKLISTED,
|
||||
status4k: MediaStatus.BLACKLISTED,
|
||||
mediaType: blacklistRequest.mediaType,
|
||||
blacklist: Promise.resolve(blacklist),
|
||||
tmdbId: blocklistRequest.tmdbId,
|
||||
status: MediaStatus.BLOCKLISTED,
|
||||
status4k: MediaStatus.BLOCKLISTED,
|
||||
mediaType: blocklistRequest.mediaType,
|
||||
blocklist: Promise.resolve(blocklist),
|
||||
});
|
||||
|
||||
await mediaRepository.save(media);
|
||||
} else {
|
||||
media.blacklist = Promise.resolve(blacklist);
|
||||
media.status = MediaStatus.BLACKLISTED;
|
||||
media.status4k = MediaStatus.BLACKLISTED;
|
||||
media.blocklist = Promise.resolve(blocklist);
|
||||
media.status = MediaStatus.BLOCKLISTED;
|
||||
media.status4k = MediaStatus.BLOCKLISTED;
|
||||
|
||||
await mediaRepository.save(media);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { Blacklist } from '@server/entity/Blacklist';
|
||||
import { Blocklist } from '@server/entity/Blocklist';
|
||||
import type { User } from '@server/entity/User';
|
||||
import { Watchlist } from '@server/entity/Watchlist';
|
||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
@@ -126,8 +126,8 @@ class Media {
|
||||
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
|
||||
public issues: Issue[];
|
||||
|
||||
@OneToOne(() => Blacklist, (blacklist) => blacklist.media)
|
||||
public blacklist: Promise<Blacklist>;
|
||||
@OneToOne(() => Blocklist, (blocklist) => blocklist.media)
|
||||
public blocklist: Promise<Blocklist>;
|
||||
|
||||
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
public createdAt: Date;
|
||||
|
||||
@@ -35,7 +35,7 @@ export class RequestPermissionError extends Error {}
|
||||
export class QuotaRestrictedError extends Error {}
|
||||
export class DuplicateMediaRequestError extends Error {}
|
||||
export class NoSeasonsAvailableError extends Error {}
|
||||
export class BlacklistedMediaError extends Error {}
|
||||
export class BlocklistedMediaError extends Error {}
|
||||
|
||||
type MediaRequestOptions = {
|
||||
isAutoRequest?: boolean;
|
||||
@@ -140,14 +140,14 @@ export class MediaRequest {
|
||||
mediaType: requestBody.mediaType,
|
||||
});
|
||||
} else {
|
||||
if (media.status === MediaStatus.BLACKLISTED) {
|
||||
logger.warn('Request for media blocked due to being blacklisted', {
|
||||
if (media.status === MediaStatus.BLOCKLISTED) {
|
||||
logger.warn('Request for media blocked due to being blocklisted', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
mediaType: requestBody.mediaType,
|
||||
label: 'Media Request',
|
||||
});
|
||||
|
||||
throw new BlacklistedMediaError('This media is blacklisted.');
|
||||
throw new BlocklistedMediaError('This media is blocklisted.');
|
||||
}
|
||||
|
||||
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { User } from '@server/entity/User';
|
||||
import type { PaginatedResponse } from '@server/interfaces/api/common';
|
||||
|
||||
export interface BlacklistItem {
|
||||
export interface BlocklistItem {
|
||||
tmdbId: number;
|
||||
mediaType: 'movie' | 'tv';
|
||||
title?: string;
|
||||
createdAt?: Date;
|
||||
user?: User;
|
||||
blacklistedTags?: string;
|
||||
blocklistedTags?: string;
|
||||
}
|
||||
|
||||
export interface BlacklistResultsResponse extends PaginatedResponse {
|
||||
results: BlacklistItem[];
|
||||
export interface BlocklistResultsResponse extends PaginatedResponse {
|
||||
results: BlocklistItem[];
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export interface PublicSettingsResponse {
|
||||
applicationTitle: string;
|
||||
applicationUrl: string;
|
||||
hideAvailable: boolean;
|
||||
hideBlacklisted: boolean;
|
||||
hideBlocklisted: boolean;
|
||||
localLogin: boolean;
|
||||
mediaServerLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import dataSource from '@server/datasource';
|
||||
import { Blacklist } from '@server/entity/Blacklist';
|
||||
import { Blocklist } from '@server/entity/Blocklist';
|
||||
import Media from '@server/entity/Media';
|
||||
import type {
|
||||
RunnableScanner,
|
||||
@@ -20,7 +20,7 @@ import type { EntityManager } from 'typeorm';
|
||||
const TMDB_API_DELAY_MS = 250;
|
||||
class AbortTransaction extends Error {}
|
||||
|
||||
class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
class BlocklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
private running = false;
|
||||
private progress = 0;
|
||||
private total = 0;
|
||||
@@ -30,12 +30,12 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
|
||||
try {
|
||||
await dataSource.transaction(async (em) => {
|
||||
await this.cleanBlacklist(em);
|
||||
await this.createBlacklistEntries(em);
|
||||
await this.cleanBlocklist(em);
|
||||
await this.createBlocklistEntries(em);
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof AbortTransaction) {
|
||||
logger.info('Aborting job: Process Blacklisted Tags', {
|
||||
logger.info('Aborting job: Process Blocklisted Tags', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
} else {
|
||||
@@ -64,37 +64,37 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
private async createBlacklistEntries(em: EntityManager) {
|
||||
private async createBlocklistEntries(em: EntityManager) {
|
||||
const tmdb = createTmdbWithRegionLanguage();
|
||||
|
||||
const settings = getSettings();
|
||||
const blacklistedTags = settings.main.blacklistedTags;
|
||||
const blacklistedTagsArr = blacklistedTags.split(',');
|
||||
const blocklistedTags = settings.main.blocklistedTags;
|
||||
const blocklistedTagsArr = blocklistedTags.split(',');
|
||||
|
||||
const pageLimit = settings.main.blacklistedTagsLimit;
|
||||
const pageLimit = settings.main.blocklistedTagsLimit;
|
||||
const invalidKeywords = new Set<string>();
|
||||
|
||||
if (blacklistedTags.length === 0) {
|
||||
if (blocklistedTags.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The maximum number of queries we're expected to execute
|
||||
this.total =
|
||||
2 * blacklistedTagsArr.length * pageLimit * SortOptionsIterable.length;
|
||||
2 * blocklistedTagsArr.length * pageLimit * SortOptionsIterable.length;
|
||||
|
||||
for (const type of [MediaType.MOVIE, MediaType.TV]) {
|
||||
const getDiscover =
|
||||
type === MediaType.MOVIE ? tmdb.getDiscoverMovies : tmdb.getDiscoverTv;
|
||||
|
||||
// Iterate for each tag
|
||||
for (const tag of blacklistedTagsArr) {
|
||||
for (const tag of blocklistedTagsArr) {
|
||||
const keywordDetails = await tmdb.getKeywordDetails({
|
||||
keywordId: Number(tag),
|
||||
});
|
||||
|
||||
if (keywordDetails === null) {
|
||||
logger.warn('Skipping invalid keyword in blacklisted tags', {
|
||||
label: 'Blacklisted Tags Processor',
|
||||
logger.warn('Skipping invalid keyword in blocklisted tags', {
|
||||
label: 'Blocklisted Tags Processor',
|
||||
keywordId: tag,
|
||||
});
|
||||
invalidKeywords.add(tag);
|
||||
@@ -134,8 +134,8 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
queryMax = response.total_pages;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing keyword in blacklisted tags', {
|
||||
label: 'Blacklisted Tags Processor',
|
||||
logger.error('Error processing keyword in blocklisted tags', {
|
||||
label: 'Blocklisted Tags Processor',
|
||||
keywordId: tag,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
@@ -145,19 +145,19 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
}
|
||||
|
||||
if (invalidKeywords.size > 0) {
|
||||
const currentTags = blacklistedTagsArr.filter(
|
||||
const currentTags = blocklistedTagsArr.filter(
|
||||
(tag) => !invalidKeywords.has(tag)
|
||||
);
|
||||
const cleanedTags = currentTags.join(',');
|
||||
|
||||
if (cleanedTags !== blacklistedTags) {
|
||||
settings.main.blacklistedTags = cleanedTags;
|
||||
if (cleanedTags !== blocklistedTags) {
|
||||
settings.main.blocklistedTags = cleanedTags;
|
||||
await settings.save();
|
||||
|
||||
logger.info('Cleaned up invalid keywords from settings', {
|
||||
label: 'Blacklisted Tags Processor',
|
||||
label: 'Blocklisted Tags Processor',
|
||||
removedKeywords: Array.from(invalidKeywords),
|
||||
newBlacklistedTags: cleanedTags,
|
||||
newBlocklistedTags: cleanedTags,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -169,33 +169,33 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
mediaType: MediaType,
|
||||
em: EntityManager
|
||||
) {
|
||||
const blacklistRepository = em.getRepository(Blacklist);
|
||||
const blocklistRepository = em.getRepository(Blocklist);
|
||||
|
||||
for (const entry of response.results) {
|
||||
const blacklistEntry = await blacklistRepository.findOne({
|
||||
const blocklistEntry = await blocklistRepository.findOne({
|
||||
where: { tmdbId: entry.id },
|
||||
});
|
||||
|
||||
if (blacklistEntry) {
|
||||
// Don't mark manual blacklists with tags
|
||||
// If media wasn't previously blacklisted for this tag, add the tag to the media's blacklist
|
||||
if (blocklistEntry) {
|
||||
// Don't mark manual blocklists with tags
|
||||
// If media wasn't previously blocklisted for this tag, add the tag to the media's blocklist
|
||||
if (
|
||||
blacklistEntry.blacklistedTags &&
|
||||
!blacklistEntry.blacklistedTags.includes(`,${keywordId},`)
|
||||
blocklistEntry.blocklistedTags &&
|
||||
!blocklistEntry.blocklistedTags.includes(`,${keywordId},`)
|
||||
) {
|
||||
await blacklistRepository.update(blacklistEntry.id, {
|
||||
blacklistedTags: `${blacklistEntry.blacklistedTags}${keywordId},`,
|
||||
await blocklistRepository.update(blocklistEntry.id, {
|
||||
blocklistedTags: `${blocklistEntry.blocklistedTags}${keywordId},`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Media wasn't previously blacklisted, add it to the blacklist
|
||||
await Blacklist.addToBlacklist(
|
||||
// Media wasn't previously blocklisted, add it to the blocklist
|
||||
await Blocklist.addToBlocklist(
|
||||
{
|
||||
blacklistRequest: {
|
||||
blocklistRequest: {
|
||||
mediaType,
|
||||
title: 'title' in entry ? entry.title : entry.name,
|
||||
tmdbId: entry.id,
|
||||
blacklistedTags: `,${keywordId},`,
|
||||
blocklistedTags: `,${keywordId},`,
|
||||
},
|
||||
},
|
||||
em
|
||||
@@ -204,22 +204,22 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanBlacklist(em: EntityManager) {
|
||||
// Remove blacklist and media entries blacklisted by tags
|
||||
private async cleanBlocklist(em: EntityManager) {
|
||||
// Remove blocklist and media entries blocklisted by tags
|
||||
const mediaRepository = em.getRepository(Media);
|
||||
const mediaToRemove = await mediaRepository
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect(Blacklist, 'blist', 'blist.tmdbId = media.tmdbId')
|
||||
.where(`blist.blacklistedTags IS NOT NULL`)
|
||||
.innerJoinAndSelect(Blocklist, 'blist', 'blist.tmdbId = media.tmdbId')
|
||||
.where(`blist.blocklistedTags IS NOT NULL`)
|
||||
.getMany();
|
||||
|
||||
// Batch removes so the query doesn't get too large
|
||||
for (let i = 0; i < mediaToRemove.length; i += 500) {
|
||||
await mediaRepository.remove(mediaToRemove.slice(i, i + 500)); // This also deletes the blacklist entries via cascading
|
||||
await mediaRepository.remove(mediaToRemove.slice(i, i + 500)); // This also deletes the blocklist entries via cascading
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blacklistedTagsProcessor = new BlacklistedTagProcessor();
|
||||
const blocklistedTagsProcessor = new BlocklistedTagProcessor();
|
||||
|
||||
export default blacklistedTagsProcessor;
|
||||
export default blocklistedTagsProcessor;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import blacklistedTagsProcessor from '@server/job/blacklistedTagsProcessor';
|
||||
import blocklistedTagsProcessor from '@server/job/blocklistedTagsProcessor';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
@@ -239,19 +239,19 @@ export const startJobs = (): void => {
|
||||
});
|
||||
|
||||
scheduledJobs.push({
|
||||
id: 'process-blacklisted-tags',
|
||||
name: 'Process Blacklisted Tags',
|
||||
id: 'process-blocklisted-tags',
|
||||
name: 'Process Blocklisted Tags',
|
||||
type: 'process',
|
||||
interval: 'days',
|
||||
cronSchedule: jobs['process-blacklisted-tags'].schedule,
|
||||
job: schedule.scheduleJob(jobs['process-blacklisted-tags'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Process Blacklisted Tags', {
|
||||
cronSchedule: jobs['process-blocklisted-tags'].schedule,
|
||||
job: schedule.scheduleJob(jobs['process-blocklisted-tags'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Process Blocklisted Tags', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
blacklistedTagsProcessor.run();
|
||||
blocklistedTagsProcessor.run();
|
||||
}),
|
||||
running: () => blacklistedTagsProcessor.status().running,
|
||||
cancelFn: () => blacklistedTagsProcessor.cancel(),
|
||||
running: () => blocklistedTagsProcessor.status().running,
|
||||
cancelFn: () => blocklistedTagsProcessor.cancel(),
|
||||
});
|
||||
|
||||
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
|
||||
|
||||
@@ -27,8 +27,8 @@ export enum Permission {
|
||||
AUTO_REQUEST_TV = 33554432,
|
||||
RECENT_VIEW = 67108864,
|
||||
WATCHLIST_VIEW = 134217728,
|
||||
MANAGE_BLACKLIST = 268435456,
|
||||
VIEW_BLACKLIST = 1073741824,
|
||||
MANAGE_BLOCKLIST = 268435456,
|
||||
VIEW_BLOCKLIST = 1073741824,
|
||||
}
|
||||
|
||||
export interface PermissionCheckOptions {
|
||||
|
||||
@@ -132,15 +132,15 @@ export interface MainSettings {
|
||||
tv: Quota;
|
||||
};
|
||||
hideAvailable: boolean;
|
||||
hideBlacklisted: boolean;
|
||||
hideBlocklisted: boolean;
|
||||
localLogin: boolean;
|
||||
mediaServerLogin: boolean;
|
||||
newPlexLogin: boolean;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
originalLanguage: string;
|
||||
blacklistedTags: string;
|
||||
blacklistedTagsLimit: number;
|
||||
blocklistedTags: string;
|
||||
blocklistedTagsLimit: number;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
@@ -181,7 +181,7 @@ interface FullPublicSettings extends PublicSettings {
|
||||
applicationTitle: string;
|
||||
applicationUrl: string;
|
||||
hideAvailable: boolean;
|
||||
hideBlacklisted: boolean;
|
||||
hideBlocklisted: boolean;
|
||||
localLogin: boolean;
|
||||
mediaServerLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
@@ -346,7 +346,7 @@ export type JobId =
|
||||
| 'jellyfin-full-scan'
|
||||
| 'image-cache-cleanup'
|
||||
| 'availability-sync'
|
||||
| 'process-blacklisted-tags';
|
||||
| 'process-blocklisted-tags';
|
||||
|
||||
export interface AllSettings {
|
||||
clientId: string;
|
||||
@@ -389,15 +389,15 @@ class Settings {
|
||||
tv: {},
|
||||
},
|
||||
hideAvailable: false,
|
||||
hideBlacklisted: false,
|
||||
hideBlocklisted: false,
|
||||
localLogin: true,
|
||||
mediaServerLogin: true,
|
||||
newPlexLogin: true,
|
||||
discoverRegion: '',
|
||||
streamingRegion: '',
|
||||
originalLanguage: '',
|
||||
blacklistedTags: '',
|
||||
blacklistedTagsLimit: 50,
|
||||
blocklistedTags: '',
|
||||
blocklistedTagsLimit: 50,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
@@ -570,7 +570,7 @@ class Settings {
|
||||
'image-cache-cleanup': {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
'process-blacklisted-tags': {
|
||||
'process-blocklisted-tags': {
|
||||
schedule: '0 30 1 */7 * *',
|
||||
},
|
||||
},
|
||||
@@ -671,7 +671,7 @@ class Settings {
|
||||
applicationTitle: this.data.main.applicationTitle,
|
||||
applicationUrl: this.data.main.applicationUrl,
|
||||
hideAvailable: this.data.main.hideAvailable,
|
||||
hideBlacklisted: this.data.main.hideBlacklisted,
|
||||
hideBlocklisted: this.data.main.hideBlocklisted,
|
||||
localLogin: this.data.main.localLogin,
|
||||
mediaServerLogin: this.data.main.mediaServerLogin,
|
||||
jellyfinExternalHost: this.data.jellyfin.externalHostname,
|
||||
|
||||
@@ -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;
|
||||
@@ -3,7 +3,7 @@ import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import {
|
||||
BlacklistedMediaError,
|
||||
BlocklistedMediaError,
|
||||
DuplicateMediaRequestError,
|
||||
MediaRequest,
|
||||
NoSeasonsAvailableError,
|
||||
@@ -145,8 +145,8 @@ class WatchlistSync {
|
||||
errorMessage: e.message,
|
||||
});
|
||||
break;
|
||||
// Blacklisted media should be silently ignored during watchlist sync to avoid spam
|
||||
case BlacklistedMediaError:
|
||||
// Blocklisted media should be silently ignored during watchlist sync to avoid spam
|
||||
case BlocklistedMediaError:
|
||||
break;
|
||||
default:
|
||||
logger.error('Failed to create media request from watchlist', {
|
||||
|
||||
49
server/middleware/deprecation.ts
Normal file
49
server/middleware/deprecation.ts
Normal 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;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface {
|
||||
name = 'RenameBlacklistToBlocklist1746900000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "blacklist" RENAME TO "blocklist"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blocklist" RENAME COLUMN "blacklistedTags" TO "blocklistedTags"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blocklist" RENAME COLUMN "blocklistedTags" TO "blacklistedTags"`
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "blocklist" RENAME TO "blacklist"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameBlacklistToBlocklist1746900000000 implements MigrationInterface {
|
||||
name = 'RenameBlacklistToBlocklist1746900000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "temporary_blocklist" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"mediaType" varchar NOT NULL,
|
||||
"title" varchar,
|
||||
"tmdbId" integer NOT NULL,
|
||||
"blocklistedTags" varchar,
|
||||
"createdAt" datetime NOT NULL DEFAULT (datetime('now')),
|
||||
"userId" integer,
|
||||
"mediaId" integer,
|
||||
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"),
|
||||
CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"),
|
||||
CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "temporary_blocklist" ("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId")
|
||||
SELECT "id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId" FROM "blacklist"
|
||||
`);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_blocklist" RENAME TO "blocklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blocklist" ("tmdbId")`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "blocklist" RENAME TO "blacklist"`);
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "temporary_blacklist" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"mediaType" varchar NOT NULL,
|
||||
"title" varchar,
|
||||
"tmdbId" integer NOT NULL,
|
||||
"blacklistedTags" varchar,
|
||||
"createdAt" datetime NOT NULL DEFAULT (datetime('now')),
|
||||
"userId" integer,
|
||||
"mediaId" integer,
|
||||
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"),
|
||||
CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"),
|
||||
CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
|
||||
CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "temporary_blacklist" ("id", "mediaType", "title", "tmdbId", "blacklistedTags", "createdAt", "userId", "mediaId")
|
||||
SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "blacklist"
|
||||
`);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId")`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { Blacklist } from '@server/entity/Blacklist';
|
||||
import { Blocklist } from '@server/entity/Blocklist';
|
||||
import Media from '@server/entity/Media';
|
||||
import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces';
|
||||
import type { BlocklistResultsResponse } from '@server/interfaces/api/blocklistInterfaces';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
@@ -10,53 +10,53 @@ import { Router } from 'express';
|
||||
import { EntityNotFoundError, QueryFailedError } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
const blacklistRoutes = Router();
|
||||
const blocklistRoutes = Router();
|
||||
|
||||
export const blacklistAdd = z.object({
|
||||
export const blocklistAdd = z.object({
|
||||
tmdbId: z.coerce.number(),
|
||||
mediaType: z.nativeEnum(MediaType),
|
||||
title: z.coerce.string().optional(),
|
||||
user: z.coerce.number(),
|
||||
});
|
||||
|
||||
const blacklistGet = z.object({
|
||||
const blocklistGet = z.object({
|
||||
take: z.coerce.number().int().positive().default(25),
|
||||
skip: z.coerce.number().int().nonnegative().default(0),
|
||||
search: z.string().optional(),
|
||||
filter: z.enum(['all', 'manual', 'blacklistedTags']).optional(),
|
||||
filter: z.enum(['all', 'manual', 'blocklistedTags']).optional(),
|
||||
});
|
||||
|
||||
blacklistRoutes.get(
|
||||
blocklistRoutes.get(
|
||||
'/',
|
||||
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
|
||||
isAuthenticated([Permission.MANAGE_BLOCKLIST, Permission.VIEW_BLOCKLIST], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
const { take, skip, search, filter } = blacklistGet.parse(req.query);
|
||||
const { take, skip, search, filter } = blocklistGet.parse(req.query);
|
||||
|
||||
try {
|
||||
let query = getRepository(Blacklist)
|
||||
.createQueryBuilder('blacklist')
|
||||
.leftJoinAndSelect('blacklist.user', 'user')
|
||||
let query = getRepository(Blocklist)
|
||||
.createQueryBuilder('blocklist')
|
||||
.leftJoinAndSelect('blocklist.user', 'user')
|
||||
.where('1 = 1'); // Allow use of andWhere later
|
||||
|
||||
switch (filter) {
|
||||
case 'manual':
|
||||
query = query.andWhere('blacklist.blacklistedTags IS NULL');
|
||||
query = query.andWhere('blocklist.blocklistedTags IS NULL');
|
||||
break;
|
||||
case 'blacklistedTags':
|
||||
query = query.andWhere('blacklist.blacklistedTags IS NOT NULL');
|
||||
case 'blocklistedTags':
|
||||
query = query.andWhere('blocklist.blocklistedTags IS NOT NULL');
|
||||
break;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query = query.andWhere('blacklist.title like :title', {
|
||||
query = query.andWhere('blocklist.title like :title', {
|
||||
title: `%${search}%`,
|
||||
});
|
||||
}
|
||||
|
||||
const [blacklistedItems, itemsCount] = await query
|
||||
.orderBy('blacklist.createdAt', 'DESC')
|
||||
const [blocklistedItems, itemsCount] = await query
|
||||
.orderBy('blocklist.createdAt', 'DESC')
|
||||
.take(take)
|
||||
.skip(skip)
|
||||
.getManyAndCount();
|
||||
@@ -68,35 +68,35 @@ blacklistRoutes.get(
|
||||
results: itemsCount,
|
||||
page: Math.ceil(skip / take) + 1,
|
||||
},
|
||||
results: blacklistedItems,
|
||||
} as BlacklistResultsResponse);
|
||||
results: blocklistedItems,
|
||||
} as BlocklistResultsResponse);
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong while retrieving blacklisted items', {
|
||||
label: 'Blacklist',
|
||||
logger.error('Something went wrong while retrieving blocklisted items', {
|
||||
label: 'Blocklist',
|
||||
errorMessage: error.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Unable to retrieve blacklisted items.',
|
||||
message: 'Unable to retrieve blocklisted items.',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
blacklistRoutes.get(
|
||||
blocklistRoutes.get(
|
||||
'/:id',
|
||||
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
||||
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const blacklisteRepository = getRepository(Blacklist);
|
||||
const blocklisteRepository = getRepository(Blocklist);
|
||||
|
||||
const blacklistItem = await blacklisteRepository.findOneOrFail({
|
||||
const blocklistItem = await blocklisteRepository.findOneOrFail({
|
||||
where: { tmdbId: Number(req.params.id) },
|
||||
});
|
||||
|
||||
return res.status(200).send(blacklistItem);
|
||||
return res.status(200).send(blocklistItem);
|
||||
} catch (e) {
|
||||
if (e instanceof EntityNotFoundError) {
|
||||
return next({
|
||||
@@ -109,17 +109,17 @@ blacklistRoutes.get(
|
||||
}
|
||||
);
|
||||
|
||||
blacklistRoutes.post(
|
||||
blocklistRoutes.post(
|
||||
'/',
|
||||
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
||||
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const values = blacklistAdd.parse(req.body);
|
||||
const values = blocklistAdd.parse(req.body);
|
||||
|
||||
await Blacklist.addToBlacklist({
|
||||
blacklistRequest: values,
|
||||
await Blocklist.addToBlocklist({
|
||||
blocklistRequest: values,
|
||||
});
|
||||
|
||||
return res.status(201).send();
|
||||
@@ -131,12 +131,12 @@ blacklistRoutes.post(
|
||||
if (error instanceof QueryFailedError) {
|
||||
switch (error.driverError.errno) {
|
||||
case 19:
|
||||
return next({ status: 412, message: 'Item already blacklisted' });
|
||||
return next({ status: 412, message: 'Item already blocklisted' });
|
||||
default:
|
||||
logger.warn('Something wrong with data blacklist', {
|
||||
logger.warn('Something wrong with data blocklist', {
|
||||
tmdbId: req.body.tmdbId,
|
||||
mediaType: req.body.mediaType,
|
||||
label: 'Blacklist',
|
||||
label: 'Blocklist',
|
||||
});
|
||||
return next({ status: 409, message: 'Something wrong' });
|
||||
}
|
||||
@@ -147,20 +147,20 @@ blacklistRoutes.post(
|
||||
}
|
||||
);
|
||||
|
||||
blacklistRoutes.delete(
|
||||
blocklistRoutes.delete(
|
||||
'/:id',
|
||||
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
||||
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
|
||||
type: 'or',
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const blacklisteRepository = getRepository(Blacklist);
|
||||
const blocklisteRepository = getRepository(Blocklist);
|
||||
|
||||
const blacklistItem = await blacklisteRepository.findOneOrFail({
|
||||
const blocklistItem = await blocklisteRepository.findOneOrFail({
|
||||
where: { tmdbId: Number(req.params.id) },
|
||||
});
|
||||
|
||||
await blacklisteRepository.remove(blacklistItem);
|
||||
await blocklisteRepository.remove(blocklistItem);
|
||||
|
||||
const mediaRepository = getRepository(Media);
|
||||
|
||||
@@ -183,4 +183,4 @@ blacklistRoutes.delete(
|
||||
}
|
||||
);
|
||||
|
||||
export default blacklistRoutes;
|
||||
export default blocklistRoutes;
|
||||
@@ -12,6 +12,7 @@ import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { checkUser, isAuthenticated } from '@server/middleware/auth';
|
||||
import deprecatedRoute from '@server/middleware/deprecation';
|
||||
import { mapProductionCompany } from '@server/models/Movie';
|
||||
import { mapNetwork } from '@server/models/Tv';
|
||||
import { mapWatchProviderDetails } from '@server/models/common';
|
||||
@@ -28,7 +29,7 @@ import restartFlag from '@server/utils/restartFlag';
|
||||
import { isPerson } from '@server/utils/typeHelpers';
|
||||
import { Router } from 'express';
|
||||
import authRoutes from './auth';
|
||||
import blacklistRoutes from './blacklist';
|
||||
import blocklistRoutes from './blocklist';
|
||||
import collectionRoutes from './collection';
|
||||
import discoverRoutes, { createTmdbWithRegionLanguage } from './discover';
|
||||
import issueRoutes from './issue';
|
||||
@@ -151,7 +152,17 @@ router.use('/search', isAuthenticated(), searchRoutes);
|
||||
router.use('/discover', isAuthenticated(), discoverRoutes);
|
||||
router.use('/request', isAuthenticated(), requestRoutes);
|
||||
router.use('/watchlist', isAuthenticated(), watchlistRoutes);
|
||||
router.use('/blacklist', isAuthenticated(), blacklistRoutes);
|
||||
router.use('/blocklist', isAuthenticated(), blocklistRoutes);
|
||||
router.use(
|
||||
'/blacklist',
|
||||
isAuthenticated(),
|
||||
deprecatedRoute({
|
||||
oldPath: '/api/v1/blacklist',
|
||||
newPath: '/api/v1/blocklist',
|
||||
sunsetDate: '2026-06-01',
|
||||
}),
|
||||
blocklistRoutes
|
||||
);
|
||||
router.use('/movie', isAuthenticated(), movieRoutes);
|
||||
router.use('/tv', isAuthenticated(), tvRoutes);
|
||||
router.use('/media', isAuthenticated(), mediaRoutes);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import {
|
||||
BlacklistedMediaError,
|
||||
BlocklistedMediaError,
|
||||
DuplicateMediaRequestError,
|
||||
MediaRequest,
|
||||
NoSeasonsAvailableError,
|
||||
@@ -326,7 +326,7 @@ requestRoutes.post<never, MediaRequest, MediaRequestBody>(
|
||||
return next({ status: 409, message: error.message });
|
||||
case NoSeasonsAvailableError:
|
||||
return next({ status: 202, message: error.message });
|
||||
case BlacklistedMediaError:
|
||||
case BlocklistedMediaError:
|
||||
return next({ status: 403, message: error.message });
|
||||
default:
|
||||
return next({ status: 500, message: error.message });
|
||||
|
||||
Reference in New Issue
Block a user