Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
270 changes: 270 additions & 0 deletions server/routes/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,273 @@ describe('POST /request/:requestId/retry', () => {
assert.ok(persisted.updatedAt > failed.updatedAt);
});
});

describe('DELETE /request/:requestId, deleted media status restoration', () => {
async function seedDeletedMediaScenario() {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);

const admin = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 99001,
status: MediaStatus.DELETED,
status4k: MediaStatus.UNKNOWN,
})
);

const staleRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.COMPLETED,
media,
requestedBy: admin,
is4k: false,
isAutoRequest: true,
})
);

media.status = MediaStatus.PENDING;
await mediaRepo.save(media);

const newRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.APPROVED,
media,
requestedBy: admin,
is4k: false,
})
);

return { media, staleRequest, newRequest, admin };
}

it('restores media status to DELETED when the re-request is deleted and a stale completed request remains', async () => {
const mediaRepo = getRepository(Media);
const { media, newRequest } = await seedDeletedMediaScenario();

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.delete(`/request/${newRequest.id}`);

assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status, MediaStatus.DELETED);
});

it('restores media status4k to DELETED when the re-request is deleted and a stale completed request remains', async () => {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);

const admin = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 99003,
status: MediaStatus.UNKNOWN,
status4k: MediaStatus.DELETED,
})
);

await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.COMPLETED,
media,
requestedBy: admin,
is4k: true,
isAutoRequest: true,
})
);

media.status4k = MediaStatus.PENDING;
await mediaRepo.save(media);

const newRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.APPROVED,
media,
requestedBy: admin,
is4k: true,
})
);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.delete(`/request/${newRequest.id}`);

assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status4k, MediaStatus.DELETED);
});

it('resets media status to UNKNOWN when the stale completed request is also deleted', async () => {
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);
const { media, newRequest, staleRequest } =
await seedDeletedMediaScenario();

const agent = await loginAs('admin@seerr.dev', 'test1234');

await agent.delete(`/request/${newRequest.id}`);

const res = await agent.delete(`/request/${staleRequest.id}`);
assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status, MediaStatus.UNKNOWN);

Comment thread
fallenbagel marked this conversation as resolved.
const remaining = await requestRepo.find({
where: { media: { id: media.id } },
});
assert.strictEqual(remaining.length, 0);
});

it('resets media status4k to UNKNOWN when the stale completed 4K request is also deleted', async () => {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);

const admin = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 99004,
status: MediaStatus.UNKNOWN,
status4k: MediaStatus.DELETED,
})
);

const staleRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.COMPLETED,
media,
requestedBy: admin,
is4k: true,
isAutoRequest: true,
})
);

media.status4k = MediaStatus.PENDING;
await mediaRepo.save(media);

const newRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.APPROVED,
media,
requestedBy: admin,
is4k: true,
})
);

const agent = await loginAs('admin@seerr.dev', 'test1234');

await agent.delete(`/request/${newRequest.id}`);

const res = await agent.delete(`/request/${staleRequest.id}`);
assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN);
});

it('does not reset media status when other active requests still exist', async () => {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);

const admin = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 99002,
status: MediaStatus.PENDING,
status4k: MediaStatus.UNKNOWN,
})
);

const req1 = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.PENDING,
media,
requestedBy: admin,
is4k: false,
})
);

await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.PENDING,
media,
requestedBy: admin,
is4k: false,
})
);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.delete(`/request/${req1.id}`);

assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status, MediaStatus.PENDING);
});

it('does not reset media status when status is PARTIALLY_AVAILABLE and only completed requests remain', async () => {
const userRepo = getRepository(User);
const mediaRepo = getRepository(Media);
const requestRepo = getRepository(MediaRequest);

const admin = await userRepo.findOneOrFail({
where: { email: 'admin@seerr.dev' },
});

const media = await mediaRepo.save(
new Media({
mediaType: MediaType.MOVIE,
tmdbId: 99005,
status: MediaStatus.PARTIALLY_AVAILABLE,
status4k: MediaStatus.UNKNOWN,
})
);

const completedRequest = await requestRepo.save(
new MediaRequest({
type: MediaType.MOVIE,
status: MediaRequestStatus.COMPLETED,
media,
requestedBy: admin,
is4k: false,
})
);

const agent = await loginAs('admin@seerr.dev', 'test1234');
const res = await agent.delete(`/request/${completedRequest.id}`);

assert.strictEqual(res.status, 204);

const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
assert.strictEqual(updated.status, MediaStatus.PARTIALLY_AVAILABLE);
});
});
38 changes: 32 additions & 6 deletions server/subscriber/MediaRequestSubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,13 +952,28 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
relations: { requests: true },
});

const hasActive = fullMedia.requests.some(
(request) =>
!request.is4k &&
request.status !== MediaRequestStatus.COMPLETED &&
request.status !== MediaRequestStatus.DECLINED
);
const hasActive4k = fullMedia.requests.some(
(request) =>
request.is4k &&
request.status !== MediaRequestStatus.COMPLETED &&
request.status !== MediaRequestStatus.DECLINED
);

const needsStatusUpdate =
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE;
!hasActive &&
fullMedia.status !== MediaStatus.AVAILABLE &&
fullMedia.status !== MediaStatus.PARTIALLY_AVAILABLE;

const needs4kStatusUpdate =
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE;
!hasActive4k &&
fullMedia.status4k !== MediaStatus.AVAILABLE &&
fullMedia.status4k !== MediaStatus.PARTIALLY_AVAILABLE;

if (needsStatusUpdate || needs4kStatusUpdate) {
// Re-fetch WITHOUT requests to avoid cascade issues on save
Expand All @@ -967,10 +982,21 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
});

if (needsStatusUpdate) {
cleanMedia.status = MediaStatus.UNKNOWN;
const hadCompleted = fullMedia.requests.some(
(r) => !r.is4k && r.status === MediaRequestStatus.COMPLETED
);
cleanMedia.status = hadCompleted
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
Comment thread
fallenbagel marked this conversation as resolved.
}

if (needs4kStatusUpdate) {
cleanMedia.status4k = MediaStatus.UNKNOWN;
const hadCompleted4k = fullMedia.requests.some(
(r) => r.is4k && r.status === MediaRequestStatus.COMPLETED
);
cleanMedia.status4k = hadCompleted4k
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
}

await manager.save(cleanMedia);
Expand Down
Loading