diff --git a/src/app/api/admin/manual-import/route.ts b/src/app/api/admin/manual-import/route.ts index d2aa482..dfbb6d0 100644 --- a/src/app/api/admin/manual-import/route.ts +++ b/src/app/api/admin/manual-import/route.ts @@ -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 = {}; + + 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: { diff --git a/src/lib/processors/organize-files.processor.ts b/src/lib/processors/organize-files.processor.ts index 3e6252b..99c3d56 100644 --- a/src/lib/processors/organize-files.processor.ts +++ b/src/lib/processors/organize-files.processor.ts @@ -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 = {}; + + 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 = {}; + + 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)