mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-06 17:31:41 +00:00
Merge 067d3f6394 into 6f03467f35
This commit is contained in:
commit
bf18bb923a
5 changed files with 117 additions and 6 deletions
|
|
@ -507,6 +507,8 @@ class LibraryItemController {
|
|||
|
||||
req.libraryItem.media.audioFiles = updatedAudioFiles
|
||||
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()
|
||||
|
||||
SocketAuthority.libraryItemEmitter('item_updated', req.libraryItem)
|
||||
|
|
|
|||
|
|
@ -261,6 +261,19 @@ class Book extends Model {
|
|||
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() {
|
||||
return !!this.hasAudioTracks || !!this.ebookFile
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const LibraryItemController = require('../../../server/controllers/LibraryItemCo
|
|||
const ApiCacheManager = require('../../../server/managers/ApiCacheManager')
|
||||
const Auth = require('../../../server/Auth')
|
||||
const Logger = require('../../../server/Logger')
|
||||
const SocketAuthority = require('../../../server/SocketAuthority')
|
||||
|
||||
describe('LibraryItemController', () => {
|
||||
/** @type {ApiRouter} */
|
||||
|
|
@ -299,4 +300,41 @@ describe('LibraryItemController', () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
62
test/server/models/Book.test.js
Normal file
62
test/server/models/Book.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue