Skip to content

Commit cb20918

Browse files
committed
fix(request): restore media status correctly when deleting requests
Reset media status to DELETED (or UNKNOWN) when the last active request is removed, based on whether completed requests exist
1 parent 32169d9 commit cb20918

2 files changed

Lines changed: 302 additions & 6 deletions

File tree

server/routes/request.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,273 @@ describe('POST /request/:requestId/retry', () => {
265265
assert.ok(persisted.updatedAt > failed.updatedAt);
266266
});
267267
});
268+
269+
describe('DELETE /request/:requestId, deleted media status restoration', () => {
270+
async function seedDeletedMediaScenario() {
271+
const userRepo = getRepository(User);
272+
const mediaRepo = getRepository(Media);
273+
const requestRepo = getRepository(MediaRequest);
274+
275+
const admin = await userRepo.findOneOrFail({
276+
where: { email: 'admin@seerr.dev' },
277+
});
278+
279+
const media = await mediaRepo.save(
280+
new Media({
281+
mediaType: MediaType.MOVIE,
282+
tmdbId: 99001,
283+
status: MediaStatus.DELETED,
284+
status4k: MediaStatus.UNKNOWN,
285+
})
286+
);
287+
288+
const staleRequest = await requestRepo.save(
289+
new MediaRequest({
290+
type: MediaType.MOVIE,
291+
status: MediaRequestStatus.COMPLETED,
292+
media,
293+
requestedBy: admin,
294+
is4k: false,
295+
isAutoRequest: true,
296+
})
297+
);
298+
299+
media.status = MediaStatus.PENDING;
300+
await mediaRepo.save(media);
301+
302+
const newRequest = await requestRepo.save(
303+
new MediaRequest({
304+
type: MediaType.MOVIE,
305+
status: MediaRequestStatus.APPROVED,
306+
media,
307+
requestedBy: admin,
308+
is4k: false,
309+
})
310+
);
311+
312+
return { media, staleRequest, newRequest, admin };
313+
}
314+
315+
it('restores media status to DELETED when the re-request is deleted and a stale completed request remains', async () => {
316+
const mediaRepo = getRepository(Media);
317+
const { media, newRequest } = await seedDeletedMediaScenario();
318+
319+
const agent = await loginAs('admin@seerr.dev', 'test1234');
320+
const res = await agent.delete(`/request/${newRequest.id}`);
321+
322+
assert.strictEqual(res.status, 204);
323+
324+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
325+
assert.strictEqual(updated.status, MediaStatus.DELETED);
326+
});
327+
328+
it('restores media status4k to DELETED when the re-request is deleted and a stale completed request remains', async () => {
329+
const userRepo = getRepository(User);
330+
const mediaRepo = getRepository(Media);
331+
const requestRepo = getRepository(MediaRequest);
332+
333+
const admin = await userRepo.findOneOrFail({
334+
where: { email: 'admin@seerr.dev' },
335+
});
336+
337+
const media = await mediaRepo.save(
338+
new Media({
339+
mediaType: MediaType.MOVIE,
340+
tmdbId: 99003,
341+
status: MediaStatus.UNKNOWN,
342+
status4k: MediaStatus.DELETED,
343+
})
344+
);
345+
346+
await requestRepo.save(
347+
new MediaRequest({
348+
type: MediaType.MOVIE,
349+
status: MediaRequestStatus.COMPLETED,
350+
media,
351+
requestedBy: admin,
352+
is4k: true,
353+
isAutoRequest: true,
354+
})
355+
);
356+
357+
media.status4k = MediaStatus.PENDING;
358+
await mediaRepo.save(media);
359+
360+
const newRequest = await requestRepo.save(
361+
new MediaRequest({
362+
type: MediaType.MOVIE,
363+
status: MediaRequestStatus.APPROVED,
364+
media,
365+
requestedBy: admin,
366+
is4k: true,
367+
})
368+
);
369+
370+
const agent = await loginAs('admin@seerr.dev', 'test1234');
371+
const res = await agent.delete(`/request/${newRequest.id}`);
372+
373+
assert.strictEqual(res.status, 204);
374+
375+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
376+
assert.strictEqual(updated.status4k, MediaStatus.DELETED);
377+
});
378+
379+
it('resets media status to UNKNOWN when the stale completed request is also deleted', async () => {
380+
const mediaRepo = getRepository(Media);
381+
const requestRepo = getRepository(MediaRequest);
382+
const { media, newRequest, staleRequest } =
383+
await seedDeletedMediaScenario();
384+
385+
const agent = await loginAs('admin@seerr.dev', 'test1234');
386+
387+
await agent.delete(`/request/${newRequest.id}`);
388+
389+
const res = await agent.delete(`/request/${staleRequest.id}`);
390+
assert.strictEqual(res.status, 204);
391+
392+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
393+
assert.strictEqual(updated.status, MediaStatus.UNKNOWN);
394+
395+
const remaining = await requestRepo.find({
396+
where: { media: { id: media.id } },
397+
});
398+
assert.strictEqual(remaining.length, 0);
399+
});
400+
401+
it('resets media status4k to UNKNOWN when the stale completed 4K request is also deleted', async () => {
402+
const userRepo = getRepository(User);
403+
const mediaRepo = getRepository(Media);
404+
const requestRepo = getRepository(MediaRequest);
405+
406+
const admin = await userRepo.findOneOrFail({
407+
where: { email: 'admin@seerr.dev' },
408+
});
409+
410+
const media = await mediaRepo.save(
411+
new Media({
412+
mediaType: MediaType.MOVIE,
413+
tmdbId: 99004,
414+
status: MediaStatus.UNKNOWN,
415+
status4k: MediaStatus.DELETED,
416+
})
417+
);
418+
419+
const staleRequest = await requestRepo.save(
420+
new MediaRequest({
421+
type: MediaType.MOVIE,
422+
status: MediaRequestStatus.COMPLETED,
423+
media,
424+
requestedBy: admin,
425+
is4k: true,
426+
isAutoRequest: true,
427+
})
428+
);
429+
430+
media.status4k = MediaStatus.PENDING;
431+
await mediaRepo.save(media);
432+
433+
const newRequest = await requestRepo.save(
434+
new MediaRequest({
435+
type: MediaType.MOVIE,
436+
status: MediaRequestStatus.APPROVED,
437+
media,
438+
requestedBy: admin,
439+
is4k: true,
440+
})
441+
);
442+
443+
const agent = await loginAs('admin@seerr.dev', 'test1234');
444+
445+
await agent.delete(`/request/${newRequest.id}`);
446+
447+
const res = await agent.delete(`/request/${staleRequest.id}`);
448+
assert.strictEqual(res.status, 204);
449+
450+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
451+
assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN);
452+
});
453+
454+
it('does not reset media status when other active requests still exist', async () => {
455+
const userRepo = getRepository(User);
456+
const mediaRepo = getRepository(Media);
457+
const requestRepo = getRepository(MediaRequest);
458+
459+
const admin = await userRepo.findOneOrFail({
460+
where: { email: 'admin@seerr.dev' },
461+
});
462+
463+
const media = await mediaRepo.save(
464+
new Media({
465+
mediaType: MediaType.MOVIE,
466+
tmdbId: 99002,
467+
status: MediaStatus.PENDING,
468+
status4k: MediaStatus.UNKNOWN,
469+
})
470+
);
471+
472+
const req1 = await requestRepo.save(
473+
new MediaRequest({
474+
type: MediaType.MOVIE,
475+
status: MediaRequestStatus.PENDING,
476+
media,
477+
requestedBy: admin,
478+
is4k: false,
479+
})
480+
);
481+
482+
await requestRepo.save(
483+
new MediaRequest({
484+
type: MediaType.MOVIE,
485+
status: MediaRequestStatus.PENDING,
486+
media,
487+
requestedBy: admin,
488+
is4k: false,
489+
})
490+
);
491+
492+
const agent = await loginAs('admin@seerr.dev', 'test1234');
493+
const res = await agent.delete(`/request/${req1.id}`);
494+
495+
assert.strictEqual(res.status, 204);
496+
497+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
498+
assert.strictEqual(updated.status, MediaStatus.PENDING);
499+
});
500+
501+
it('does not reset media status when status is PARTIALLY_AVAILABLE and only completed requests remain', async () => {
502+
const userRepo = getRepository(User);
503+
const mediaRepo = getRepository(Media);
504+
const requestRepo = getRepository(MediaRequest);
505+
506+
const admin = await userRepo.findOneOrFail({
507+
where: { email: 'admin@seerr.dev' },
508+
});
509+
510+
const media = await mediaRepo.save(
511+
new Media({
512+
mediaType: MediaType.MOVIE,
513+
tmdbId: 99005,
514+
status: MediaStatus.PARTIALLY_AVAILABLE,
515+
status4k: MediaStatus.UNKNOWN,
516+
})
517+
);
518+
519+
const completedRequest = await requestRepo.save(
520+
new MediaRequest({
521+
type: MediaType.MOVIE,
522+
status: MediaRequestStatus.COMPLETED,
523+
media,
524+
requestedBy: admin,
525+
is4k: false,
526+
})
527+
);
528+
529+
const agent = await loginAs('admin@seerr.dev', 'test1234');
530+
const res = await agent.delete(`/request/${completedRequest.id}`);
531+
532+
assert.strictEqual(res.status, 204);
533+
534+
const updated = await mediaRepo.findOneOrFail({ where: { id: media.id } });
535+
assert.strictEqual(updated.status, MediaStatus.PARTIALLY_AVAILABLE);
536+
});
537+
});

