From e944b2a2f5d2792cb2d9412708d87815f0e8088c Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 21 May 2026 17:08:39 -0700 Subject: [PATCH 1/5] Add unique UUID to access and refresh tokens --- server/auth/TokenManager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 42ca262bb..5933209c7 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -1,4 +1,5 @@ const { Op } = require('sequelize') +const uuid = require('uuid') const Database = require('../Database') const Logger = require('../Logger') @@ -115,6 +116,7 @@ class TokenManager { const payload = { userId: user.id, username: user.username, + jti: uuid.v4(), type: 'access' } const options = { @@ -138,6 +140,7 @@ class TokenManager { const payload = { userId: user.id, username: user.username, + jti: uuid.v4(), type: 'refresh' } const options = { From c97b36e11c51230c4ec3537990b3fbdad72ad44d Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Thu, 21 May 2026 21:06:17 -0700 Subject: [PATCH 2/5] Add `ignoreDuplicates` for bookAuthor when renaming to respect unique index --- server/controllers/AuthorController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 80471ec47..8c2e80aec 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -149,7 +149,7 @@ class AuthorController { }) if (libraryItems.length) { await Database.bookAuthorModel.removeByIds(req.author.id) // Remove all old BookAuthor - await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate) // Create all new BookAuthor + await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate, { ignoreDuplicates: true }) // Create all new unique BookAuthor for (const libraryItem of libraryItems) { await libraryItem.saveMetadataFile() } From 1bad2d907294d5eeacbc29e868e2e25613da1176 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 27 May 2026 17:33:14 -0500 Subject: [PATCH 3/5] Cleanup abmetadata file parsing & fix server crash #5268 #4287 #5142 --- server/scanner/AbsMetadataFileScanner.js | 29 ++-- server/scanner/BookScanner.js | 3 + server/scanner/PodcastScanner.js | 3 + .../utils/generators/abmetadataGenerator.js | 143 +++++++++++++++--- 4 files changed, 145 insertions(+), 33 deletions(-) diff --git a/server/scanner/AbsMetadataFileScanner.js b/server/scanner/AbsMetadataFileScanner.js index e554dfb44..0ea34b1bb 100644 --- a/server/scanner/AbsMetadataFileScanner.js +++ b/server/scanner/AbsMetadataFileScanner.js @@ -5,15 +5,15 @@ const { LogLevel } = require('../utils/constants') const abmetadataGenerator = require('../utils/generators/abmetadataGenerator') class AbsMetadataFileScanner { - constructor() { } + constructor() {} /** * Check for metadata.json file and set book metadata - * - * @param {import('./LibraryScan')} libraryScan - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {Object} bookMetadata - * @param {string} [existingLibraryItemId] + * + * @param {import('./LibraryScan')} libraryScan + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {Object} bookMetadata + * @param {string} [existingLibraryItemId] */ async scanBookMetadataFile(libraryScan, libraryItemData, bookMetadata, existingLibraryItemId = null) { const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile @@ -32,7 +32,8 @@ class AbsMetadataFileScanner { if (metadataText) { libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) - const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + const abMetadata = abmetadataGenerator.parseJson(metadataText, 'book') || {} + for (const key in abMetadata) { // TODO: When to override with null or empty arrays? if (abMetadata[key] === undefined || abMetadata[key] === null) continue @@ -48,11 +49,11 @@ class AbsMetadataFileScanner { /** * Check for metadata.json file and set podcast metadata - * - * @param {import('./LibraryScan')} libraryScan - * @param {import('./LibraryItemScanData')} libraryItemData - * @param {Object} podcastMetadata - * @param {string} [existingLibraryItemId] + * + * @param {import('./LibraryScan')} libraryScan + * @param {import('./LibraryItemScanData')} libraryItemData + * @param {Object} podcastMetadata + * @param {string} [existingLibraryItemId] */ async scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId = null) { const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile @@ -71,7 +72,7 @@ class AbsMetadataFileScanner { if (metadataText) { libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`) - const abMetadata = abmetadataGenerator.parseJson(metadataText) || {} + const abMetadata = abmetadataGenerator.parseJson(metadataText, 'podcast') || {} for (const key in abMetadata) { if (abMetadata[key] === undefined || abMetadata[key] === null) continue if (key === 'tags' && !abMetadata.tags?.length) continue @@ -81,4 +82,4 @@ class AbsMetadataFileScanner { } } } -module.exports = new AbsMetadataFileScanner() \ No newline at end of file +module.exports = new AbsMetadataFileScanner() diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index ac93c6379..8ae616fe8 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -825,6 +825,9 @@ class BookScanner { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + /** + * Keys must match abmetadataGenerator.js + */ const jsonObject = { tags: libraryItem.media.tags || [], chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [], diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 6ab2d332c..f3241d536 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -425,6 +425,9 @@ class PodcastScanner { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) + /** + * Keys must match abmetadataGenerator.js + */ const jsonObject = { tags: libraryItem.media.tags || [], title: libraryItem.media.title, diff --git a/server/utils/generators/abmetadataGenerator.js b/server/utils/generators/abmetadataGenerator.js index 01f853288..d0d278752 100644 --- a/server/utils/generators/abmetadataGenerator.js +++ b/server/utils/generators/abmetadataGenerator.js @@ -1,7 +1,51 @@ const Logger = require('../../Logger') const parseSeriesString = require('../parsers/parseSeriesString') -function parseJsonMetadataText(text) { +const mediaTypeKeys = { + book: { + tags: 'stringArray', + title: 'string', + subtitle: 'string', + authors: 'stringArray', + narrators: 'stringArray', + series: 'stringArray', + genres: 'stringArray', + publishedYear: 'string', + publishedDate: 'string', + publisher: 'string', + description: 'string', + isbn: 'string', + asin: 'string', + language: 'string', + explicit: 'boolean', + abridged: 'boolean' + }, + podcast: { + tags: 'stringArray', + title: 'string', + author: 'string', + description: 'string', + releaseDate: 'string', + genres: 'stringArray', + feedURL: 'string', + imageURL: 'string', + itunesPageURL: 'string', + itunesId: 'string', + itunesArtistId: 'string', + asin: 'string', + language: 'string', + explicit: 'boolean', + podcastType: 'string' + } +} + +/** + * + * @param {string} text + * @param {"book" | "podcast"} mediaType + * @returns {Object} + */ +function parseJsonMetadataText(text, mediaType) { try { const abmetadataData = JSON.parse(text) @@ -19,28 +63,41 @@ function parseJsonMetadataText(text) { } delete abmetadataData.metadata - if (abmetadataData.series?.length) { - abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))] - abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(series)) + const expectedKeys = mediaTypeKeys[mediaType] + if (!expectedKeys) { + Logger.error(`[abmetadataGenerator] Invalid media type "${mediaType}"`) + return null } - // clean tags & remove dupes - if (abmetadataData.tags?.length) { - abmetadataData.tags = [...new Set(abmetadataData.tags.map((t) => t?.trim()).filter((t) => t))] + + const validated = {} + for (const key in expectedKeys) { + const expectedType = expectedKeys[key] + if (!(key in abmetadataData)) continue + + const validatedValue = validateMetadataValue(key, abmetadataData[key], expectedType) + if (validatedValue !== undefined) { + validated[key] = validatedValue + } } - if (abmetadataData.chapters?.length) { - abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title) + + if (validated.series?.length) { + validated.series = validated.series.map((series) => parseSeriesString.parse(series)).filter(Boolean) } - // clean remove dupes - if (abmetadataData.authors?.length) { - abmetadataData.authors = [...new Set(abmetadataData.authors.map((t) => t?.trim()).filter((t) => t))] + + if (mediaType === 'book' && 'chapters' in abmetadataData) { + if (abmetadataData.chapters === null) { + validated.chapters = [] + } else if (Array.isArray(abmetadataData.chapters)) { + const cleanedChapters = cleanChaptersArray(abmetadataData.chapters, validated.title ?? abmetadataData.title) + if (cleanedChapters) { + validated.chapters = cleanedChapters + } + } else { + Logger.warn(`[abmetadataGenerator] Invalid metadata key "chapters" expected array, got ${typeof abmetadataData.chapters}`) + } } - if (abmetadataData.narrators?.length) { - abmetadataData.narrators = [...new Set(abmetadataData.narrators.map((t) => t?.trim()).filter((t) => t))] - } - if (abmetadataData.genres?.length) { - abmetadataData.genres = [...new Set(abmetadataData.genres.map((t) => t?.trim()).filter((t) => t))] - } - return abmetadataData + + return validated } catch (error) { Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error) return null @@ -48,6 +105,54 @@ function parseJsonMetadataText(text) { } module.exports.parseJson = parseJsonMetadataText +/** + * @param {string} key + * @param {*} value + * @param {string} expectedType + * @returns {*|undefined} undefined excludes the key + */ +function validateMetadataValue(key, value, expectedType) { + if (expectedType === 'string') { + if (value === null) return null + if (typeof value === 'number') return String(value) + if (typeof value === 'string') return value + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string, got ${typeof value}`) + return undefined + } + + if (expectedType === 'boolean') { + if (value === null) return null + if (typeof value === 'boolean') return value + if (typeof value === 'string') { + const lower = value.toLowerCase() + if (lower === 'true') return true + if (lower === 'false') return false + } + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected boolean, got ${typeof value}`) + return undefined + } + + // Filter empty strings and deduplicate + if (expectedType === 'stringArray') { + if (value === null) return [] + if (!Array.isArray(value)) { + Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string array, got ${typeof value}`) + return undefined + } + + const cleanedArray = value.filter((t) => typeof t === 'string') + return [...new Set(cleanedArray.map((t) => t.trim()).filter((t) => t))] + } + + Logger.warn(`[abmetadataGenerator] Unknown expected type "${expectedType}" for key "${key}"`) + return undefined +} + +/** + * @param {Object[]} chaptersArray + * @param {string} mediaTitle + * @returns {Object[]} + */ function cleanChaptersArray(chaptersArray, mediaTitle) { const chapters = [] let index = 0 From 499b52b4dd29b5d91ff142069339e2913bc6cd20 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 28 May 2026 14:49:49 -0500 Subject: [PATCH 4/5] Update Sequelize where query for User username/email case insensitive --- server/models/User.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/server/models/User.js b/server/models/User.js index 936efde13..f380f8e4f 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -352,11 +352,7 @@ class User extends Model { if (cachedUser) return cachedUser const user = await this.findOne({ - where: { - username: { - [sequelize.Op.like]: username - } - }, + where: sequelize.where(sequelize.fn('lower', sequelize.col('username')), username.toLowerCase()), include: this.sequelize.models.mediaProgress }) @@ -377,11 +373,7 @@ class User extends Model { if (cachedUser) return cachedUser const user = await this.findOne({ - where: { - email: { - [sequelize.Op.like]: email - } - }, + where: sequelize.where(sequelize.fn('lower', sequelize.col('email')), email.toLowerCase()), include: this.sequelize.models.mediaProgress }) From aacdcc47ec0b6fa778b84627c79deb7ee0cecd70 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 28 May 2026 15:22:55 -0500 Subject: [PATCH 5/5] Version bump v2.35.1 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index ba071852b..502ba4b03 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 71d64f60a..0ec070b32 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.35.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 312675d71..9cee15b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index a0b340244..a7755fd66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.35.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js",