mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Implement file hash-based library matching and remove fuzzy ASIN matching
Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
This commit is contained in:
@@ -180,6 +180,80 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
|
||||
|
||||
logger.info(`Scan complete: ${libraryItems.length} items scanned, ${newCount} new, ${updatedCount} updated, ${skippedCount} skipped`);
|
||||
|
||||
// 4b. For Audiobookshelf: Trigger metadata match for items without ASIN
|
||||
// This ensures ASIN gets populated so items can be matched against requests
|
||||
if (backendMode === 'audiobookshelf') {
|
||||
logger.info(`Checking for Audiobookshelf items without ASIN...`);
|
||||
const { triggerABSItemMatch, getABSItem } = await import('../services/audiobookshelf/api');
|
||||
const { generateFilesHash } = await import('../utils/files-hash');
|
||||
|
||||
const itemsWithoutAsin = libraryItems.filter(item => !item.asin && item.externalId);
|
||||
|
||||
if (itemsWithoutAsin.length > 0) {
|
||||
logger.info(`Found ${itemsWithoutAsin.length} items without ASIN, attempting file hash matching...`);
|
||||
|
||||
let fileMatchCount = 0;
|
||||
let fuzzyMatchCount = 0;
|
||||
|
||||
for (const item of itemsWithoutAsin) {
|
||||
try {
|
||||
// 1. Fetch full item details to get file list
|
||||
const absItem = await getABSItem(item.externalId);
|
||||
|
||||
// 2. Extract audio filenames and generate hash
|
||||
const audioFilenames = absItem.media?.audioFiles?.map((f: any) => f.metadata?.filename).filter(Boolean) || [];
|
||||
const itemHash = generateFilesHash(audioFilenames);
|
||||
|
||||
// 3. Query database for matching downloaded request
|
||||
let matchedAsin: string | undefined = undefined;
|
||||
|
||||
if (itemHash) {
|
||||
const matchedAudiobook = await prisma.audiobook.findFirst({
|
||||
where: {
|
||||
filesHash: itemHash,
|
||||
status: 'completed',
|
||||
},
|
||||
select: {
|
||||
audibleAsin: true,
|
||||
title: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (matchedAudiobook?.audibleAsin) {
|
||||
matchedAsin = matchedAudiobook.audibleAsin;
|
||||
logger.info(
|
||||
`File hash match found for "${item.title}" → ASIN: ${matchedAsin} (from "${matchedAudiobook.title}")`
|
||||
);
|
||||
fileMatchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Trigger metadata match (with ASIN if matched, undefined if not)
|
||||
await triggerABSItemMatch(item.externalId, matchedAsin);
|
||||
|
||||
if (matchedAsin) {
|
||||
logger.info(`Triggered metadata match with ASIN ${matchedAsin} for: "${item.title}"`);
|
||||
} else {
|
||||
logger.info(`No file match found, triggering fuzzy metadata match for: "${item.title}"`);
|
||||
fuzzyMatchCount++;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to process metadata match for "${item.title}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
fuzzyMatchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Metadata match complete: ${fileMatchCount} file hash matches, ${fuzzyMatchCount} fuzzy matches (ASIN population is async)`
|
||||
);
|
||||
} else {
|
||||
logger.info(`All items have ASIN, no metadata match needed`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Remove stale records from plex_library (items no longer in the actual library)
|
||||
// This ensures the database is a fresh snapshot of the library state
|
||||
logger.info(`Checking for stale library records...`);
|
||||
@@ -445,15 +519,8 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
|
||||
|
||||
matchedCount++;
|
||||
|
||||
// Trigger metadata match for Audiobookshelf items (only for our downloaded requests)
|
||||
if (backendMode === 'audiobookshelf') {
|
||||
const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID
|
||||
const asin = audiobook.audibleAsin || undefined;
|
||||
const matchInfo = asin ? ` with ASIN ${asin}` : '';
|
||||
logger.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`);
|
||||
const { triggerABSItemMatch } = await import('../services/audiobookshelf/api');
|
||||
await triggerABSItemMatch(itemId, asin);
|
||||
}
|
||||
// Note: Audiobookshelf metadata matching is handled in the file hash phase above
|
||||
// Items without ASIN get file-hash-matched ASIN, items with ASIN already have correct metadata
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
|
||||
Reference in New Issue
Block a user