mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-06 07:59:43 +00:00
Merge d5a2ea9feb into 1d0b7e383a
This commit is contained in:
commit
53106ce268
17 changed files with 592 additions and 29 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,37 @@ const { DataTypes, Model, where, fn, col, literal } = require('sequelize')
|
|||
|
||||
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils/index')
|
||||
|
||||
/**
|
||||
* Normalize and validate Audible Series ASIN.
|
||||
* - null/undefined/empty → null
|
||||
* - Extracts ASIN from Audible series URLs
|
||||
* - Validates 10 alphanumeric chars
|
||||
* - Uppercases
|
||||
*
|
||||
* @param {*} value
|
||||
* @returns {string|null} Normalized ASIN or null
|
||||
* @throws {Error} If value is invalid format
|
||||
*/
|
||||
function normalizeAudibleSeriesAsin(value) {
|
||||
if (value == null) return null
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('audibleSeriesAsin must be a string or null')
|
||||
}
|
||||
|
||||
const raw = value.trim()
|
||||
if (!raw) return null
|
||||
|
||||
// Extract ASIN from Audible series URL if provided
|
||||
// e.g., https://www.audible.com/series/Harry-Potter/B0182NWM9I or /series/B0182NWM9I
|
||||
const urlMatch = raw.match(/\/series\/(?:[^/]+\/)?([A-Z0-9]{10})(?:[/?#]|$)/i)
|
||||
const candidate = (urlMatch ? urlMatch[1] : raw).toUpperCase()
|
||||
|
||||
if (!/^[A-Z0-9]{10}$/.test(candidate)) {
|
||||
throw new Error('Invalid ASIN format. Must be exactly 10 alphanumeric characters.')
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
class Series extends Model {
|
||||
constructor(values, options) {
|
||||
super(values, options)
|
||||
|
|
@ -14,6 +45,8 @@ class Series extends Model {
|
|||
this.nameIgnorePrefix
|
||||
/** @type {string} */
|
||||
this.description
|
||||
/** @type {string} */
|
||||
this.audibleSeriesAsin
|
||||
/** @type {UUIDV4} */
|
||||
this.libraryId
|
||||
/** @type {Date} */
|
||||
|
|
@ -70,15 +103,26 @@ class Series extends Model {
|
|||
*
|
||||
* @param {string} seriesName
|
||||
* @param {string} libraryId
|
||||
* @param {string} [asin] - Optional Audible series ASIN
|
||||
* @returns {Promise<Series>}
|
||||
*/
|
||||
static async findOrCreateByNameAndLibrary(seriesName, libraryId) {
|
||||
static async findOrCreateByNameAndLibrary(seriesName, libraryId, asin = null) {
|
||||
const series = await this.getByNameAndLibrary(seriesName, libraryId)
|
||||
if (series) return series
|
||||
if (series) {
|
||||
// Update ASIN if provided and not already set
|
||||
if (asin && !series.audibleSeriesAsin) {
|
||||
series.audibleSeriesAsin = asin
|
||||
await series.save()
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
SocketAuthority.emitter('series_updated', series.toOldJSON())
|
||||
}
|
||||
return series
|
||||
}
|
||||
return this.create({
|
||||
name: seriesName,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
|
||||
libraryId
|
||||
libraryId,
|
||||
audibleSeriesAsin: asin || null
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +140,8 @@ class Series extends Model {
|
|||
},
|
||||
name: DataTypes.STRING,
|
||||
nameIgnorePrefix: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
description: DataTypes.TEXT,
|
||||
audibleSeriesAsin: DataTypes.STRING
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
|
|
@ -129,6 +174,14 @@ class Series extends Model {
|
|||
}
|
||||
)
|
||||
|
||||
// Hook to normalize/validate audibleSeriesAsin before save
|
||||
// This ensures ALL routes get the same validation
|
||||
Series.beforeValidate((series) => {
|
||||
if (series.changed('audibleSeriesAsin')) {
|
||||
series.audibleSeriesAsin = normalizeAudibleSeriesAsin(series.audibleSeriesAsin)
|
||||
}
|
||||
})
|
||||
|
||||
const { library } = sequelize.models
|
||||
library.hasMany(Series, {
|
||||
onDelete: 'CASCADE'
|
||||
|
|
@ -171,6 +224,7 @@ class Series extends Model {
|
|||
name: this.name,
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
|
||||
description: this.description,
|
||||
audibleSeriesAsin: this.audibleSeriesAsin,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf(),
|
||||
libraryId: this.libraryId
|
||||
|
|
@ -187,3 +241,4 @@ class Series extends Model {
|
|||
}
|
||||
|
||||
module.exports = Series
|
||||
module.exports.normalizeAudibleSeriesAsin = normalizeAudibleSeriesAsin
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue