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.
This commit is contained in:
kikootwo
2026-02-03 12:20:44 -05:00
parent 11376b36a2
commit c559f8ebe9
12 changed files with 805 additions and 131 deletions
@@ -100,9 +100,20 @@ model Request {
- 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
- Also clears plex_library cache records
5. **Soft Delete Request**
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
@@ -201,6 +212,17 @@ where: {
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
+64 -8
View File
@@ -65,9 +65,18 @@ Service uses singleton pattern. When settings change, singleton invalidated to f
**Category:** `readmeabook` (auto-created for all downloads)
**Save Path Synchronization:**
- Category created on first download if not exists
- Category path set to `download_dir` config value
- Unlike qBittorrent, SABnzbd categories are less frequently updated (set once at creation)
- Category created/updated on every download (matches qBittorrent behavior)
- Fetches SABnzbd's `complete_dir` setting via API to understand download location
- Applies remote path mapping to translate RMAB's `download_dir` to SABnzbd's perspective
- Calculates optimal category path (relative, absolute, or root)
**Smart Path Calculation:**
1. Get SABnzbd's `complete_dir` from `misc.complete_dir` config
2. Apply `PathMapper.reverseTransform()` to RMAB's `download_dir`
3. Compare transformed path to `complete_dir`:
- **Match:** Use empty string (downloads go to complete_dir root)
- **Subdirectory:** Use relative path (e.g., `audiobooks`)
- **Different:** Use absolute path (e.g., `/mnt/media/audiobooks`)
## Post-Processing
@@ -121,6 +130,12 @@ interface HistoryItem {
completedTimestamp: string; // Unix timestamp
downloadTime: string; // Seconds
}
interface SABnzbdConfig {
version: string;
completeDir: string; // SABnzbd's configured complete download folder
categories: Array<{ name: string; dir: string }>;
}
```
## NZB ID vs Torrent Hash
@@ -168,11 +183,40 @@ interface HistoryItem {
**Use Case:** SABnzbd runs on different machine/container with different filesystem perspective.
**Example Scenario:**
- SABnzbd reports: `/remote/usenet/complete/Audiobook.Name`
- ReadMeABook needs: `/downloads/Audiobook.Name`
- Mapping: Remote `/remote/usenet/complete` Local `/downloads`
- SABnzbd sees: `/mnt/usenet/complete`
- ReadMeABook sees: `/downloads`
- Mapping: Remote `/mnt/usenet/complete` Local `/downloads`
**Implementation:** Same as qBittorrent (uses `PathMapper` utility)
**Bidirectional Path Mapping:**
**1. Outgoing (RMAB → SABnzbd):** When setting category path
- RMAB's download path: `/downloads`
- Translated to SABnzbd's path: `/mnt/usenet/complete`
- Applied in `sabnzbd.service.ts` via `PathMapper.reverseTransform()`
- Combined with `complete_dir` detection for optimal category configuration
**2. Incoming (SABnzbd → RMAB):** When processing completed downloads
- SABnzbd reports: `/mnt/usenet/complete/Audiobook.Name`
- Translated to RMAB's path: `/downloads/Audiobook.Name`
- Applied in `monitor-download.processor.ts` via `PathMapper.transform()`
- Ensures RMAB can find and organize files
**Path Transformation Examples:**
```typescript
// Outgoing: RMAB → SABnzbd (when setting category)
localPath = "/downloads"
config = { remotePath: "/mnt/usenet/complete", localPath: "/downloads" }
remotePath = PathMapper.reverseTransform(localPath, config)
// Result: "/mnt/usenet/complete"
// Incoming: SABnzbd → RMAB (when processing completion)
sabPath = "/mnt/usenet/complete/Audiobook.Name"
config = { remotePath: "/mnt/usenet/complete", localPath: "/downloads" }
organizePath = PathMapper.transform(sabPath, config)
// Result: "/downloads/Audiobook.Name"
```
**Implementation:** Uses `PathMapper` utility (same as qBittorrent)
## Fixed Issues ✅
@@ -181,6 +225,16 @@ interface HistoryItem {
**3. Post-Processing Tracking** - Monitors extracting/repairing states
**4. Queue vs History Logic** - Checks queue first, falls back to history
**5. SSL Certificate Errors** - Optional SSL verification disable for self-signed certs
**6. Category path not synced with complete_dir** - SABnzbd downloads to its own `complete_dir`, not RMAB's path. Fixed by:
- Fetching SABnzbd's `complete_dir` from config API (`misc.complete_dir`)
- Calculating relative or absolute category path based on path comparison
- Applying remote path mapping before comparison
- Syncing category path on every download (same as qBittorrent)
**7. Remote path mapping not applied** - Paths weren't translated between RMAB and SABnzbd perspectives. Fixed by:
- Adding `PathMappingConfig` to SABnzbd service constructor
- Applying `reverseTransform()` when setting category path (outgoing)
- Applying `transform()` when processing completed downloads (incoming)
- Using same `PathMapper` utility as qBittorrent for consistency
## Automatic Cleanup
@@ -223,9 +277,11 @@ interface HistoryItem {
| ID Format | NZB ID (immediate) | Torrent hash (extracted) |
| Post-Processing | Automatic (par2, extraction) | None (manual) |
| Seeding | N/A (Usenet is not P2P) | Required (tracker) |
| Categories | Path-based | Path + tag-based |
| Categories | Path-based (relative to complete_dir) | Path + tag-based |
| File Handling | Auto-extracts archives | Downloads as-is |
| Cleanup | Automatic (optional, per-indexer) | Seeding time based |
| Path Mapping | ✅ Bidirectional (same as qBit) | ✅ Bidirectional |
| Category Sync | ✅ Every download | ✅ Every download |
## Tech Stack