mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
c559f8ebe9
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.
291 lines
9.3 KiB
Markdown
291 lines
9.3 KiB
Markdown
# 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:**
|
|
```prisma
|
|
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
|
|
|
|
```typescript
|
|
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:**
|
|
```json
|
|
{
|
|
"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:
|
|
```typescript
|
|
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
|
|
|
|
## Related
|
|
|
|
- [Admin Dashboard](../admin-dashboard.md) - Dashboard overview
|
|
- [Scheduler](../backend/services/scheduler.md) - Cleanup job details
|
|
- [File Organization](../phase3/file-organization.md) - Media directory structure
|
|
- [qBittorrent](../phase3/qbittorrent.md) - Torrent management
|