Skip to content

Commit

Permalink
feat: blacklist items from Discover page (Fallenbagel#632)
Browse files Browse the repository at this point in the history
* feat: blacklist media items

re Fallenbagel#490

* feat: blacklist media items

* feat: blacklist media items

* style: formatting

* refactor: close the manage slide-over when the media item is removed from the blacklist

* fix: fix media data in the db when blacklisting an item

* refactor: refactor component to accept show boolean

* refactor: hide watchlist button in the media page when it's blacklisted. Also add a blacklist button

* style: formatting

---------

Co-authored-by: JoaquinOlivero <[email protected]>
  • Loading branch information
2 people authored and thibodelanghe committed Dec 18, 2024
1 parent 33f19df commit 57dd2ad
Show file tree
Hide file tree
Showing 30 changed files with 1,939 additions and 350 deletions.
103 changes: 103 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ tags:
description: Endpoints related to getting service (Radarr/Sonarr) details.
- name: watchlist
description: Collection of media to watch later
- name: blacklist
description: Blacklisted media from discovery page.
servers:
- url: '{server}/api/v1'
variables:
Expand All @@ -46,6 +48,19 @@ servers:

components:
schemas:
Blacklist:
type: object
properties:
tmdbId:
type: number
example: 1
title:
type: string
media:
$ref: '#/components/schemas/MediaInfo'
userId:
type: number
example: 1
Watchlist:
type: object
properties:
Expand Down Expand Up @@ -4042,6 +4057,94 @@ paths:
restricted:
type: boolean
example: false
/blacklist:
get:
summary: Returns blacklisted items
description: Returns list of all blacklisted media
tags:
- settings
parameters:
- in: query
name: take
schema:
type: number
nullable: true
example: 25
- in: query
name: skip
schema:
type: number
nullable: true
example: 0
- in: query
name: search
schema:
type: string
nullable: true
example: dune
responses:
'200':
description: Blacklisted items returned
content:
application/json:
schema:
type: object
properties:
pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
type: object
properties:
user:
$ref: '#/components/schemas/User'
createdAt:
type: string
example: 2024-04-21T01:55:44.000Z
id:
type: number
example: 1
mediaType:
type: string
example: movie
title:
type: string
example: Dune
tmdbId:
type: number
example: 438631
post:
summary: Add media to blacklist
tags:
- blacklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Blacklist'
responses:
'201':
description: Item succesfully blacklisted
'412':
description: Item has already been blacklisted
/blacklist/{tmdbId}:
delete:
summary: Remove media from blacklist
tags:
- blacklist
parameters:
- in: path
name: tmdbId
description: tmdbId ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed media item
/watchlist:
post:
summary: Add media to watchlist
Expand Down
1 change: 1 addition & 0 deletions server/constants/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export enum MediaStatus {
PROCESSING,
PARTIALLY_AVAILABLE,
AVAILABLE,
BLACKLISTED,
}
95 changes: 95 additions & 0 deletions server/entity/Blacklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { MediaStatus, type MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { User } from '@server/entity/User';
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import type { ZodNumber, ZodOptional, ZodString } from 'zod';

@Entity()
@Unique(['tmdbId'])
export class Blacklist implements BlacklistItem {
@PrimaryGeneratedColumn()
public id: number;

@Column({ type: 'varchar' })
public mediaType: MediaType;

@Column({ nullable: true, type: 'varchar' })
title?: string;

@Column()
@Index()
public tmdbId: number;

@ManyToOne(() => User, (user) => user.id, {
eager: true,
})
user: User;

@OneToOne(() => Media, (media) => media.blacklist, {
onDelete: 'CASCADE',
})
@JoinColumn()
public media: Media;

@CreateDateColumn()
public createdAt: Date;

constructor(init?: Partial<Blacklist>) {
Object.assign(this, init);
}

public static async addToBlacklist({
blacklistRequest,
}: {
blacklistRequest: {
mediaType: MediaType;
title?: ZodOptional<ZodString>['_output'];
tmdbId: ZodNumber['_output'];
};
}): Promise<void> {
const blacklist = new this({
...blacklistRequest,
});

const mediaRepository = getRepository(Media);
let media = await mediaRepository.findOne({
where: {
tmdbId: blacklistRequest.tmdbId,
},
});

const blacklistRepository = getRepository(this);

await blacklistRepository.save(blacklist);

if (!media) {
media = new Media({
tmdbId: blacklistRequest.tmdbId,
status: MediaStatus.BLACKLISTED,
status4k: MediaStatus.BLACKLISTED,
mediaType: blacklistRequest.mediaType,
blacklist: blacklist,
});

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

await mediaRepository.save(media);
}
}
}
9 changes: 8 additions & 1 deletion server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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 type { User } from '@server/entity/User';
import { Watchlist } from '@server/entity/Watchlist';
import type { DownloadingItem } from '@server/lib/downloadtracker';
Expand All @@ -17,6 +18,7 @@ import {
Entity,
Index,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
Expand Down Expand Up @@ -66,7 +68,7 @@ class Media {

try {
const media = await mediaRepository.findOne({
where: { tmdbId: id, mediaType },
where: { tmdbId: id, mediaType: mediaType },
relations: { requests: true, issues: true },
});

Expand Down Expand Up @@ -116,6 +118,11 @@ class Media {
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
public issues: Issue[];

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

@CreateDateColumn()
public createdAt: Date;

Expand Down
11 changes: 11 additions & 0 deletions server/entity/MediaRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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 {}

type MediaRequestOptions = {
isAutoRequest?: boolean;
Expand Down Expand Up @@ -143,6 +144,16 @@ export class MediaRequest {
mediaType: requestBody.mediaType,
});
} else {
if (media.status === MediaStatus.BLACKLISTED) {
logger.warn('Request for media blocked due to being blacklisted', {
tmdbId: tmdbMedia.id,
mediaType: requestBody.mediaType,
label: 'Media Request',
});

throw new BlacklistedMediaError('This media is blacklisted.');
}

if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
media.status = MediaStatus.PENDING;
}
Expand Down
14 changes: 14 additions & 0 deletions server/interfaces/api/blacklistInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { User } from '@server/entity/User';
import type { PaginatedResponse } from '@server/interfaces/api/common';

export interface BlacklistItem {
tmdbId: number;
mediaType: 'movie' | 'tv';
title?: string;
createdAt?: Date;
user: User;
}

export interface BlacklistResultsResponse extends PaginatedResponse {
results: BlacklistItem[];
}
2 changes: 2 additions & 0 deletions server/lib/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export enum Permission {
AUTO_REQUEST_TV = 33554432,
RECENT_VIEW = 67108864,
WATCHLIST_VIEW = 134217728,
MANAGE_BLACKLIST = 268435456,
VIEW_BLACKLIST = 1073741824,
}

export interface PermissionCheckOptions {
Expand Down
20 changes: 20 additions & 0 deletions server/migration/1699901142442-AddBlacklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddBlacklist1699901142442 implements MigrationInterface {
name = 'AddBlacklist1699901142442';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')),"userId" integer, "mediaId" integer,CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId"))`
);

await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "blacklist"`);
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
}
}
Loading

0 comments on commit 57dd2ad

Please sign in to comment.