mirror of
https://github.com/seerr-team/seerr.git
synced 2025-12-23 23:58:07 -05:00
* feat(blacklist): add blacktag settings to main settings page * feat(blacklist): create blacktag logic and infrastructure * feat(blacklist): add scheduling for blacktags job * feat(blacklist): create blacktag ui badge for blacklist * docs(blacklist): document blacktags in using-jellyseerr * fix(blacklist): batch blacklist and media db removes to avoid expression tree too large error * feat(blacklist): allow easy import and export of blacktag configuration * fix(settings): don't copy the API key every time you press enter on the main settings * fix(blacklist): move filter inline with page title to match all the other pages * feat(blacklist): allow filtering between manually blacklisted and automatically blacklisted entries * docs(blacklist): reword blacktag documentation a little * refactor(blacklist): remove blacktag settings from public settings interfaces There's no reason for it to be there * refactor(blacklist): remove unused variable from processResults in blacktagsProcessor * refactor(blacklist): change all instances of blacktag to blacklistedTag and update doc to match * docs(blacklist): update general documentation for blacklisted tag settings * fix(blacklist): update setting use of "blacklisted tag" to match between modals * perf(blacklist): remove media type constraint from existing blacklist entry query Doesn't make sense to keep it because tmdbid has a unique constraint on it * fix(blacklist): remove whitespace line causing prettier to fail in CI * refactor(blacklist): swap out some != and == for !s and _s * fix(blacklist): merge back CopyButton changes, disable button when there's nothing to copy * refactor(blacklist): use axios instead of fetch for blacklisted tag queries * style(blacklist): use templated axios types and remove redundant try-catches
187 lines
5.0 KiB
TypeScript
187 lines
5.0 KiB
TypeScript
import { MediaType } from '@server/constants/media';
|
|
import { getRepository } from '@server/datasource';
|
|
import { Blacklist } from '@server/entity/Blacklist';
|
|
import Media from '@server/entity/Media';
|
|
import type { BlacklistResultsResponse } from '@server/interfaces/api/blacklistInterfaces';
|
|
import { Permission } from '@server/lib/permissions';
|
|
import logger from '@server/logger';
|
|
import { isAuthenticated } from '@server/middleware/auth';
|
|
import { Router } from 'express';
|
|
import { EntityNotFoundError, QueryFailedError } from 'typeorm';
|
|
import { z } from 'zod';
|
|
|
|
const blacklistRoutes = Router();
|
|
|
|
export const blacklistAdd = z.object({
|
|
tmdbId: z.coerce.number(),
|
|
mediaType: z.nativeEnum(MediaType),
|
|
title: z.coerce.string().optional(),
|
|
user: z.coerce.number(),
|
|
});
|
|
|
|
const blacklistGet = 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(),
|
|
});
|
|
|
|
blacklistRoutes.get(
|
|
'/',
|
|
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
|
|
type: 'or',
|
|
}),
|
|
async (req, res, next) => {
|
|
const { take, skip, search, filter } = blacklistGet.parse(req.query);
|
|
|
|
try {
|
|
let query = getRepository(Blacklist)
|
|
.createQueryBuilder('blacklist')
|
|
.leftJoinAndSelect('blacklist.user', 'user')
|
|
.where('1 = 1'); // Allow use of andWhere later
|
|
|
|
switch (filter) {
|
|
case 'manual':
|
|
query = query.andWhere('blacklist.blacklistedTags IS NULL');
|
|
break;
|
|
case 'blacklistedTags':
|
|
query = query.andWhere('blacklist.blacklistedTags IS NOT NULL');
|
|
break;
|
|
}
|
|
|
|
if (search) {
|
|
query = query.andWhere('blacklist.title like :title', {
|
|
title: `%${search}%`,
|
|
});
|
|
}
|
|
|
|
const [blacklistedItems, itemsCount] = await query
|
|
.orderBy('blacklist.createdAt', 'DESC')
|
|
.take(take)
|
|
.skip(skip)
|
|
.getManyAndCount();
|
|
|
|
return res.status(200).json({
|
|
pageInfo: {
|
|
pages: Math.ceil(itemsCount / take),
|
|
pageSize: take,
|
|
results: itemsCount,
|
|
page: Math.ceil(skip / take) + 1,
|
|
},
|
|
results: blacklistedItems,
|
|
} as BlacklistResultsResponse);
|
|
} catch (error) {
|
|
logger.error('Something went wrong while retrieving blacklisted items', {
|
|
label: 'Blacklist',
|
|
errorMessage: error.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve blacklisted items.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
blacklistRoutes.get(
|
|
'/:id',
|
|
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
|
type: 'or',
|
|
}),
|
|
async (req, res, next) => {
|
|
try {
|
|
const blacklisteRepository = getRepository(Blacklist);
|
|
|
|
const blacklistItem = await blacklisteRepository.findOneOrFail({
|
|
where: { tmdbId: Number(req.params.id) },
|
|
});
|
|
|
|
return res.status(200).send(blacklistItem);
|
|
} catch (e) {
|
|
if (e instanceof EntityNotFoundError) {
|
|
return next({
|
|
status: 401,
|
|
message: e.message,
|
|
});
|
|
}
|
|
return next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
blacklistRoutes.post(
|
|
'/',
|
|
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
|
type: 'or',
|
|
}),
|
|
async (req, res, next) => {
|
|
try {
|
|
const values = blacklistAdd.parse(req.body);
|
|
|
|
await Blacklist.addToBlacklist({
|
|
blacklistRequest: values,
|
|
});
|
|
|
|
return res.status(201).send();
|
|
} catch (error) {
|
|
if (!(error instanceof Error)) {
|
|
return;
|
|
}
|
|
|
|
if (error instanceof QueryFailedError) {
|
|
switch (error.driverError.errno) {
|
|
case 19:
|
|
return next({ status: 412, message: 'Item already blacklisted' });
|
|
default:
|
|
logger.warn('Something wrong with data blacklist', {
|
|
tmdbId: req.body.tmdbId,
|
|
mediaType: req.body.mediaType,
|
|
label: 'Blacklist',
|
|
});
|
|
return next({ status: 409, message: 'Something wrong' });
|
|
}
|
|
}
|
|
|
|
return next({ status: 500, message: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
blacklistRoutes.delete(
|
|
'/:id',
|
|
isAuthenticated([Permission.MANAGE_BLACKLIST], {
|
|
type: 'or',
|
|
}),
|
|
async (req, res, next) => {
|
|
try {
|
|
const blacklisteRepository = getRepository(Blacklist);
|
|
|
|
const blacklistItem = await blacklisteRepository.findOneOrFail({
|
|
where: { tmdbId: Number(req.params.id) },
|
|
});
|
|
|
|
await blacklisteRepository.remove(blacklistItem);
|
|
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const mediaItem = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: Number(req.params.id) },
|
|
});
|
|
|
|
await mediaRepository.remove(mediaItem);
|
|
|
|
return res.status(204).send();
|
|
} catch (e) {
|
|
if (e instanceof EntityNotFoundError) {
|
|
return next({
|
|
status: 401,
|
|
message: e.message,
|
|
});
|
|
}
|
|
return next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
export default blacklistRoutes;
|