diff --git a/documentation/features/bookdate.md b/documentation/features/bookdate.md index 88a46c0..03fc762 100644 --- a/documentation/features/bookdate.md +++ b/documentation/features/bookdate.md @@ -449,6 +449,26 @@ Personalized audiobook discovery using OpenAI/Claude APIs. Admin configures AI p - User with autoApproveRequests=true auto-approves (status: 'pending', sends approved notification, triggers search) - User with autoApproveRequests=null checks global setting +**10. Empty ASIN Matching All Library Books** +- Issue: All AI recommendations incorrectly matched to first library book, causing empty recommendation list +- User Experience: "BookDate returns 0 recommendations. Logs show AI generated 20, but all matched to 'Murder Your Employer'" +- Impact: Critical - BookDate completely non-functional, all recommendations filtered out +- Cause: Empty ASIN in database query matched every record in library + - AI generates recommendations without ASINs (title/author only) + - `isInLibrary()` calls `findPlexMatch()` with `asin: ""` + - Database query: `{ plexGuid: { contains: "" } }` returns all 29 library books + - Code checks: `plexGuid.includes("")` returns true for first book + - All 20 recommendations matched to same book → filtered out as "already in library" + - SQL behavior: `WHERE plexGuid LIKE '%%'` matches all rows +- Fix: Add guard clause to return null if ASIN is empty or falsy + - Early return prevents database query with empty string + - First `isInLibrary()` call (no ASIN) → Returns false immediately + - Recommendation matches to Audnexus → Gets real ASIN + - Second `isInLibrary()` call (with ASIN) → Correctly checks for exact match + - Only books actually in library get filtered out +- Files updated: `src/lib/utils/audiobook-matcher.ts:44-61` +- Documentation: `documentation/fixes/asin-matching-fix.md` - Phase 3 section added + ## Related - Full requirements: [features/bookdate-prd.md](bookdate-prd.md) diff --git a/documentation/fixes/asin-matching-fix.md b/documentation/fixes/asin-matching-fix.md index b985cd0..28d2011 100644 --- a/documentation/fixes/asin-matching-fix.md +++ b/documentation/fixes/asin-matching-fix.md @@ -511,4 +511,82 @@ This fix resolves the critical ASIN matching issue for Audiobookshelf by impleme - **Preserves critical functionality:** Fuzzy matching kept for Prowlarr torrent ranking - **Improves performance:** O(1) indexed lookups replace O(n²) string comparisons -**Status:** ✅ Both phases complete and production-ready +**Status:** ✅ All phases complete and production-ready + +## Phase 3: Empty ASIN Guard (January 2026) + +**Status:** ✅ Implemented +**Date:** 2026-01-28 +**Issue:** Empty ASIN causing all library books to match AI recommendations + +### Problem Statement + +**BookDate Recommendations Returning Empty:** +1. AI generates 20 recommendations (without ASINs) +2. BookDate calls `isInLibrary()` to filter out books already in library +3. `isInLibrary()` calls `findPlexMatch()` with empty ASIN (`asin: ""`) +4. Database query: `{ plexGuid: { contains: "" } }` matches ALL records (29 books) +5. Code checks: `plexGuid.includes("")` returns true for first book +6. All 20 recommendations incorrectly matched to first library book ("Murder Your Employer") +7. All recommendations filtered out → User sees 0 recommendations + +### Root Cause + +**Empty string matching bug in database query:** +- SQL: `WHERE plexGuid LIKE '%' + '' + '%'` matches every record +- JavaScript: `anyString.includes("")` always returns true +- Prisma: `{ contains: "" }` returns all rows in table + +### Solution + +Add guard clause at start of `findPlexMatch()` to return `null` immediately if ASIN is empty or falsy. + +**Implementation:** +```typescript +export async function findPlexMatch(audiobook: AudiobookMatchInput) { + // Early return if no ASIN provided (prevents empty string matching all records) + if (!audiobook.asin || audiobook.asin.trim() === '') { + logger.debug('Matcher result', { + MATCHER: { + input: { title: audiobook.title, author: audiobook.author, asin: audiobook.asin }, + candidatesFound: 0, + matchType: 'no_asin_provided', + matched: false, + result: null, + } + }); + return null; + } + + // Existing ASIN query logic... +} +``` + +### Expected Behavior + +**BookDate Flow (After Phase 3):** +1. AI generates 20 recommendations (no ASINs) +2. First `isInLibrary()` call with empty ASIN → Returns `false` immediately ✅ +3. Recommendation matches to Audnexus → Gets real ASIN +4. Second `isInLibrary()` call with real ASIN → Correctly checks for exact match ✅ +5. Only books actually in library get filtered out ✅ +6. User sees 10-15 new recommendations ✅ + +### Files Modified + +**Matching Logic:** +- ✅ `src/lib/utils/audiobook-matcher.ts:44-61` - Added empty ASIN guard clause + +**Documentation:** +- ✅ `documentation/fixes/asin-matching-fix.md` - Added Phase 3 section +- ✅ `documentation/features/bookdate.md` - Added to Fixed Issues + +### Benefits + +1. **Fixes critical bug:** Empty ASIN no longer matches all library books +2. **Prevents false positives:** Only exact ASIN matches are considered matches +3. **Aligns with design:** ASIN-only matcher requires valid ASIN to match +4. **Single-line fix:** Minimal code change with maximum impact +5. **No breaking changes:** All existing functionality preserved + +**Status:** ✅ All three phases complete and production-ready diff --git a/src/lib/utils/audiobook-matcher.ts b/src/lib/utils/audiobook-matcher.ts index 15fb39f..61ad68e 100644 --- a/src/lib/utils/audiobook-matcher.ts +++ b/src/lib/utils/audiobook-matcher.ts @@ -41,6 +41,25 @@ export interface AudiobookMatchResult { export async function findPlexMatch( audiobook: AudiobookMatchInput ): Promise { + // Early return if no ASIN provided (prevents empty string matching all records) + if (!audiobook.asin || audiobook.asin.trim() === '') { + logger.debug('Matcher result', { + MATCHER: { + input: { + title: audiobook.title, + author: audiobook.author, + narrator: audiobook.narrator || null, + asin: audiobook.asin, + }, + candidatesFound: 0, + matchType: 'no_asin_provided', + matched: false, + result: null, + } + }); + return null; + } + // Query plex_library directly by ASIN (indexed O(1) lookup) // Check both dedicated asin field and plexGuid for backward compatibility const plexBooks = await prisma.plexLibrary.findMany({