Files
ReadMeABook/documentation/admin-features/request-deletion.md
T
kikootwo 3a9ae4a439 Add request approval system and audiobook path template
Implements admin approval workflow for user requests with global and per-user auto-approve controls. Adds new request statuses ('awaiting_approval', 'denied'), related API endpoints, and UI for pending approvals. Introduces configurable audiobook organization path template with validation and preview in settings, updates database schema and migrations for new fields.
2026-01-28 11:41:59 -05:00

7.5 KiB

Request Deletion (Admin Feature)

Status: Implemented

Admin feature for deleting requests with intelligent cleanup of media files and torrents.

Overview

Allows admins to delete requests from the admin dashboard with smart handling of:

  • Soft deletion (allows re-requesting)
  • Media file cleanup
  • Torrent seeding management
  • Orphaned download tracking

Key Features

  1. Soft Delete - Preserves request history, allows re-requesting
  2. 1:1 Request-to-Files - No duplicate requests for same audiobook
  3. Seeding Awareness - Keeps torrents seeding until requirements met
  4. Confirmation Dialog - Prevents accidental deletions
  5. Automatic Cleanup - Scheduled job handles orphaned downloads

User Flow

Admin Dashboard

  1. Navigate to Admin Dashboard → Recent Requests table
  2. Click "Delete" button next to request
  3. Review confirmation dialog with details:
    • Request title
    • Actions that will be taken
    • Warning about re-requesting
  4. Click "Delete" to confirm or "Cancel" to abort
  5. Request deleted, UI updates automatically

Technical Implementation

Database Schema

Soft Delete Fields:

model Request {
  // ... existing fields ...
  deletedAt  DateTime? @map("deleted_at")
  deletedBy  String?   @map("deleted_by")
}

Unique Constraint: Removed from schema, enforced in application code

Deletion Logic Flow

Service: src/lib/services/request-delete.service.ts

Steps:

  1. Find Request

    • Query: deletedAt: null
    • Return 404 if not found or already deleted
  2. Handle Downloads & Seeding

    For each selected download:

    IF torrent not in qBittorrent:
      → Skip (already removed)
    
    ELSE IF unlimited seeding (0):
      → Log: "Keeping for unlimited seeding"
      → Do nothing (stop monitoring)
      → torrentsKeptUnlimited++
    
    ELSE IF download not completed:
      → Delete torrent + files
      → torrentsRemoved++
    
    ELSE:
      → Query actual seeding time
      → Calculate remaining = (target - actual)
    
      IF remaining > 0:
        → Log: "Keeping for X more minutes"
        → torrentsKeptSeeding++
      ELSE:
        → Delete torrent + files
        → torrentsRemoved++
    
  3. Delete Media Files

    • Path: [media_dir]/[author]/[title]/
    • ONLY deletes title folder (not author folder)
    • Handles missing folders gracefully
  4. Delete from Library Backend

    • Audiobookshelf Mode: Delete library item via API if absItemId exists
      • Prevents "ghost" entries in Audiobookshelf library
      • Only removes from ABS database, not files (already deleted in step 3)
    • Plex Mode: Clear plex_library cache records
  5. Soft Delete Request

    • UPDATE: deletedAt = NOW(), deletedBy = adminUserId
    • Preserves for audit trail and orphaned download tracking

Cleanup Job Enhancement

Processor: src/lib/processors/cleanup-seeded-torrents.processor.ts

Query: Finds both active + soft-deleted requests

where: {
  OR: [
    { status: ['available', 'downloaded'], deletedAt: null },
    { deletedAt: { not: null } }
  ]
}

Behavior:

  • Active requests: Delete torrent when seeding complete
  • Soft-deleted requests: Delete torrent + hard-delete request when seeding complete
  • Unlimited seeding: Hard-delete orphaned request immediately (no monitoring)

API Endpoint

DELETE /api/admin/requests/:id

Authorization: Admin only

