This commit is contained in:
wakamex 2026-06-29 20:57:34 -04:00 committed by GitHub
commit bf18bb923a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 6 deletions

View file

@ -507,6 +507,8 @@ class LibraryItemController {
req.libraryItem.media.audioFiles = updatedAudioFiles req.libraryItem.media.audioFiles = updatedAudioFiles
req.libraryItem.media.changed('audioFiles', true) req.libraryItem.media.changed('audioFiles', true)
// Recompute duration so disabled (excluded) tracks no longer count toward the total
req.libraryItem.media.updateDuration()
await req.libraryItem.media.save() await req.libraryItem.media.save()
SocketAuthority.libraryItemEmitter('item_updated', req.libraryItem) SocketAuthority.libraryItemEmitter('item_updated', req.libraryItem)

View file

@ -261,6 +261,19 @@ class Book extends Model {
return this.audioFiles.filter((af) => !af.exclude) return this.audioFiles.filter((af) => !af.exclude)
} }
/**
* Recalculate the book duration from the included (non-excluded) audio files.
* Sets `this.duration` and returns whether it changed.
*
* @returns {boolean}
*/
updateDuration() {
const newDuration = this.includedAudioFiles.reduce((total, af) => total + (isNaN(af.duration) ? 0 : Number(af.duration)), 0)
if (this.duration === newDuration) return false
this.duration = newDuration
return true
}
get hasMediaFiles() { get hasMediaFiles() {
return !!this.hasAudioTracks || !!this.ebookFile return !!this.hasAudioTracks || !!this.ebookFile
} }

View file

@ -136,12 +136,8 @@ class BookScanner {
media.audioFiles = AudioFileScanner.runSmartTrackOrder(existingLibraryItem.relPath, media.audioFiles) media.audioFiles = AudioFileScanner.runSmartTrackOrder(existingLibraryItem.relPath, media.audioFiles)
media.duration = 0 // Recompute duration from included (non-excluded) tracks so disabled tracks don't get re-counted on rescan
media.audioFiles.forEach((af) => { media.updateDuration()
if (!isNaN(af.duration)) {
media.duration += af.duration
}
})
media.changed('audioFiles', true) media.changed('audioFiles', true)
} }

View file

@ -8,6 +8,7 @@ const LibraryItemController = require('../../../server/controllers/LibraryItemCo
const ApiCacheManager = require('../../../server/managers/ApiCacheManager') const ApiCacheManager = require('../../../server/managers/ApiCacheManager')
const Auth = require('../../../server/Auth') const Auth = require('../../../server/Auth')
const Logger = require('../../../server/Logger') const Logger = require('../../../server/Logger')
const SocketAuthority = require('../../../server/SocketAuthority')
describe('LibraryItemController', () => { describe('LibraryItemController', () => {
/** @type {ApiRouter} */ /** @type {ApiRouter} */
@ -299,4 +300,41 @@ describe('LibraryItemController', () => {
expect(fakeRes.sendStatus.calledWith(403)).to.be.true expect(fakeRes.sendStatus.calledWith(403)).to.be.true
}) })
}) })
describe('updateTracks', () => {
it('recomputes book duration to exclude disabled audio tracks', async () => {
const library = await Database.libraryModel.create({ name: 'Tracks Library', mediaType: 'book' })
const libraryFolder = await Database.libraryFolderModel.create({ path: '/tracks', libraryId: library.id })
// Book with two audio tracks (100s + 50s) and a stored duration of 150s
const audioFiles = [
{ index: 1, ino: 'file-1', duration: 100, exclude: false, metadata: { filename: 'track1.mp3', size: 1000 } },
{ index: 2, ino: 'file-2', duration: 50, exclude: false, metadata: { filename: 'track2.mp3', size: 1000 } }
]
const book = await Database.bookModel.create({ title: 'Two Track Book', duration: 150, audioFiles, tags: [], narrators: [], genres: [], chapters: [] })
const libraryItem = await Database.libraryItemModel.create({ libraryFiles: [], mediaId: book.id, mediaType: 'book', libraryId: library.id, libraryFolderId: libraryFolder.id })
sinon.stub(SocketAuthority, 'libraryItemEmitter')
const expandedLibraryItem = await Database.libraryItemModel.getExpandedById(libraryItem.id)
const fakeReq = {
libraryItem: expandedLibraryItem,
body: {
orderedFileData: [
{ ino: 'file-1', exclude: false },
{ ino: 'file-2', exclude: true }
]
}
}
const fakeRes = { json: sinon.spy(), sendStatus: sinon.spy() }
await LibraryItemController.updateTracks.bind(apiRouter)(fakeReq, fakeRes)
expect(fakeRes.json.calledOnce).to.be.true
// Duration should now reflect only the enabled track (100s), not the original 150s
const updatedBook = await Database.bookModel.findByPk(book.id)
expect(updatedBook.duration).to.equal(100)
})
})
}) })

View file

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