Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d46b45a
feat(permissions): add removal request permissions
MrDWilson Apr 5, 2026
90de224
feat(media): add media removal requests
MrDWilson Apr 5, 2026
bd5c3f2
feat(sonarr): add season file removal and unmonitoring
MrDWilson Apr 5, 2026
b853456
feat(api): add removal request endpoints
MrDWilson Apr 5, 2026
9ba384e
feat(permissions): add removal request permissions to UI
MrDWilson Apr 5, 2026
e5d5281
feat(ui): add media removal request management
MrDWilson Apr 5, 2026
86f104f
feat(ui): add media and season removal request management
MrDWilson Apr 5, 2026
c896897
feat(ui): move removal requester info to tooltip
MrDWilson Apr 5, 2026
c4c92c3
feat(api): refine removal request logic and validation
MrDWilson Apr 5, 2026
f7ab2e2
feat(i18n): add translations for removal requests
MrDWilson Apr 5, 2026
eef4032
feat(removal): add 4k season support and refine request logic
MrDWilson Apr 5, 2026
cd19b83
feat(api): add requestedBy filter for removal requests
MrDWilson Apr 5, 2026
abca294
Merge branch 'develop' into feat/removal-request
MrDWilson Apr 5, 2026
f6cd337
feat(removal): improve request validation and data persistence
MrDWilson Apr 6, 2026
da4da62
feat(removal): include 4k status in request lookup, load media relations
MrDWilson Apr 6, 2026
3c6bdfb
feat(removal): implement multi-user aware media removal and partial t…
MrDWilson Apr 6, 2026
de780f8
Merge remote-tracking branch 'seerr-team/develop' into feat/removal-r…
MrDWilson May 30, 2026
53f6833
fix(removal): scope multi-user consent by 4K and season
MrDWilson May 30, 2026
fe8ed93
fix(removal): make Servarr removal idempotent and simplify create guard
MrDWilson May 30, 2026
0f88ac8
fix(removal): regenerate migrations against current develop
MrDWilson May 30, 2026
6871e0c
fix(removal): improve removal UI feedback, gating, and a11y
MrDWilson May 30, 2026
a14b501
docs(api): document removal-request schema and response codes
MrDWilson May 30, 2026
d095571
test(removal): cover removal-request routes and multi-user consent
MrDWilson May 30, 2026
d8a91b8
fix(removal): stop the season modal from freezing the manage pane
MrDWilson May 30, 2026
576802d
fix(removal): show users their own removal requests and unify the UI
MrDWilson May 30, 2026
503ec1c
fix(removal): count full-series requests in season consent
MrDWilson May 30, 2026
9fd6681
style(api): satisfy prettier for the removal-request filter enum
MrDWilson May 30, 2026
752f052
Merge branch 'develop' into feat/removal-request
MrDWilson Jun 2, 2026
08d0578
Merge branch 'develop' into feat/removal-request
MrDWilson Jun 5, 2026
25caecb
Merge branch 'develop' into feat/removal-request
MrDWilson Jun 10, 2026
e1f972a
Merge branch 'develop' into feat/removal-request
MrDWilson Jun 16, 2026
152aacb
Merge branch 'develop' into feat/removal-request
MrDWilson Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6245,6 +6245,160 @@ paths:
type: string
title:
type: string
/removal-request:
get:
summary: Get all removal requests
description: |
Returns all removal requests if the user has the `ADMIN` or `MANAGE_REQUESTS` permissions. Otherwise, only the logged-in user's removal requests are returned.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
tags:
- removal-request
parameters:
- in: query
name: take
schema:
type: number
nullable: true
example: 20
- in: query
name: skip
schema:
type: number
nullable: true
example: 0
- in: query
name: filter
schema:
type: string
enum: [pending, approved, declined, failed, all]
nullable: true
responses:
'200':
description: Removal requests returned
content:
application/json:
schema:
type: object
properties:
pageInfo:
$ref: '#/components/schemas/PageInfo'
results:
type: array
items:
type: object
post:
summary: Create a removal request
description: |
Creates a new removal request. Requires the `REQUEST_REMOVAL` permission.
tags:
- removal-request
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- mediaId
properties:
mediaId:
type: number
example: 1
is4k:
type: boolean
default: false
reason:
type: string
nullable: true
seasons:
type: array
items:
type: number
nullable: true
responses:
'201':
description: Removal request created
content:
application/json:
schema:
type: object
'409':
description: A pending removal request already exists for this media
/removal-request/{requestId}/approve:
post:
summary: Approve a removal request
description: |
Approves a removal request and executes the media removal. Requires the `MANAGE_REQUESTS` permission.
tags:
- removal-request
parameters:
- in: path
name: requestId
required: true
schema:
type: number
responses:
'200':
description: Removal request approved
content:
application/json:
schema:
type: object
/removal-request/{requestId}/decline:
post:
summary: Decline a removal request
description: |
Declines a removal request. Requires the `MANAGE_REQUESTS` permission.
tags:
- removal-request
parameters:
- in: path
name: requestId
required: true
schema:
type: number
responses:
'200':
description: Removal request declined
content:
application/json:
schema:
type: object
/removal-request/{requestId}/retry:
post:
summary: Retry a failed removal request
description: |
Retries a previously failed removal request. Requires the `MANAGE_REQUESTS` permission.
tags:
- removal-request
parameters:
- in: path
name: requestId
required: true
schema:
type: number
responses:
'200':
description: Removal request retried
content:
application/json:
schema:
type: object
/removal-request/{requestId}:
delete:
summary: Delete a removal request
description: |
Deletes a removal request. The requester or a user with `MANAGE_REQUESTS` permission can delete.
tags:
- removal-request
parameters:
- in: path
name: requestId
required: true
schema:
type: number
responses:
'204':
description: Removal request deleted
/request:
get:
summary: Get all requests
Expand Down
49 changes: 49 additions & 0 deletions server/api/servarr/sonarr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,55 @@ class SonarrAPI extends ServarrBase<{
}
};

