Add works table and ASIN deduping

Add persistent cross-ASIN "works" mapping and client-side deduplication to improve library matching. Introduces a Prisma migration and models (Work, WorkAsin) plus src/lib/services/works.service for persisting dedup groups, seeding ASINs at request time, and sibling lookup. Adds a deduplication utility (deduplicate-audiobooks) that normalizes titles/narrators, compares durations, and returns grouping metadata; API routes (search, author, series) now deduplicate results before enrichment and fire-and-forget persist groups. Adds sibling-ASIN expansion into audiobook matcher and expands getAvailableAsins accordingly. Extracts runtime parsing into a shared parse-runtime util and updates audible scrapers/services to use it. Includes unit tests for dedup logic and works service and updates test Prisma mocks.
This commit is contained in:
kikootwo
2026-03-03 13:31:46 -05:00
parent ff80d995c5
commit 610873af6b
15 changed files with 1446 additions and 32 deletions
+12 -2
View File
@@ -6,6 +6,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAudibleService } from '@/lib/integrations/audible.service';
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
import { deduplicateAndCollectGroups } from '@/lib/utils/deduplicate-audiobooks';
import { persistDedupGroups } from '@/lib/services/works.service';
import { getCurrentUser } from '@/lib/middleware/auth';
import { RMABLogger } from '@/lib/utils/logger';
@@ -53,9 +55,17 @@ export async function GET(
const audibleService = getAudibleService();
const result = await audibleService.searchByAuthorAsin(authorName.trim(), asin, page);
// Deduplicate before enrichment to avoid wasted DB queries on duplicate entries
const { books: dedupedBooks, groups } = deduplicateAndCollectGroups(result.books);
// Fire-and-forget: persist dedup groups to works table for cross-ASIN matching
if (groups.length > 0) {
persistDedupGroups(groups).catch(() => {});
}
// Enrich with library availability and request status
const userId = currentUser.sub || undefined;
const enrichedBooks = await enrichAudiobooksWithMatches(result.books, userId);
const enrichedBooks = await enrichAudiobooksWithMatches(dedupedBooks, userId);
logger.info(`Author books complete: "${authorName}" → ${enrichedBooks.length} books (page ${page})`);
@@ -64,7 +74,7 @@ export async function GET(
books: enrichedBooks,
authorName: authorName.trim(),
authorAsin: asin,
totalBooks: result.totalResults || enrichedBooks.length,
totalBooks: enrichedBooks.length,
hasMore: result.hasMore,
page: result.page,
});