Request: No body

Response:

{
  "success": true,
  "message": "Request deleted successfully",
  "details": {
    "filesDeleted": true,
    "torrentsRemoved": 2,
    "torrentsKeptSeeding": 1,
    "torrentsKeptUnlimited": 0
  }
}

Errors:

  • 401: Unauthorized (not logged in)
  • 403: Forbidden (not admin)
  • 404: Request not found or already deleted
  • 500: Internal server error

Frontend Components

ConfirmDialog (src/app/admin/components/ConfirmDialog.tsx)

  • Reusable confirmation modal
  • Props: title, message, confirmLabel, confirmVariant
  • Supports danger (red) and primary (blue) variants

RecentRequestsTable (src/app/admin/components/RecentRequestsTable.tsx)

  • Added "Actions" column with Delete button
  • State management for confirmation dialog
  • SWR cache invalidation after deletion
  • Loading states during deletion

Re-Requesting After Deletion

Application-Level Uniqueness:

All prisma.request.findMany/findFirst queries include:

where: {
  // ... other conditions
  deletedAt: null  // Only active requests
}

Re-Request Flow:

  1. User requests audiobook previously deleted
  2. Query checks for existing request: deletedAt: null
  3. No active request found → allowed to create new request
  4. Old soft-deleted request remains in DB for audit

Edge Cases Handled

  1. Torrent not in qBittorrent - Skip deletion, continue with files
  2. Unlimited seeding (0) - Keep in qBittorrent, hard-delete orphaned request
  3. Incomplete download - Delete torrent + files immediately
  4. Seeding requirement met - Delete torrent + files
  5. Still seeding - Keep torrent, soft-delete request, cleanup job handles later
  6. Media folder not found - Log and continue (already deleted)
  7. Multiple delete clicks - Button disabled during deletion
  8. Network error - Alert shown, request remains
  9. ABS library item deletion fails - Log error, continue with soft delete
  10. No absItemId present - Skip ABS deletion (not yet in library)

File Structure

Backend:
- prisma/schema.prisma (deletedAt, deletedBy fields)
- src/lib/services/request-delete.service.ts (deletion logic)
- src/app/api/admin/requests/[id]/route.ts (DELETE endpoint)
- src/lib/processors/cleanup-seeded-torrents.processor.ts (orphaned cleanup)

Frontend:
- src/app/admin/components/ConfirmDialog.tsx (confirmation modal)
- src/app/admin/components/RecentRequestsTable.tsx (Delete button + logic)

Queries Updated (deletedAt: null filters):
- src/app/api/requests/route.ts (GET, POST)
- src/app/api/requests/[id]/route.ts (GET, PATCH)
- src/app/api/admin/requests/recent/route.ts (GET)
- src/app/api/admin/metrics/route.ts (GET)
- src/app/api/admin/downloads/active/route.ts (GET)
- src/lib/processors/*.ts (all processors)

Configuration

No new config required - uses existing:

  • prowlarr_indexers (seeding time per indexer)
  • media_dir (file deletion path)

Security

  • Authorization: Admin role required
  • Audit Trail: deletedBy tracks admin user ID
  • Soft Delete: Preserves history, prevents permanent data loss
  • Confirmation Required: Prevents accidental deletion

Monitoring & Logging

Logs:

  • [RequestDelete] prefix for deletion service
  • [CleanupSeededTorrents] prefix for cleanup job
  • Torrent status (removed/kept/unlimited)
  • File deletion success/failure
  • Orphaned request hard deletion

Admin Dashboard:

  • Request count updates after deletion
  • Recent requests table refreshes automatically
  • Toast notifications (via console.log - can be enhanced)

Future Enhancements

  • Toast notifications instead of console.log
  • Deletion history view (soft-deleted requests)
  • Bulk delete operations
  • Restore deleted requests (undo)
  • Email notifications for deletions
  • Deletion reason/notes field