public removeSeasonFiles = async (
tvdbId: number,
seasonNumbers: number[]
): Promise<void> => {
try {
const series = await this.getSeriesByTvdbId(tvdbId);
if (!series.id) {
throw new Error('Series not found in Sonarr');
}

const episodes = await this.getEpisodes(series.id);
const targetEpisodes = episodes.filter((ep) =>
seasonNumbers.includes(ep.seasonNumber)
);
const episodeFileIds = targetEpisodes
.filter((ep) => ep.hasFile && ep.episodeFileId > 0)
.map((ep) => ep.episodeFileId);

// Unmonitor the affected episodes before deleting files
const episodeIds = targetEpisodes.map((ep) => ep.id);
if (episodeIds.length > 0) {
await this.axios.put('/episode/monitor', {
episodeIds,
monitored: false,
});
Comment thread
MrDWilson marked this conversation as resolved.
}

// Delete episode files
for (const fileId of [...new Set(episodeFileIds)]) {
await this.axios.delete(`/episodefile/${fileId}`);
Comment thread
MrDWilson marked this conversation as resolved.
}

// Unmonitor the seasons
series.seasons = series.seasons.map((s) => ({
...s,
monitored: seasonNumbers.includes(s.seasonNumber) ? false : s.monitored,
}));
await this.axios.put(`/series/${series.id}`, series);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
MrDWilson marked this conversation as resolved.

logger.info(
`[Sonarr] Removed files for seasons ${seasonNumbers.join(', ')} of ${series.title}`
);
} catch (e) {
throw new Error(`[Sonarr] Failed to remove season files: ${e.message}`, {
cause: e,
});
}
};

public clearCache = ({
tvdbId,
externalId,
Expand Down
7 changes: 7 additions & 0 deletions server/constants/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ export enum MediaStatus {
BLOCKLISTED,
DELETED,
}

export enum MediaRemovalRequestStatus {
PENDING = 1,
APPROVED,
DECLINED,
FAILED,
}
10 changes: 9 additions & 1 deletion server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PrimaryGeneratedColumn,
} from 'typeorm';
import Issue from './Issue';
import { MediaRemovalRequest } from './MediaRemovalRequest';
import { MediaRequest } from './MediaRequest';
import Season from './Season';

Expand Down Expand Up @@ -70,7 +71,7 @@ class Media {
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id, mediaType: mediaType },
relations: { requests: true, issues: true },
relations: { requests: true, issues: true, removalRequests: true },
});

return media ?? undefined;
Expand Down Expand Up @@ -111,6 +112,13 @@ class Media {
})
public requests: MediaRequest[];

@OneToMany(
() => MediaRemovalRequest,
(removalRequest) => removalRequest.media,
{ cascade: ['insert', 'remove'] }
)
public removalRequests: MediaRemovalRequest[];

@OneToMany(() => Watchlist, (watchlist) => watchlist.media)
public watchlists: null | Watchlist[];

Expand Down
Loading
Loading