mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-24 21:04:33 +02:00
Add:Chapter editor, lookup chapters via audnexus, chapters table on audiobook landing page #435
This commit is contained in:
parent
095f49824e
commit
cc1181b301
16 changed files with 613 additions and 108 deletions
|
@ -359,7 +359,7 @@ class LibraryItemController {
|
|||
})
|
||||
}
|
||||
|
||||
// POST: api/items/:id/audio-metadata
|
||||
// GET: api/items/:id/audio-metadata
|
||||
async updateAudioFileMetadata(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
|
||||
|
@ -375,6 +375,36 @@ class LibraryItemController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/items/:id/chapters
|
||||
async updateMediaChapters(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.error(`[LibraryItemController] User attempted to update chapters with invalid permissions`, req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
|
||||
Logger.error(`[LibraryItemController] Invalid library item`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const chapters = req.body.chapters || []
|
||||
if (!chapters.length) {
|
||||
Logger.error(`[LibraryItemController] Invalid payload`)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
this.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
updated: wasUpdated
|
||||
})
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
|
|
@ -225,6 +225,15 @@ class MiscController {
|
|||
res.json(author)
|
||||
}
|
||||
|
||||
async findChapters(req, res) {
|
||||
var asin = req.query.asin
|
||||
var chapterData = await this.bookFinder.findChapters(asin)
|
||||
if (!chapterData) {
|
||||
return res.json({ error: 'Chapters not found' })
|
||||
}
|
||||
res.json(chapterData)
|
||||
}
|
||||
|
||||
authorize(req, res) {
|
||||
if (!req.user) {
|
||||
Logger.error('Invalid user in authorize')
|
||||
|
|
|
@ -3,6 +3,7 @@ const LibGen = require('../providers/LibGen')
|
|||
const GoogleBooks = require('../providers/GoogleBooks')
|
||||
const Audible = require('../providers/Audible')
|
||||
const iTunes = require('../providers/iTunes')
|
||||
const Audnexus = require('../providers/Audnexus')
|
||||
const Logger = require('../Logger')
|
||||
const { levenshteinDistance } = require('../utils/index')
|
||||
|
||||
|
@ -13,6 +14,7 @@ class BookFinder {
|
|||
this.googleBooks = new GoogleBooks()
|
||||
this.audible = new Audible()
|
||||
this.iTunesApi = new iTunes()
|
||||
this.audnexus = new Audnexus()
|
||||
|
||||
this.verbose = false
|
||||
}
|
||||
|
@ -226,5 +228,9 @@ class BookFinder {
|
|||
})
|
||||
return covers
|
||||
}
|
||||
|
||||
findChapters(asin) {
|
||||
return this.audnexus.getChaptersByASIN(asin)
|
||||
}
|
||||
}
|
||||
module.exports = BookFinder
|
|
@ -153,6 +153,30 @@ class Book {
|
|||
return hasUpdates
|
||||
}
|
||||
|
||||
updateChapters(chapters) {
|
||||
var hasUpdates = this.chapters.length !== chapters.length
|
||||
if (hasUpdates) {
|
||||
this.chapters = chapters.map(ch => ({
|
||||
id: ch.id,
|
||||
start: ch.start,
|
||||
end: ch.end,
|
||||
title: ch.title
|
||||
}))
|
||||
} else {
|
||||
for (let i = 0; i < this.chapters.length; i++) {
|
||||
const currChapter = this.chapters[i]
|
||||
const newChapter = chapters[i]
|
||||
if (!hasUpdates && (currChapter.title !== newChapter.title || currChapter.start !== newChapter.start || currChapter.end !== newChapter.end)) {
|
||||
hasUpdates = true
|
||||
}
|
||||
this.chapters[i].title = newChapter.title
|
||||
this.chapters[i].start = newChapter.start
|
||||
this.chapters[i].end = newChapter.end
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
updateCover(coverPath) {
|
||||
coverPath = coverPath.replace(/\\/g, '/')
|
||||
if (this.coverPath === coverPath) return false
|
||||
|
@ -381,19 +405,27 @@ class Book {
|
|||
// If audio file has chapters use chapters
|
||||
if (file.chapters && file.chapters.length) {
|
||||
file.chapters.forEach((chapter) => {
|
||||
var chapterDuration = chapter.end - chapter.start
|
||||
if (chapterDuration > 0) {
|
||||
var title = `Chapter ${currChapterId}`
|
||||
if (chapter.title) {
|
||||
title += ` (${chapter.title})`
|
||||
if (chapter.start > this.duration) {
|
||||
Logger.warn(`[Book] Invalid chapter start time > duration`)
|
||||
} else {
|
||||
var chapterAlreadyExists = this.chapters.find(ch => ch.start === chapter.start)
|
||||
if (!chapterAlreadyExists) {
|
||||
var chapterDuration = chapter.end - chapter.start
|
||||
if (chapterDuration > 0) {
|
||||
var title = `Chapter ${currChapterId}`
|
||||
if (chapter.title) {
|
||||
title += ` (${chapter.title})`
|
||||
}
|
||||
var endTime = Math.min(this.duration, currStartTime + chapterDuration)
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
end: endTime,
|
||||
title
|
||||
})
|
||||
currStartTime += chapterDuration
|
||||
}
|
||||
}
|
||||
this.chapters.push({
|
||||
id: currChapterId++,
|
||||
start: currStartTime,
|
||||
end: currStartTime + chapterDuration,
|
||||
title
|
||||
})
|
||||
currStartTime += chapterDuration
|
||||
}
|
||||
})
|
||||
} else if (file.duration) {
|
||||
|
|
|
@ -45,5 +45,15 @@ class Audnexus {
|
|||
name: author.name
|
||||
}
|
||||
}
|
||||
|
||||
async getChaptersByASIN(asin) {
|
||||
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}`)
|
||||
return axios.get(`${this.baseUrl}/books/${asin}/chapters`).then((res) => {
|
||||
return res.data
|
||||
}).catch((error) => {
|
||||
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = Audnexus
|
|
@ -92,8 +92,9 @@ class ApiRouter {
|
|||
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||
this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this))
|
||||
this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this))
|
||||
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
|
||||
this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this)) // Root only
|
||||
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
|
||||
this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))
|
||||
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||
|
@ -204,6 +205,7 @@ class ApiRouter {
|
|||
this.router.get('/search/books', MiscController.findBooks.bind(this))
|
||||
this.router.get('/search/podcast', MiscController.findPodcasts.bind(this))
|
||||
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
||||
this.router.get('/search/chapters', MiscController.findChapters.bind(this))
|
||||
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue