Enrich audiobook metadata from Audnexus

Query Audnexus (Audible) to backfill missing metadata during manual imports and file organization. Adds getAudibleService imports and calls to fetch audiobook details by ASIN, then backfills series, seriesPart, seriesAsin, year (from releaseDate) and narrator when missing and updates the DB. Failures are non-fatal and logged; logs were added to surface enrichment steps. Also uses the resolved series/seriesPart when building organization metadata.
This commit is contained in:
kikootwo
2026-03-04 12:19:37 -05:00
parent cbf02d3e24
commit d0ce485bdc
2 changed files with 152 additions and 3 deletions
+43
View File
@@ -12,6 +12,7 @@ import { prisma } from '@/lib/db';
import { getJobQueueService } from '@/lib/services/job-queue.service';
import { RMABLogger } from '@/lib/utils/logger';
import { AUDIO_EXTENSIONS } from '@/lib/constants/audio-formats';
import { getAudibleService } from '@/lib/integrations/audible.service';
const logger = RMABLogger.create('API.Admin.ManualImport');
@@ -174,6 +175,48 @@ export async function POST(request: NextRequest) {
);
}
// Enrich missing series/year data from Audnexus (mirrors request-creator.service.ts)
if (audiobook.audibleAsin && (!audiobook.series || !audiobook.year)) {
try {
const audibleService = getAudibleService();
const audnexusData = await audibleService.getAudiobookDetails(audiobook.audibleAsin);
if (audnexusData) {
const updates: Record<string, any> = {};
if (!audiobook.series && audnexusData.series) {
updates.series = audnexusData.series;
}
if (!audiobook.seriesPart && audnexusData.seriesPart) {
updates.seriesPart = audnexusData.seriesPart;
}
if (!audiobook.seriesAsin && audnexusData.seriesAsin) {
updates.seriesAsin = audnexusData.seriesAsin;
}
if (!audiobook.year && audnexusData.releaseDate) {
const releaseYear = new Date(audnexusData.releaseDate).getFullYear();
if (!isNaN(releaseYear)) {
updates.year = releaseYear;
}
}
if (!audiobook.narrator && audnexusData.narrator) {
updates.narrator = audnexusData.narrator;
}
if (Object.keys(updates).length > 0) {
await prisma.audiobook.update({
where: { id: audiobook.id },
data: updates,
});
logger.info(`Enriched audiobook metadata from Audnexus for ASIN ${audiobook.audibleAsin}`, updates);
}
}
} catch (error) {
// Non-fatal: series enrichment failure should never block the import
logger.warn(`Failed to enrich metadata from Audnexus for ASIN ${audiobook.audibleAsin}: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Check for existing requests
const existingRequest = await prisma.request.findFirst({
where: {
+109 -3
View File
@@ -15,6 +15,7 @@ import { PathMapper, PathMappingConfig } from '../utils/path-mapper';
import { generateFilesHash } from '../utils/files-hash';
import { fixEpubForKindle, cleanupFixedEpub } from '../utils/epub-fixer';
import { removeEmptyParentDirectories } from '../utils/cleanup-helpers';
import { getAudibleService } from '../integrations/audible.service';
/**
* Process organize files job
@@ -118,7 +119,62 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
}
}
logger.info(`Final metadata for path organization: year=${year || 'null'}, narrator=${narrator || 'null'}`)
// Enrich missing series data from Audnexus (safety net for records created without series)
let series = audiobook.series || undefined;
let seriesPart = audiobook.seriesPart || undefined;
if (audiobook.audibleAsin && !series) {
try {
logger.info(`Missing series data, fetching from Audnexus for ASIN: ${audiobook.audibleAsin}`);
const audibleService = getAudibleService();
const audnexusData = await audibleService.getAudiobookDetails(audiobook.audibleAsin);
if (audnexusData) {
const updates: Record<string, any> = {};
if (audnexusData.series) {
series = audnexusData.series;
updates.series = series;
logger.info(`Got series "${series}" from Audnexus`);
}
if (audnexusData.seriesPart) {
seriesPart = audnexusData.seriesPart;
updates.seriesPart = seriesPart;
logger.info(`Got seriesPart "${seriesPart}" from Audnexus`);
}
if (audnexusData.seriesAsin) {
updates.seriesAsin = audnexusData.seriesAsin;
}
// Also backfill year/narrator if still missing
if (!year && audnexusData.releaseDate) {
const releaseYear = new Date(audnexusData.releaseDate).getFullYear();
if (!isNaN(releaseYear)) {
year = releaseYear;
updates.year = year;
logger.info(`Got year ${year} from Audnexus`);
}
}
if (!narrator && audnexusData.narrator) {
narrator = audnexusData.narrator;
updates.narrator = narrator;
logger.info(`Got narrator "${narrator}" from Audnexus`);
}
if (Object.keys(updates).length > 0) {
await prisma.audiobook.update({
where: { id: audiobookId },
data: updates,
});
logger.info(`Updated audiobook record with Audnexus metadata`);
}
}
} catch (error) {
// Non-fatal: missing series won't block organization, just degrades path quality
logger.warn(`Failed to fetch Audnexus data for ASIN ${audiobook.audibleAsin}: ${error instanceof Error ? error.message : String(error)}`);
}
}
logger.info(`Final metadata for path organization: year=${year || 'null'}, narrator=${narrator || 'null'}, series=${series || 'null'}, seriesPart=${seriesPart || 'null'}`);
// Get file organizer (reads media_dir from database config)
const organizer = await getFileOrganizer();
@@ -151,8 +207,8 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
coverArtUrl: audiobook.coverArtUrl || undefined,
asin: audiobook.audibleAsin || undefined,
year,
series: audiobook.series || undefined,
seriesPart: audiobook.seriesPart || undefined,
series,
seriesPart,
},
template,
jobId ? { jobId, context: 'FileOrganizer' } : undefined,
@@ -545,6 +601,56 @@ async function processEbookOrganization(
}
}
// Enrich missing series data from Audnexus (safety net for records created without series)
if (book.audibleAsin && !series) {
try {
logger.info(`Missing series data for ebook, fetching from Audnexus for ASIN: ${book.audibleAsin}`);
const audibleService = getAudibleService();
const audnexusData = await audibleService.getAudiobookDetails(book.audibleAsin);
if (audnexusData) {
const updates: Record<string, any> = {};
if (audnexusData.series) {
series = audnexusData.series;
updates.series = series;
logger.info(`Got series "${series}" from Audnexus`);
}
if (audnexusData.seriesPart) {
seriesPart = audnexusData.seriesPart;
updates.seriesPart = seriesPart;
logger.info(`Got seriesPart "${seriesPart}" from Audnexus`);
}
if (audnexusData.seriesAsin) {
updates.seriesAsin = audnexusData.seriesAsin;
}
if (!year && audnexusData.releaseDate) {
const releaseYear = new Date(audnexusData.releaseDate).getFullYear();
if (!isNaN(releaseYear)) {
year = releaseYear;
updates.year = year;
logger.info(`Got year ${year} from Audnexus`);
}
}
if (!narrator && audnexusData.narrator) {
narrator = audnexusData.narrator;
updates.narrator = narrator;
logger.info(`Got narrator "${narrator}" from Audnexus`);
}
if (Object.keys(updates).length > 0) {
await prisma.audiobook.update({
where: { id: audiobookId },
data: updates,
});
logger.info(`Updated book record with Audnexus metadata`);
}
}
} catch (error) {
logger.warn(`Failed to fetch Audnexus data for ASIN ${book.audibleAsin}: ${error instanceof Error ? error.message : String(error)}`);
}
}
logger.info(`Final metadata for path organization: year=${year || 'null'}, narrator=${narrator || 'null'}, series=${series || 'null'}, seriesPart=${seriesPart || 'null'}`);
// Check if this is an indexer download (needs to keep source for seeding)