diff --git a/.gitignore b/.gitignore
index 12ebec1c2..6256bb889 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ sw.*
.idea/*
tailwind.compiled.css
tailwind.config.js
+dev.sh
+docs-backend-guide.md
+docs-enhancements.md
diff --git a/client/components/modals/EditSeriesInputInnerModal.vue b/client/components/modals/EditSeriesInputInnerModal.vue
index bd568321f..8d749f5ef 100644
--- a/client/components/modals/EditSeriesInputInnerModal.vue
+++ b/client/components/modals/EditSeriesInputInnerModal.vue
@@ -14,6 +14,16 @@
+ check_circle + {{ extractedMessage }} +
++ check + {{ validMessage }} +
++ error + {{ invalidMessage }} +
+http://192.168.1.1:8337 then you would put http://192.168.1.1:8337/notify.",
"MessageAsinCheck": "Ensure you are using the ASIN from the correct Audible region, not Amazon.",
+ "MessageAsinExtractedFromUrl": "ASIN extracted from URL",
"MessageAuthenticationLegacyTokenWarning": "Legacy API tokens will be removed in the future. Use API Keys instead.",
"MessageAuthenticationOIDCChangesRestart": "Restart your server after saving to apply OIDC changes.",
"MessageAuthenticationSecurityMessage": "Authentication has been improved for security. All users are required to re-login.",
@@ -938,6 +940,7 @@
"MessageUploaderItemFailed": "Failed to upload",
"MessageUploaderItemSuccess": "Successfully Uploaded!",
"MessageUploading": "Uploading...",
+ "MessageValidAsinFormat": "Valid ASIN format",
"MessageValidCronExpression": "Valid cron expression",
"MessageWatcherIsDisabledGlobally": "Watcher is disabled globally in server settings",
"MessageXLibraryIsEmpty": "{0} Library is empty!",
diff --git a/docs/objects/entities/Series.yaml b/docs/objects/entities/Series.yaml
index ef35a5b35..9f671dc90 100644
--- a/docs/objects/entities/Series.yaml
+++ b/docs/objects/entities/Series.yaml
@@ -14,6 +14,12 @@ components:
type: string
nullable: true
example: The Sword of Truth is a series of twenty one epic fantasy novels written by Terry Goodkind.
+ audibleSeriesAsin:
+ description: The Audible ASIN (Amazon Standard Identification Number) for this series. Used for metadata lookups. Will be null if not set.
+ type: string
+ nullable: true
+ pattern: '^[A-Z0-9]{10}$'
+ example: B08G9PRS1K
sequence:
description: The position in the series the book is.
type: string
@@ -45,6 +51,8 @@ components:
$ref: '#/components/schemas/seriesName'
description:
$ref: '#/components/schemas/seriesDescription'
+ audibleSeriesAsin:
+ $ref: '#/components/schemas/audibleSeriesAsin'
addedAt:
$ref: '../../schemas.yaml#/components/schemas/addedAt'
updatedAt:
@@ -73,6 +81,10 @@ components:
$ref: '#/components/schemas/seriesId'
name:
$ref: '#/components/schemas/seriesName'
+ description:
+ $ref: '#/components/schemas/seriesDescription'
+ audibleSeriesAsin:
+ $ref: '#/components/schemas/audibleSeriesAsin'
addedAt:
$ref: '../../schemas.yaml#/components/schemas/addedAt'
nameIgnorePrefix:
diff --git a/package-lock.json b/package-lock.json
index 08707893d..e07fba51d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.32.1",
+ "version": "2.33.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.32.1",
+ "version": "2.33.0",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
diff --git a/package.json b/package.json
index 3ee3fb391..3108b5170 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.32.1",
+ "version": "2.33.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js
index 21c93f332..08100596d 100644
--- a/server/controllers/SeriesController.js
+++ b/server/controllers/SeriesController.js
@@ -62,17 +62,37 @@ class SeriesController {
}
/**
- * TODO: Currently unused in the client, should check for duplicate name
+ * PATCH /api/series/:id
+ * Update series metadata (name, description, audibleSeriesAsin)
+ *
+ * TODO: should check for duplicate name
*
* @param {SeriesControllerRequest} req
* @param {Response} res
*/
async update(req, res) {
- const keysToUpdate = ['name', 'description']
+ const keysToUpdate = ['name', 'description', 'audibleSeriesAsin']
const payload = {}
for (const key of keysToUpdate) {
- if (req.body[key] !== undefined && typeof req.body[key] === 'string') {
- payload[key] = req.body[key]
+ if (req.body[key] !== undefined) {
+ const value = req.body[key]
+
+ // audibleSeriesAsin accepts null, empty string, or string
+ // Model hook will normalize (extract from URL, uppercase) and validate
+ // SAFEGUARD: null/empty values will NOT clear an existing ASIN (prevents accidental data loss)
+ if (key === 'audibleSeriesAsin') {
+ if (value === null || value === '') {
+ // Skip adding to payload if empty - existing ASIN will be preserved
+ // To explicitly clear, user must delete/recreate series or use a special endpoint
+ continue
+ } else if (typeof value === 'string') {
+ payload[key] = value // Model hook will normalize & validate
+ } else {
+ return res.status(400).send('audibleSeriesAsin must be a string or null')
+ }
+ } else if (typeof value === 'string') {
+ payload[key] = value
+ }
}
}
if (!Object.keys(payload).length) {
@@ -80,7 +100,15 @@ class SeriesController {
}
req.series.set(payload)
if (req.series.changed()) {
- await req.series.save()
+ try {
+ await req.series.save()
+ } catch (error) {
+ // Handle model-level validation errors (e.g., invalid ASIN format)
+ if (error.message?.includes('ASIN') || error.message?.includes('audibleSeriesAsin')) {
+ return res.status(400).send(error.message)
+ }
+ throw error // Re-throw unexpected errors
+ }
SocketAuthority.emitter('series_updated', req.series.toOldJSON())
}
res.json(req.series.toOldJSON())