Add ASIN/ISBN fields to library and improve matching

Introduces `asin` and `isbn` fields to the PlexLibrary schema and database, with migration and indexing for fast lookups. Updates scan and recently-added processors to persist ASIN/ISBN from both Plex and Audiobookshelf backends. Enhances matching logic to prioritize exact ASIN matches using the new fields, improving match accuracy for Audiobookshelf users. Also includes minor improvements: fixes cover art handling for cached thumbnails, adds download URL validation in Prowlarr and qBittorrent integrations, and updates documentation to reflect these changes.
This commit is contained in:
kikootwo
2025-12-22 14:20:22 -05:00
parent a3381cba31
commit 1cefa437b7
12 changed files with 469 additions and 16 deletions
+32 -3
View File
@@ -84,6 +84,8 @@ export async function findPlexMatch(
plexGuid: true,
title: true,
author: true,
asin: true, // Include ASIN field for direct matching
isbn: true, // Include ISBN field for additional matching
},
take: 20,
});
@@ -109,10 +111,27 @@ export async function findPlexMatch(
return null;
}
// PRIORITY 1: Check for EXACT ASIN match in plexGuid
// PRIORITY 1a: Check for EXACT ASIN match in dedicated field (works for all backends)
for (const plexBook of plexBooks) {
if (plexBook.asin && plexBook.asin.toLowerCase() === audiobook.asin.toLowerCase()) {
matchResult.matchType = 'asin_exact_field';
matchResult.matched = true;
matchResult.result = {
plexGuid: plexBook.plexGuid,
plexTitle: plexBook.title,
plexAuthor: plexBook.author,
asin: plexBook.asin,
confidence: 100,
};
if (DEBUG_ENABLED) console.log(JSON.stringify({ MATCHER: matchResult }));
return plexBook;
}
}
// PRIORITY 1b: Check for ASIN in plexGuid (backward compatibility for Plex)
for (const plexBook of plexBooks) {
if (plexBook.plexGuid && plexBook.plexGuid.includes(audiobook.asin)) {
matchResult.matchType = 'asin_exact';
matchResult.matchType = 'asin_exact_guid';
matchResult.matched = true;
matchResult.result = {
plexGuid: plexBook.plexGuid,
@@ -125,10 +144,20 @@ export async function findPlexMatch(
}
}
// FILTER OUT candidates with wrong ASINs in plexGuid
// FILTER OUT candidates with wrong ASINs (check both dedicated field and plexGuid)
const ASIN_PATTERN = /[A-Z0-9]{10}/g;
const rejectedAsins: string[] = [];
const validCandidates = plexBooks.filter((plexBook) => {
// Check dedicated ASIN field first (more reliable)
if (plexBook.asin) {
if (plexBook.asin.toLowerCase() !== audiobook.asin.toLowerCase()) {
rejectedAsins.push(plexBook.asin);
return false; // Wrong ASIN in dedicated field - reject
}
return true; // Correct ASIN in dedicated field - keep
}
// Fall back to checking plexGuid for legacy Plex data
if (!plexBook.plexGuid) return true;
const asinsInGuid = plexBook.plexGuid.match(ASIN_PATTERN);
if (!asinsInGuid || asinsInGuid.length === 0) return true;
+20 -6
View File
@@ -393,18 +393,32 @@ export class FileOrganizer {
}
/**
* Download cover art from URL
* Download cover art from URL or copy from local cache
*/
private async downloadCoverArt(url: string, targetDir: string): Promise<void> {
const targetPath = path.join(targetDir, 'cover.jpg');
try {
const response = await axios.get(url, {
responseType: 'arraybuffer',
timeout: 30000,
});
// Check if this is a cached thumbnail (local file)
if (url.startsWith('/api/cache/thumbnails/')) {
// Extract filename from the API path
const filename = url.replace('/api/cache/thumbnails/', '');
const cachedPath = path.join('/app/cache/thumbnails', filename);
await fs.writeFile(targetPath, response.data);
// Copy from local cache instead of downloading
const coverData = await fs.readFile(cachedPath);
await fs.writeFile(targetPath, coverData, { mode: 0o644 });
console.log(`[FileOrganizer] Copied cover art from cache: ${filename}`);
} else {
// Download from external URL (e.g., Audible CDN)
const response = await axios.get(url, {
responseType: 'arraybuffer',
timeout: 30000,
});
await fs.writeFile(targetPath, response.data);
console.log(`[FileOrganizer] Downloaded cover art from URL`);
}
} catch (error) {
console.error('[FileOrganizer] Failed to download cover art:', error);
throw error;