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) + }) + }) +})