From d5a2ea9feba67f2da7806afb29a34337d4ee8aee Mon Sep 17 00:00:00 2001 From: Quentin King Date: Sat, 3 Jan 2026 10:34:25 -0600 Subject: [PATCH] feat: auto-populate series ASIN from Audible metadata - Update Audible provider to return series ASIN in search results - Pass series ASIN through Match.vue when selecting metadata - Update Book.updateSeriesFromRequest to forward ASIN to Series model - Update Scanner to use series ASIN during quick match When using the Audible metadata provider, the series ASIN is now automatically captured and stored with the series. --- client/components/modals/item/tabs/Match.vue | 7 +++++-- server/models/Book.js | 12 ++++++++++-- server/providers/Audible.js | 6 ++++-- server/scanner/Scanner.js | 20 +++++++++++++++++++- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 4b92f6cd8..763a1bfd4 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -544,7 +544,8 @@ export default { id: `new-${Math.floor(Math.random() * 10000)}`, displayName: se.sequence ? `${se.series} #${se.sequence}` : se.series, name: se.series, - sequence: se.sequence || '' + sequence: se.sequence || '', + asin: se.asin || null } }) } @@ -580,7 +581,9 @@ export default { seriesPayload.push({ id: seriesItem.id, name: seriesItem.name, - sequence: seriesItem.sequence + sequence: seriesItem.sequence, + // Support both 'asin' (from provider) and 'audibleSeriesAsin' (from edit form) + asin: seriesItem.asin || seriesItem.audibleSeriesAsin || null }) ) updatePayload.metadata.series = seriesPayload diff --git a/server/models/Book.js b/server/models/Book.js index 96371f3a2..849c9bc2d 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -524,8 +524,16 @@ class Book extends Model { hasUpdates = true Logger.debug(`[Book] "${this.title}" Updated series "${existingSeries.name}" sequence ${seriesObjSequence}`) } + // Update series ASIN if provided and not already set + if (seriesObj.asin && !existingSeries.audibleSeriesAsin) { + existingSeries.audibleSeriesAsin = seriesObj.asin + await existingSeries.save() + const SocketAuthority = require('../SocketAuthority') + SocketAuthority.emitter('series_updated', existingSeries.toOldJSON()) + Logger.debug(`[Book] "${this.title}" Updated series "${existingSeries.name}" ASIN ${seriesObj.asin}`) + } } else { - const series = await seriesModel.findOrCreateByNameAndLibrary(seriesObj.name, libraryId) + const series = await seriesModel.findOrCreateByNameAndLibrary(seriesObj.name, libraryId, seriesObj.asin) series.bookSeries = await bookSeriesModel.create({ bookId: this.id, seriesId: series.id, sequence: seriesObjSequence }) this.series.push(series) seriesAdded.push(series) @@ -553,7 +561,7 @@ class Book extends Model { */ oldMetadataToJSON() { const authors = this.authors.map((au) => ({ id: au.id, name: au.name })) - const series = this.series.map((se) => ({ id: se.id, name: se.name, sequence: se.bookSeries.sequence })) + const series = this.series.map((se) => ({ id: se.id, name: se.name, sequence: se.bookSeries.sequence, audibleSeriesAsin: se.audibleSeriesAsin })) return { title: this.title, subtitle: this.subtitle, diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 133d3c0d8..3b25232a8 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -47,13 +47,15 @@ class Audible { if (seriesPrimary) { series.push({ series: seriesPrimary.name, - sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || '') + sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || ''), + asin: seriesPrimary.asin || null }) } if (seriesSecondary) { series.push({ series: seriesSecondary.name, - sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || '') + sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || ''), + asin: seriesSecondary.asin || null }) } diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index af4405987..e4cd4514d 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -303,17 +303,35 @@ class Scanner { Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Updated series sequence for "${existingSeries.name}" to ${seriesMatchItem.sequence} in "${libraryItem.media.title}"`) hasSeriesUpdates = true } + // Update series ASIN if provided and not already set + if (seriesMatchItem.asin && !existingSeries.audibleSeriesAsin) { + existingSeries.set({ audibleSeriesAsin: seriesMatchItem.asin }) + if (existingSeries.changed()) { + await existingSeries.save() + SocketAuthority.emitter('series_updated', existingSeries.toOldJSON()) + Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Updated series "${existingSeries.name}" with ASIN ${seriesMatchItem.asin}`) + } + } } else { let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId) if (!seriesItem) { seriesItem = await Database.seriesModel.create({ name: seriesMatchItem.series, nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series), - libraryId: libraryItem.libraryId + libraryId: libraryItem.libraryId, + audibleSeriesAsin: seriesMatchItem.asin || null }) // Update filter data Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id) SocketAuthority.emitter('series_added', seriesItem.toOldJSON()) + } else if (seriesMatchItem.asin && !seriesItem.audibleSeriesAsin) { + // Series exists but has no ASIN, update it + seriesItem.set({ audibleSeriesAsin: seriesMatchItem.asin }) + if (seriesItem.changed()) { + await seriesItem.save() + SocketAuthority.emitter('series_updated', seriesItem.toOldJSON()) + Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Updated series "${seriesItem.name}" with ASIN ${seriesMatchItem.asin}`) + } } const bookSeries = await Database.bookSeriesModel.create({ seriesId: seriesItem.id,