Files
ReadMeABook/documentation/admin-features/request-deletion.md
T
kikootwo c559f8ebe9 SABnzbd path mapping + ASIN-based request deletion
Add bidirectional path mapping and complete_dir-aware category sync to the SABnzbd integration. Introduces PathMapper usage, complete_dir extraction, calculateCategoryPath(), and ensureCategory() logic to choose empty/relative/absolute category paths; ensureCategory is invoked before adding NZBs. Update singleton factory to load download_dir and path-mapping config from DownloadClientManager and recreate the service when config is not loaded. Make DownloadClientManager pass path-mapping config into the SABnzbd service. Change request deletion to remove plex_library records by ASIN (deleteMany) with a fallback to exact title/author matches so availability checks and deletions are consistent. Update documentation and tests to reflect the new behavior and APIs.
2026-02-03 12:20:44 -05:00

9.3 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: Delete library item via API if plexGuid exists
      • Queries plex_library table to get plexRatingKey from audiobook's plexGuid
      • Calls Plex DELETE /library/metadata/{ratingKey} endpoint with the ratingKey
      • Requires deletion enabled in Plex: Settings > Server > Library
  5. Delete plex_library Cache Records

    • Primary: Delete by ASIN (same query as availability check)
      • WHERE asin = audiobookAsin OR plexGuid CONTAINS audiobookAsin
      • Ensures exact same record found during availability check gets deleted
    • Fallback: Delete by exact title/author (for legacy records without ASIN)
      • Only used if ASIN-based deletion finds no records
    • Result: Book immediately shows as NOT available, can be re-requested
  6. Clear Audiobook Linkage

    • Reset audiobook.status to 'requested'
    • Clear plexGuid (Plex mode) or absItemId (ABS mode)
  7. 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)
  11. Plex library item deletion fails - Log error, continue with soft delete
  12. No plexGuid present - Skip Plex deletion (not yet in library)
  13. Plex deletion not enabled in settings - Log error, continue with soft delete
  14. Title mismatch in plex_library - ASIN-based deletion handles title variations (e.g., "(Unabridged)" suffix)
  15. No ASIN available - Falls back to exact title/author matching

Fixed Issues

1. Book Shows "Available" After Deletion Until Library Scan

  • Issue: Deleted books remained "available" until the next library scan
  • Cause: plex_library deletion used title/author matching, but availability check used ASIN matching
  • Impact: Title variations (e.g., "Book Title" vs "Book Title (Unabridged)") caused plex_library records to persist
  • Fix: Changed plex_library deletion to use ASIN-based matching (same as availability check)
  • Result: Books immediately show as NOT available after deletion, can be re-requested right away

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