server/subscriber/MediaRequestSubscriber.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -952,13 +952,28 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
952952
relations: { requests: true },
953953
});
954954

955+
const hasActive = fullMedia.requests.some(
956+
(request) =>
957+
!request.is4k &&
958+
request.status !== MediaRequestStatus.COMPLETED &&
959+
request.status !== MediaRequestStatus.DECLINED
960+
);
961+
const hasActive4k = fullMedia.requests.some(
962+
(request) =>
963+
request.is4k &&
964+
request.status !== MediaRequestStatus.COMPLETED &&
965+
request.status !== MediaRequestStatus.DECLINED
966+
);
967+
955968
const needsStatusUpdate =
956-
!fullMedia.requests.some((request) => !request.is4k) &&
957-
fullMedia.status !== MediaStatus.AVAILABLE;
969+
!hasActive &&
970+
fullMedia.status !== MediaStatus.AVAILABLE &&
971+
fullMedia.status !== MediaStatus.PARTIALLY_AVAILABLE;
958972

959973
const needs4kStatusUpdate =
960-
!fullMedia.requests.some((request) => request.is4k) &&
961-
fullMedia.status4k !== MediaStatus.AVAILABLE;
974+
!hasActive4k &&
975+
fullMedia.status4k !== MediaStatus.AVAILABLE &&
976+
fullMedia.status4k !== MediaStatus.PARTIALLY_AVAILABLE;
962977

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

969984
if (needsStatusUpdate) {
970-
cleanMedia.status = MediaStatus.UNKNOWN;
985+
const hadCompleted = fullMedia.requests.some(
986+
(r) => !r.is4k && r.status === MediaRequestStatus.COMPLETED
987+
);
988+
cleanMedia.status = hadCompleted
989+
? MediaStatus.DELETED
990+
: MediaStatus.UNKNOWN;
971991
}
992+
972993
if (needs4kStatusUpdate) {
973-
cleanMedia.status4k = MediaStatus.UNKNOWN;
994+
const hadCompleted4k = fullMedia.requests.some(
995+
(r) => r.is4k && r.status === MediaRequestStatus.COMPLETED
996+
);
997+
cleanMedia.status4k = hadCompleted4k
998+
? MediaStatus.DELETED
999+
: MediaStatus.UNKNOWN;
9741000
}
9751001

9761002
await manager.save(cleanMedia);

0 commit comments

Comments
 (0)