diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index ddd94202..4afaab5d 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -4156,6 +4156,12 @@ paths: type: string nullable: true example: dune + - in: query + name: filter + schema: + type: string + enum: [all, manual, blacktags] + default: manual responses: '200': description: Blacklisted items returned diff --git a/server/routes/blacklist.ts b/server/routes/blacklist.ts index bb2dafe8..9e31d528 100644 --- a/server/routes/blacklist.ts +++ b/server/routes/blacklist.ts @@ -19,39 +19,54 @@ export const blacklistAdd = z.object({ 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', 'blacktags']).optional(), +}); + blacklistRoutes.get( '/', isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], { type: 'or', }), async (req, res, next) => { - const pageSize = req.query.take ? Number(req.query.take) : 25; - const skip = req.query.skip ? Number(req.query.skip) : 0; - const search = (req.query.search as string) ?? ''; + const { take, skip, search, filter } = blacklistGet.parse(req.query); try { let query = getRepository(Blacklist) .createQueryBuilder('blacklist') - .leftJoinAndSelect('blacklist.user', 'user'); + .leftJoinAndSelect('blacklist.user', 'user') + .where('1 = 1'); // Allow use of andWhere later - if (search.length > 0) { - query = query.where('blacklist.title like :title', { + switch (filter) { + case 'manual': + query = query.andWhere('blacklist.blacktags IS NULL'); + break; + case 'blacktags': + query = query.andWhere('blacklist.blacktags 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(pageSize) + .take(take) .skip(skip) .getManyAndCount(); return res.status(200).json({ pageInfo: { - pages: Math.ceil(itemsCount / pageSize), - pageSize, + pages: Math.ceil(itemsCount / take), + pageSize: take, results: itemsCount, - page: Math.ceil(skip / pageSize) + 1, + page: Math.ceil(skip / take) + 1, }, results: blacklistedItems, } as BlacklistResultsResponse); diff --git a/src/components/Blacklist/index.tsx b/src/components/Blacklist/index.tsx index 18e3d9f5..8173583a 100644 --- a/src/components/Blacklist/index.tsx +++ b/src/components/Blacklist/index.tsx @@ -15,6 +15,7 @@ import defineMessages from '@app/utils/defineMessages'; import { ChevronLeftIcon, ChevronRightIcon, + FunnelIcon, MagnifyingGlassIcon, TrashIcon, } from '@heroicons/react/24/solid'; @@ -42,8 +43,17 @@ const messages = defineMessages('components.Blacklist', { blacklistdate: 'date', blacklistedby: '{date} by {user}', blacklistNotFoundError: '{title} is not blacklisted.', + filterManual: 'Manual', + filterBlacktags: 'Blacktags', + showAllBlacklisted: 'Show All Blacklisted Media', }); +enum Filter { + ALL = 'all', + MANUAL = 'manual', + BLACKTAGS = 'blacktags', +} + const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => { return (movie as MovieDetails).title !== undefined; }; @@ -52,6 +62,7 @@ const Blacklist = () => { const [currentPageSize, setCurrentPageSize] = useState(10); const [searchFilter, debouncedSearchFilter, setSearchFilter] = useDebouncedState(''); + const [currentFilter, setCurrentFilter] = useState(Filter.MANUAL); const router = useRouter(); const intl = useIntl(); @@ -64,9 +75,11 @@ const Blacklist = () => { error, mutate: revalidate, } = useSWR( - `/api/v1/blacklist/?take=${currentPageSize} - &skip=${pageIndex * currentPageSize} - ${debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''}`, + `/api/v1/blacklist/?take=${currentPageSize}&skip=${ + pageIndex * currentPageSize + }&filter=${currentFilter}${ + debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : '' + }`, { refreshInterval: 0, revalidateOnFocus: false, @@ -97,7 +110,38 @@ const Blacklist = () => { {intl.formatMessage(globalMessages.blacklist)} - + + + + + + { + setCurrentFilter(e.target.value as Filter); + router.push({ + pathname: router.pathname, + query: router.query.userId + ? { userId: router.query.userId } + : {}, + }); + }} + value={currentFilter} + className="rounded-r-only" + > + + {intl.formatMessage(globalMessages.all)} + + + {intl.formatMessage(messages.filterManual)} + + + {intl.formatMessage(messages.filterBlacktags)} + + + + @@ -119,6 +163,16 @@ const Blacklist = () => { {intl.formatMessage(globalMessages.noresults)} + {currentFilter !== Filter.ALL && ( + + setCurrentFilter(Filter.ALL)} + > + {intl.formatMessage(messages.showAllBlacklisted)} + + + )} ) : ( data.results.map((item: BlacklistItem) => {