Skip to content

Commit

Permalink
fix(blacklist): request data only when modal is shown, remove useless…
Browse files Browse the repository at this point in the history
… ratelimit and lazy load blacklist (#1084)

* perf: remove eager load of Blacklist entity from Media entity

Try to resolve some performance issues by removing the eager loading of Blacklist items from the
Media entity

* fix: fix ManageSlideOver for blacklist

* perf(blacklist): request data only when modal is shown

For admin users, the button to blacklist a media (used on every media card) was displaying a Modal,
that was requesting data BEFORE the modal was displayed. This resulted in dozens of additional
requests everytime media cards were displayed.

* perf(blacklist): remove useless ratelimit
  • Loading branch information
gauthier-th authored Nov 12, 2024
1 parent a2d2fd3 commit 694913c
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 22 deletions.
15 changes: 15 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4142,6 +4142,21 @@ paths:
'412':
description: Item has already been blacklisted
/blacklist/{tmdbId}:
get:
summary: Get media from blacklist
tags:
- blacklist
parameters:
- in: path
name: tmdbId
description: tmdbId ID
required: true
example: '1'
schema:
type: string
responses:
'200':
description: Blacklist details in JSON
delete:
summary: Remove media from blacklist
tags:
Expand Down
4 changes: 2 additions & 2 deletions server/entity/Blacklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ export class Blacklist implements BlacklistItem {
status: MediaStatus.BLACKLISTED,
status4k: MediaStatus.BLACKLISTED,
mediaType: blacklistRequest.mediaType,
blacklist: blacklist,
blacklist: Promise.resolve(blacklist),
});

await mediaRepository.save(media);
} else {
media.blacklist = blacklist;
media.blacklist = Promise.resolve(blacklist);
media.status = MediaStatus.BLACKLISTED;
media.status4k = MediaStatus.BLACKLISTED;

Expand Down
6 changes: 2 additions & 4 deletions server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,8 @@ class Media {
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
public issues: Issue[];

@OneToOne(() => Blacklist, (blacklist) => blacklist.media, {
eager: true,
})
public blacklist: Blacklist;
@OneToOne(() => Blacklist, (blacklist) => blacklist.media)
public blacklist: Promise<Blacklist>;

@CreateDateColumn()
public createdAt: Date;
Expand Down
33 changes: 28 additions & 5 deletions server/routes/blacklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ import { MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import { Blacklist } from '@server/entity/Blacklist';
import Media from '@server/entity/Media';
import { NotFoundError } from '@server/entity/Watchlist';
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 rateLimit from 'express-rate-limit';
import { QueryFailedError } from 'typeorm';
import { EntityNotFoundError, QueryFailedError } from 'typeorm';
import { z } from 'zod';

const blacklistRoutes = Router();
Expand All @@ -26,7 +24,6 @@ blacklistRoutes.get(
isAuthenticated([Permission.MANAGE_BLACKLIST, Permission.VIEW_BLACKLIST], {
type: 'or',
}),
rateLimit({ windowMs: 60 * 1000, max: 50 }),
async (req, res, next) => {
const pageSize = req.query.take ? Number(req.query.take) : 25;
const skip = req.query.skip ? Number(req.query.skip) : 0;
Expand Down Expand Up @@ -71,6 +68,32 @@ blacklistRoutes.get(
}
);

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], {
Expand Down Expand Up @@ -134,7 +157,7 @@ blacklistRoutes.delete(

return res.status(204).send();
} catch (e) {
if (e instanceof NotFoundError) {
if (e instanceof EntityNotFoundError) {
return next({
status: 401,
message: e.message,
Expand Down
27 changes: 18 additions & 9 deletions src/components/BlacklistBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Tooltip from '@app/components/Common/Tooltip';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
Expand All @@ -10,27 +11,29 @@ import Link from 'next/link';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';

const messages = defineMessages('component.BlacklistBlock', {
blacklistedby: 'Blacklisted By',
blacklistdate: 'Blacklisted date',
});

interface BlacklistBlockProps {
blacklistItem: Blacklist;
tmdbId: number;
onUpdate?: () => void;
onDelete?: () => void;
}

const BlacklistBlock = ({
blacklistItem,
tmdbId,
onUpdate,
onDelete,
}: BlacklistBlockProps) => {
const { user } = useUser();
const intl = useIntl();
const [isUpdating, setIsUpdating] = useState(false);
const { addToast } = useToasts();
const { data } = useSWR<Blacklist>(`/api/v1/blacklist/${tmdbId}`);

const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);
Expand Down Expand Up @@ -62,6 +65,14 @@ const BlacklistBlock = ({
setIsUpdating(false);
};

if (!data) {
return (
<>
<LoadingSpinner />
</>
);
}

return (
<div className="px-4 py-3 text-gray-300">
<div className="flex items-center justify-between">
Expand All @@ -73,13 +84,13 @@ const BlacklistBlock = ({
<span className="w-40 truncate md:w-auto">
<Link
href={
blacklistItem.user.id === user?.id
data.user.id === user?.id
? '/profile'
: `/users/${blacklistItem.user.id}`
: `/users/${data.user.id}`
}
>
<span className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
{blacklistItem.user.displayName}
{data.user.displayName}
</span>
</Link>
</span>
Expand All @@ -91,9 +102,7 @@ const BlacklistBlock = ({
>
<Button
buttonType="danger"
onClick={() =>
removeFromBlacklist(blacklistItem.tmdbId, blacklistItem.title)
}
onClick={() => removeFromBlacklist(data.tmdbId, data.title)}
disabled={isUpdating}
>
<TrashIcon className="icon-sm" />
Expand All @@ -114,7 +123,7 @@ const BlacklistBlock = ({
<CalendarIcon className="mr-1.5 h-5 w-5 flex-shrink-0" />
</Tooltip>
<span>
{intl.formatDate(blacklistItem.createdAt, {
{intl.formatDate(data.createdAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
Expand Down
2 changes: 1 addition & 1 deletion src/components/BlacklistModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const BlacklistModal = ({
const intl = useIntl();

const { data, error } = useSWR<TvDetails | MovieDetails>(
`/api/v1/${type}/${tmdbId}`
show ? `/api/v1/${type}/${tmdbId}` : null
);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/ManageSlideOver/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ const ManageSlideOver = ({
</h3>
<div className="overflow-hidden rounded-md border border-gray-700 shadow">
<BlacklistBlock
blacklistItem={data.mediaInfo.blacklist}
tmdbId={data.mediaInfo.tmdbId}
onUpdate={() => revalidate()}
onDelete={() => onClose()}
/>
Expand Down

0 comments on commit 694913c

Please sign in to comment.