feat(blocklist): add support for collections (#1841)

This commit is contained in:
0xsysr3ll
2026-03-30 00:19:45 +02:00
committed by GitHub
parent 56b79ff38c
commit 993ae4c58e
8 changed files with 445 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus, MediaType } from '@server/constants/media';
import dataSource, { getRepository } from '@server/datasource';
import { Blocklist } from '@server/entity/Blocklist';
import Media from '@server/entity/Media';
import type { BlocklistResultsResponse } from '@server/interfaces/api/blocklistInterfaces';
@@ -7,7 +8,7 @@ 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 { EntityNotFoundError, In, QueryFailedError } from 'typeorm';
import { z } from 'zod';
const blocklistRoutes = Router();
@@ -17,6 +18,7 @@ export const blocklistAdd = z.object({
mediaType: z.nativeEnum(MediaType),
title: z.coerce.string().optional(),
user: z.coerce.number(),
blocklistedTags: z.string().optional(),
});
const blocklistGet = z.object({
@@ -158,6 +160,107 @@ blocklistRoutes.post(
}
);
blocklistRoutes.post(
'/collection/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.locale,
});
const uniqueParts = [
...new Map(collection.parts.map((p) => [p.id, p])).values(),
];
const partIds = uniqueParts.map((p) => p.id);
if (partIds.length === 0) {
return res.status(201).send();
}
await dataSource.transaction(async (em) => {
const blocklistRepository = em.getRepository(Blocklist);
const mediaRepository = em.getRepository(Media);
const [existingBlocklists, existingMedia] = await Promise.all([
blocklistRepository.find({
where: { tmdbId: In(partIds), mediaType: MediaType.MOVIE },
}),
mediaRepository.find({
where: { tmdbId: In(partIds), mediaType: MediaType.MOVIE },
}),
]);
const blocklistByTmdbId = new Map(
existingBlocklists.map((b) => [b.tmdbId, b])
);
const mediaByTmdbId = new Map(existingMedia.map((m) => [m.tmdbId, m]));
await Promise.all(
uniqueParts.map(async (part) => {
if (blocklistByTmdbId.has(part.id)) {
return;
}
let blocklist = new Blocklist({
tmdbId: part.id,
mediaType: MediaType.MOVIE,
title: part.title,
user: req.user,
});
try {
await blocklistRepository.save(blocklist);
} catch (error) {
if (
!(error instanceof QueryFailedError) ||
error.driverError.errno !== 19
) {
throw error;
}
const row = await blocklistRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (!row) {
throw error;
}
blocklist = row;
}
let media = mediaByTmdbId.get(part.id);
if (!media) {
media = new Media({
tmdbId: part.id,
status: MediaStatus.BLOCKLISTED,
status4k: MediaStatus.BLOCKLISTED,
mediaType: MediaType.MOVIE,
blocklist: Promise.resolve(blocklist),
});
} else {
media.status = MediaStatus.BLOCKLISTED;
media.status4k = MediaStatus.BLOCKLISTED;
media.blocklist = Promise.resolve(blocklist);
}
await mediaRepository.save(media);
})
);
});
return res.status(201).send();
} catch (e) {
logger.error('Error blocklisting collection', {
label: 'Blocklist',
errorMessage: e.message,
collectionId: req.params.id,
});
return next({ status: 500, message: e.message });
}
}
);
blocklistRoutes.delete(
'/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
@@ -208,4 +311,54 @@ blocklistRoutes.delete(
}
);
blocklistRoutes.delete(
'/collection/:id',
isAuthenticated([Permission.MANAGE_BLOCKLIST], {
type: 'or',
}),
async (req, res, next) => {
try {
const tmdb = new TheMovieDb();
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.locale,
});
await dataSource.transaction(async (em) => {
const blocklistRepository = em.getRepository(Blocklist);
const mediaRepository = em.getRepository(Media);
await Promise.all(
collection.parts.map(async (part) => {
const blocklistItem = await blocklistRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (blocklistItem) {
await blocklistRepository.remove(blocklistItem);
const mediaItem = await mediaRepository.findOne({
where: { tmdbId: part.id, mediaType: MediaType.MOVIE },
});
if (mediaItem) {
await mediaRepository.remove(mediaItem);
}
}
})
);
});
return res.status(204).send();
} catch (e) {
logger.error('Error unblocklisting collection', {
label: 'Blocklist',
errorMessage: e.message,
collectionId: req.params.id,
});
return next({ status: 500, message: e.message });
}
}
);
export default blocklistRoutes;