From 067d3f6394f3c9c9824ae7de8d45594494340062 Mon Sep 17 00:00:00 2001 From: wakamex Date: Mon, 29 Jun 2026 19:40:45 -0400 Subject: [PATCH] Also exclude disabled tracks from book duration on rescan BookScanner recomputed media.duration by summing all audio files, so a rescan that detected any file change would re-count a previously disabled (excluded) track and re-inflate the duration. Use the same Book.updateDuration() (included tracks only) so both the manage-tracks path and rescans stay consistent. Add a Book model unit test for updateDuration(). Co-Authored-By: Claude Opus 4.8 (1M context) --- server/scanner/BookScanner.js | 8 ++--- test/server/models/Book.test.js | 62 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 test/server/models/Book.test.js diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 8ae616fe8..ade7260d9 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -136,12 +136,8 @@ class BookScanner { media.audioFiles = AudioFileScanner.runSmartTrackOrder(existingLibraryItem.relPath, media.audioFiles) - media.duration = 0 - media.audioFiles.forEach((af) => { - if (!isNaN(af.duration)) { - media.duration += af.duration - } - }) + // Recompute duration from included (non-excluded) tracks so disabled tracks don't get re-counted on rescan + media.updateDuration() media.changed('audioFiles', true) } diff --git a/test/server/models/Book.test.js b/test/server/models/Book.test.js new file mode 100644 index 000000000..162dbe92c --- /dev/null +++ b/test/server/models/Book.test.js @@ -0,0 +1,62 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') + +const Database = require('../../../server/Database') + +describe('Book', () => { + beforeEach(async () => { + global.ServerSettings = {} + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + }) + + afterEach(async () => { + await Database.sequelize.sync({ force: true }) + }) + + describe('updateDuration', () => { + it('sums only the included (non-excluded) audio files', () => { + const book = Database.bookModel.build({ + title: 'Test Book', + duration: 0, + audioFiles: [ + { ino: '1', duration: 100, exclude: false }, + { ino: '2', duration: 50, exclude: true }, + { ino: '3', duration: 25, exclude: false } + ] + }) + + const changed = book.updateDuration() + + expect(book.duration).to.equal(125) // 100 + 25, the excluded 50 is ignored + expect(changed).to.be.true + }) + + it('returns false and leaves duration unchanged when already correct', () => { + const book = Database.bookModel.build({ + title: 'Test Book', + duration: 100, + audioFiles: [{ ino: '1', duration: 100, exclude: false }] + }) + + expect(book.updateDuration()).to.be.false + expect(book.duration).to.equal(100) + }) + + it('ignores audio files with a non-numeric duration', () => { + const book = Database.bookModel.build({ + title: 'Test Book', + duration: 0, + audioFiles: [ + { ino: '1', duration: 100, exclude: false }, + { ino: '2', duration: NaN, exclude: false } + ] + }) + + book.updateDuration() + + expect(book.duration).to.equal(100) + }) + }) +})