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.
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
- Soft Delete - Preserves request history, allows re-requesting
- 1:1 Request-to-Files - No duplicate requests for same audiobook
- Seeding Awareness - Keeps torrents seeding until requirements met
- Confirmation Dialog - Prevents accidental deletions
- Automatic Cleanup - Scheduled job handles orphaned downloads
User Flow
Admin Dashboard
- Navigate to Admin Dashboard → Recent Requests table
- Click "Delete" button next to request
- Review confirmation dialog with details:
- Request title
- Actions that will be taken
- Warning about re-requesting
- Click "Delete" to confirm or "Cancel" to abort
- 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:
-
Find Request
- Query:
deletedAt: null - Return 404 if not found or already deleted
- Query:
-
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++ -
Delete Media Files
- Path:
[media_dir]/[author]/[title]/ - ONLY deletes title folder (not author folder)
- Handles missing folders gracefully
- Path:
-
Delete from Library Backend
- Audiobookshelf Mode: Delete library item via API if
absItemIdexists- 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
plexGuidexists- 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
- Audiobookshelf Mode: Delete library item via API if
-
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
- Primary: Delete by ASIN (same query as availability check)
-
Clear Audiobook Linkage
- Reset audiobook.status to 'requested'
- Clear plexGuid (Plex mode) or absItemId (ABS mode)
-
Soft Delete Request
- UPDATE:
deletedAt = NOW(), deletedBy = adminUserId - Preserves for audit trail and orphaned download tracking
- UPDATE:
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:
- User requests audiobook previously deleted
- Query checks for existing request:
deletedAt: null - No active request found → allowed to create new request
- Old soft-deleted request remains in DB for audit
Edge Cases Handled
- ✅ Torrent not in qBittorrent - Skip deletion, continue with files
- ✅ Unlimited seeding (0) - Keep in qBittorrent, hard-delete orphaned request
- ✅ Incomplete download - Delete torrent + files immediately
- ✅ Seeding requirement met - Delete torrent + files
- ✅ Still seeding - Keep torrent, soft-delete request, cleanup job handles later
- ✅ Media folder not found - Log and continue (already deleted)
- ✅ Multiple delete clicks - Button disabled during deletion
- ✅ Network error - Alert shown, request remains
- ✅ ABS library item deletion fails - Log error, continue with soft delete
- ✅ No absItemId present - Skip ABS deletion (not yet in library)
- ✅ Plex library item deletion fails - Log error, continue with soft delete
- ✅ No plexGuid present - Skip Plex deletion (not yet in library)
- ✅ Plex deletion not enabled in settings - Log error, continue with soft delete
- ✅ Title mismatch in plex_library - ASIN-based deletion handles title variations (e.g., "(Unabridged)" suffix)
- ✅ 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:
deletedBytracks 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
Related
- Admin Dashboard - Dashboard overview
- Scheduler - Cleanup job details
- File Organization - Media directory structure
- qBittorrent - Torrent management