mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-30 23:59:39 +00:00
New data model edit tracks page, match, quick match, clean out old files
This commit is contained in:
parent
be1e1e7ba0
commit
7d66f1eec9
68 changed files with 354 additions and 1529 deletions
|
|
@ -8,7 +8,6 @@ const Logger = require('./Logger')
|
|||
const { isObject } = require('./utils/index')
|
||||
const { parsePodcastRssFeedXml } = require('./utils/podcastUtils')
|
||||
|
||||
const BookController = require('./controllers/BookController')
|
||||
const LibraryController = require('./controllers/LibraryController')
|
||||
const UserController = require('./controllers/UserController')
|
||||
const CollectionController = require('./controllers/CollectionController')
|
||||
|
|
@ -74,6 +73,8 @@ class ApiController {
|
|||
//
|
||||
// Item Routes
|
||||
//
|
||||
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
||||
|
||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||
this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this))
|
||||
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this))
|
||||
|
|
@ -83,27 +84,13 @@ class ApiController {
|
|||
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
|
||||
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
||||
this.router.get('/items/:id/stream', LibraryItemController.middleware.bind(this), LibraryItemController.openStream.bind(this))
|
||||
this.router.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.bind(this))
|
||||
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||
|
||||
//
|
||||
// Book Routes
|
||||
//
|
||||
this.router.get('/books', BookController.findAll.bind(this))
|
||||
this.router.get('/books/:id', BookController.findOne.bind(this))
|
||||
this.router.patch('/books/:id', BookController.update.bind(this))
|
||||
this.router.delete('/books/:id', BookController.delete.bind(this))
|
||||
|
||||
this.router.delete('/books/all', BookController.deleteAll.bind(this))
|
||||
this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this))
|
||||
this.router.get('/books/:id/stream', BookController.openStream.bind(this))
|
||||
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
|
||||
this.router.get('/books/:id/cover', BookController.getCover.bind(this))
|
||||
this.router.patch('/books/:id/coverfile', BookController.updateCoverFromFile.bind(this))
|
||||
this.router.post('/books/:id/match', BookController.match.bind(this))
|
||||
|
||||
//
|
||||
// User Routes
|
||||
//
|
||||
|
|
|
|||
|
|
@ -218,8 +218,6 @@ class Server {
|
|||
})
|
||||
|
||||
// Client dynamic routes
|
||||
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
|
||||
app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
|
||||
app.get('/item/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/item/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
|
|
|
|||
|
|
@ -1,279 +0,0 @@
|
|||
const Logger = require('../Logger')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
|
||||
class BookController {
|
||||
constructor() { }
|
||||
|
||||
findAll(req, res) {
|
||||
var audiobooks = []
|
||||
|
||||
if (req.query.q) {
|
||||
audiobooks = this.db.audiobooks.filter(ab => {
|
||||
return ab.isSearchMatch(req.query.q)
|
||||
}).map(ab => ab.toJSONMinified())
|
||||
} else {
|
||||
audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
|
||||
}
|
||||
res.json(audiobooks)
|
||||
}
|
||||
|
||||
findOne(req, res) {
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this audiobooks library
|
||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
res.json(audiobook.toJSONExpanded())
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to update without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
// Book has cover and update is removing cover then purge cache
|
||||
if (audiobook.cover && req.body.book && (req.body.book.cover === '' || req.body.book.cover === null)) {
|
||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
||||
}
|
||||
|
||||
var hasUpdates = audiobook.update(req.body)
|
||||
if (hasUpdates) {
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
}
|
||||
res.json(audiobook.toJSON())
|
||||
}
|
||||
|
||||
async delete(req, res) {
|
||||
if (!req.user.canDelete) {
|
||||
Logger.warn('User attempted to delete without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
await this.handleDeleteAudiobook(audiobook)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// DELETE: api/books/all
|
||||
async deleteAll(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
Logger.warn('User other than root attempted to delete all library items', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info('Removing all Library Items')
|
||||
var success = await this.db.recreateLibraryItemsDb()
|
||||
if (success) res.sendStatus(200)
|
||||
else res.sendStatus(500)
|
||||
}
|
||||
|
||||
|
||||
// POST: api/books/batch/delete
|
||||
async batchDelete(req, res) {
|
||||
if (!req.user.canDelete) {
|
||||
Logger.warn('User attempted to delete without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var { audiobookIds } = req.body
|
||||
if (!audiobookIds || !audiobookIds.length) {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var audiobooksToDelete = this.db.audiobooks.filter(ab => audiobookIds.includes(ab.id))
|
||||
if (!audiobooksToDelete.length) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
for (let i = 0; i < audiobooksToDelete.length; i++) {
|
||||
Logger.info(`[ApiController] Deleting Audiobook "${audiobooksToDelete[i].title}"`)
|
||||
await this.handleDeleteAudiobook(audiobooksToDelete[i])
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/books/batch/update
|
||||
async batchUpdate(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to batch update without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var updatePayloads = req.body
|
||||
if (!updatePayloads || !updatePayloads.length) {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var audiobooksUpdated = 0
|
||||
var audiobooks = updatePayloads.map((up) => {
|
||||
var audiobookUpdates = up.updates
|
||||
var ab = this.db.audiobooks.find(_ab => _ab.id === up.id)
|
||||
if (!ab) return null
|
||||
var hasUpdated = ab.update(audiobookUpdates)
|
||||
if (!hasUpdated) return null
|
||||
audiobooksUpdated++
|
||||
return ab
|
||||
}).filter(ab => ab)
|
||||
|
||||
if (audiobooksUpdated) {
|
||||
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
|
||||
for (let i = 0; i < audiobooks.length; i++) {
|
||||
await this.db.updateAudiobook(audiobooks[i])
|
||||
this.emitter('audiobook_updated', audiobooks[i].toJSONExpanded())
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
updates: audiobooksUpdated
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/books/batch/get
|
||||
async batchGet(req, res) {
|
||||
var bookIds = req.body.books || []
|
||||
if (!bookIds.length) {
|
||||
return res.status(403).send('Invalid payload')
|
||||
}
|
||||
var audiobooks = this.db.audiobooks.filter(ab => bookIds.includes(ab.id)).map((ab) => ab.toJSONExpanded())
|
||||
res.json(audiobooks)
|
||||
}
|
||||
|
||||
// PATCH: api/books/:id/tracks
|
||||
async updateTracks(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to update audiotracks without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
var orderedFileData = req.body.orderedFileData
|
||||
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
||||
audiobook.updateAudioTracks(orderedFileData)
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
res.json(audiobook.toJSON())
|
||||
}
|
||||
|
||||
// GET: api/books/:id/stream
|
||||
openStream(req, res) {
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
this.streamManager.openStreamApiRequest(res, req.user, audiobook)
|
||||
}
|
||||
|
||||
// POST: api/books/:id/cover
|
||||
async uploadCover(req, res) {
|
||||
if (!req.user.canUpload || !req.user.canUpdate) {
|
||||
Logger.warn('User attempted to upload a cover without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var audiobookId = req.params.id
|
||||
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
|
||||
if (!audiobook) {
|
||||
return res.status(404).send('Audiobook not found')
|
||||
}
|
||||
|
||||
var result = null
|
||||
if (req.body && req.body.url) {
|
||||
Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`)
|
||||
result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url)
|
||||
} else if (req.files && req.files.cover) {
|
||||
Logger.debug(`[ApiController] Handling uploaded cover`)
|
||||
var coverFile = req.files.cover
|
||||
result = await this.coverController.uploadCover(audiobook, coverFile)
|
||||
} else {
|
||||
return res.status(400).send('Invalid request no file or url')
|
||||
}
|
||||
|
||||
if (result && result.error) {
|
||||
return res.status(400).send(result.error)
|
||||
} else if (!result || !result.cover) {
|
||||
return res.status(500).send('Unknown error occurred')
|
||||
}
|
||||
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
res.json({
|
||||
success: true,
|
||||
cover: result.cover
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH api/books/:id/coverfile
|
||||
async updateCoverFromFile(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to update without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
var coverFile = req.body
|
||||
var updated = await audiobook.setCoverFromFile(coverFile)
|
||||
|
||||
if (updated) {
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
}
|
||||
|
||||
if (updated) res.status(200).send('Cover updated successfully')
|
||||
else res.status(200).send('No update was made to cover')
|
||||
}
|
||||
|
||||
// GET api/books/:id/cover
|
||||
async getCover(req, res) {
|
||||
let { query: { width, height, format }, params: { id } } = req
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === id)
|
||||
if (!audiobook || !audiobook.book.cover) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this audiobooks library
|
||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
// Temp fix for books without a full cover path
|
||||
if (audiobook.book.cover && !audiobook.book.coverFullPath) {
|
||||
var isFixed = audiobook.fixFullCoverPath()
|
||||
if (!isFixed) {
|
||||
Logger.warn(`[BookController] Failed to fix full cover path "${audiobook.book.cover}" for "${audiobook.book.title}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
await this.db.updateEntity('audiobook', audiobook)
|
||||
}
|
||||
|
||||
const options = {
|
||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
}
|
||||
return this.cacheManager.handleCoverCache(res, audiobook, options)
|
||||
}
|
||||
|
||||
// POST api/books/:id/match
|
||||
async match(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to match without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this audiobooks library
|
||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var options = req.body || {}
|
||||
var matchResult = await this.scanner.quickMatchBook(audiobook, options)
|
||||
res.json(matchResult)
|
||||
}
|
||||
}
|
||||
module.exports = new BookController()
|
||||
|
|
@ -230,7 +230,7 @@ class LibraryController {
|
|||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||
}
|
||||
|
||||
// api/libraries/:id/books/personalized
|
||||
// api/libraries/:id/personalized
|
||||
async getLibraryUserPersonalized(req, res) {
|
||||
var libraryItems = req.libraryItems
|
||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
|
|
|
|||
|
|
@ -138,6 +138,26 @@ class LibraryItemController {
|
|||
this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem)
|
||||
}
|
||||
|
||||
// POST api/items/:id/match
|
||||
async match(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
|
||||
var options = req.body || {}
|
||||
var matchResult = await this.scanner.quickMatchBook(libraryItem, options)
|
||||
res.json(matchResult)
|
||||
}
|
||||
|
||||
// PATCH: api/items/:id/tracks
|
||||
async updateTracks(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
var orderedFileData = req.body.orderedFileData
|
||||
Logger.info(`Updating item tracks called ${libraryItem.id}`)
|
||||
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
// POST: api/items/batch/delete
|
||||
async batchDelete(req, res) {
|
||||
if (!req.user.canDelete) {
|
||||
|
|
@ -202,6 +222,18 @@ class LibraryItemController {
|
|||
res.json(libraryItems)
|
||||
}
|
||||
|
||||
// DELETE: api/items/all
|
||||
async deleteAll(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
Logger.warn('User other than root attempted to delete all library items', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info('Removing all Library Items')
|
||||
var success = await this.db.recreateLibraryItemsDb()
|
||||
if (success) res.sendStatus(200)
|
||||
else res.sendStatus(500)
|
||||
}
|
||||
|
||||
|
||||
middleware(req, res, next) {
|
||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/audiobook/:id/reset-progress
|
||||
async resetAudiobookProgress(req, res) {
|
||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
||||
if (!audiobook) {
|
||||
return res.status(404).send('Audiobook not found')
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
req.user.resetAudiobookProgress(audiobook)
|
||||
req.user.resetAudiobookProgress(libraryItem)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
|
||||
var userAudiobookData = req.user.audiobooks[audiobook.id]
|
||||
var userAudiobookData = req.user.audiobooks[libraryItem.id]
|
||||
if (userAudiobookData) {
|
||||
this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: audiobook.id, data: userAudiobookData })
|
||||
this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: libraryItem.id, data: userAudiobookData })
|
||||
}
|
||||
|
||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
|
|
@ -36,11 +36,11 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/audiobook/:id
|
||||
async updateAudiobookData(req, res) {
|
||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
||||
if (!audiobook) {
|
||||
return res.status(404).send('Audiobook not found')
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
var wasUpdated = req.user.updateAudiobookData(audiobook.id, req.body)
|
||||
var wasUpdated = req.user.updateAudiobookData(libraryItem.id, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
|
|
@ -57,9 +57,9 @@ class MeController {
|
|||
|
||||
var shouldUpdate = false
|
||||
userAbDataPayloads.forEach((userAbData) => {
|
||||
var audiobook = this.db.audiobooks.find(ab => ab.id === userAbData.audiobookId)
|
||||
if (audiobook) {
|
||||
var wasUpdated = req.user.updateAudiobookData(audiobook.id, userAbData)
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === userAbData.audiobookId)
|
||||
if (libraryItem) {
|
||||
var wasUpdated = req.user.updateAudiobookData(libraryItem.id, userAbData)
|
||||
if (wasUpdated) shouldUpdate = true
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -250,11 +250,11 @@ class User {
|
|||
return madeUpdates
|
||||
}
|
||||
|
||||
resetAudiobookProgress(audiobook) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobook.id]) {
|
||||
resetAudiobookProgress(libraryItem) {
|
||||
if (!this.audiobooks || !this.audiobooks[libraryItem.id]) {
|
||||
return false
|
||||
}
|
||||
return this.updateAudiobookData(audiobook.id, {
|
||||
return this.updateAudiobookData(libraryItem.id, {
|
||||
progress: 0,
|
||||
currentTime: 0,
|
||||
isRead: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const Path = require('path')
|
||||
const Logger = require('../../Logger')
|
||||
const BookMetadata = require('../metadata/BookMetadata')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
|
|
@ -124,6 +125,27 @@ class Book {
|
|||
return hasUpdates
|
||||
}
|
||||
|
||||
updateAudioTracks(orderedFileData) {
|
||||
var index = 1
|
||||
this.audioFiles = orderedFileData.map((fileData) => {
|
||||
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
||||
audioFile.manuallyVerified = true
|
||||
audioFile.invalid = false
|
||||
audioFile.error = null
|
||||
if (fileData.exclude !== undefined) {
|
||||
audioFile.exclude = !!fileData.exclude
|
||||
}
|
||||
if (audioFile.exclude) {
|
||||
audioFile.index = -1
|
||||
} else {
|
||||
audioFile.index = index++
|
||||
}
|
||||
return audioFile
|
||||
})
|
||||
|
||||
this.rebuildTracks()
|
||||
}
|
||||
|
||||
updateCover(coverPath) {
|
||||
coverPath = coverPath.replace(/\\/g, '/')
|
||||
if (this.coverPath === coverPath) return false
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class LibraryFile {
|
|||
this.metadata = fileMetadata
|
||||
this.addedAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
console.log('Library file set from path', path, 'rel path', relPath)
|
||||
}
|
||||
}
|
||||
module.exports = LibraryFile
|
||||
|
|
@ -135,7 +135,7 @@ class BookMetadata {
|
|||
this.title = scanMediaData.title || null
|
||||
this.subtitle = scanMediaData.subtitle || null
|
||||
this.narrators = []
|
||||
this.publishYear = scanMediaData.publishYear || null
|
||||
this.publishedYear = scanMediaData.publishedYear || null
|
||||
this.description = scanMediaData.description || null
|
||||
this.isbn = scanMediaData.isbn || null
|
||||
this.asin = scanMediaData.asin || null
|
||||
|
|
@ -166,7 +166,7 @@ class BookMetadata {
|
|||
},
|
||||
{
|
||||
tag: 'tagDate',
|
||||
key: 'publishYear'
|
||||
key: 'publishedYear'
|
||||
},
|
||||
{
|
||||
tag: 'tagSubtitle',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Audible {
|
|||
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
||||
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
||||
publisher: publisher_name,
|
||||
publishYear: release_date ? release_date.split('-')[0] : null,
|
||||
publishedYear: release_date ? release_date.split('-')[0] : null,
|
||||
description: stripHtml(publisher_summary).result,
|
||||
cover: this.getBestImageLink(product_images),
|
||||
asin,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class GoogleBooks {
|
|||
subtitle: subtitle || null,
|
||||
author: authors ? authors.join(', ') : null,
|
||||
publisher,
|
||||
publishYear: publisherDate ? publisherDate.split('-')[0] : null,
|
||||
publishedYear: publisherDate ? publisherDate.split('-')[0] : null,
|
||||
description,
|
||||
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
|
||||
genres: categories ? categories.join(', ') : null,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class OpenLibrary {
|
|||
return {
|
||||
title: doc.title,
|
||||
author: doc.author_name ? doc.author_name.join(', ') : null,
|
||||
publishYear: this.parsePublishYear(doc, worksData),
|
||||
publishedYear: this.parsePublishYear(doc, worksData),
|
||||
edition: doc.cover_edition_key,
|
||||
cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null,
|
||||
...worksData
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class iTunes {
|
|||
title: data.collectionName,
|
||||
author: data.artistName,
|
||||
description: stripHtml(data.description || '').result,
|
||||
publishYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||
genres: data.primaryGenreName ? [data.primaryGenreName] : [],
|
||||
cover: this.getCoverArtwork(data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ class AudioFileScanner {
|
|||
constructor() { }
|
||||
|
||||
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
||||
const { title, author, series, publishYear } = mediaMetadataFromScan
|
||||
const { title, author, series, publishedYear } = mediaMetadataFromScan
|
||||
const { filename, path } = audioLibraryFile.metadata
|
||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||
|
||||
// Remove title, author, series, and publishYear from filename if there
|
||||
// Remove title, author, series, and publishedYear from filename if there
|
||||
if (title) partbasename = partbasename.replace(title, '')
|
||||
if (author) partbasename = partbasename.replace(author, '')
|
||||
if (series) partbasename = partbasename.replace(series, '')
|
||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
||||
if (publishedYear) partbasename = partbasename.replace(publishedYear)
|
||||
|
||||
// Look for disc number
|
||||
var discNumber = null
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class Scanner {
|
|||
}
|
||||
}
|
||||
}
|
||||
console.log('Finished library item scan', libraryItem.hasMediaFiles, hasUpdated)
|
||||
|
||||
if (!libraryItem.hasMediaFiles) { // Library Item is invalid
|
||||
libraryItem.setInvalid()
|
||||
hasUpdated = true
|
||||
|
|
@ -671,10 +671,10 @@ class Scanner {
|
|||
}
|
||||
}
|
||||
|
||||
async quickMatchBook(audiobook, options = {}) {
|
||||
async quickMatchBook(libraryItem, options = {}) {
|
||||
var provider = options.provider || 'google'
|
||||
var searchTitle = options.title || audiobook.book._title
|
||||
var searchAuthor = options.author || audiobook.book._author
|
||||
var searchTitle = options.title || libraryItem.media.metadata.title
|
||||
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
||||
|
||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
|
||||
if (!results.length) {
|
||||
|
|
@ -686,40 +686,70 @@ class Scanner {
|
|||
|
||||
// Update cover if not set OR overrideCover flag
|
||||
var hasUpdated = false
|
||||
if (matchData.cover && (!audiobook.book.cover || options.overrideCover)) {
|
||||
Logger.debug(`[BookController] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverController.downloadCoverFromUrl(audiobook, matchData.cover)
|
||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||
var coverResult = await this.coverController.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||
Logger.warn(`[BookController] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||
} else {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update book details if not set OR overrideDetails flag
|
||||
const detailKeysToUpdate = ['title', 'subtitle', 'author', 'narrator', 'publisher', 'publishYear', 'series', 'volumeNumber', 'asin', 'isbn']
|
||||
// Update media metadata if not set OR overrideDetails flag
|
||||
const detailKeysToUpdate = ['title', 'subtitle', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
|
||||
const updatePayload = {}
|
||||
for (const key in matchData) {
|
||||
if (matchData[key] && detailKeysToUpdate.includes(key) && (!audiobook.book[key] || options.overrideDetails)) {
|
||||
updatePayload[key] = matchData[key]
|
||||
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
||||
if (key === 'narrator') {
|
||||
if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
|
||||
updatePayload.narrators = [matchData[key]]
|
||||
}
|
||||
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||
updatePayload[key] = matchData[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add or set author if not set
|
||||
if (matchData.author && !libraryItem.media.metadata.authorName) {
|
||||
var author = this.db.authors.find(au => au.checkNameEquals(matchData.author))
|
||||
if (!author) {
|
||||
author = new Author()
|
||||
author.setData({ name: matchData.author })
|
||||
await this.db.insertEntity('author', author)
|
||||
this.emitter('author_added', author)
|
||||
}
|
||||
updatePayload.authors = [author.toJSONMinimal()]
|
||||
}
|
||||
|
||||
// Add or set series if not set
|
||||
if (matchData.series && !libraryItem.media.metadata.seriesName) {
|
||||
var seriesItem = this.db.series.find(au => au.checkNameEquals(matchData.series))
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData({ name: matchData.series })
|
||||
await this.db.insertEntity('series', seriesItem)
|
||||
this.emitter('series_added', seriesItem)
|
||||
}
|
||||
updatePayload.series = [seriesItem.toJSONMinimal(matchData.volumeNumber)]
|
||||
}
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
Logger.debug('[BookController] Updating details', updatePayload)
|
||||
if (audiobook.update({ book: updatePayload })) {
|
||||
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||
if (libraryItem.media.update({ metadata: updatePayload })) {
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateAudiobook(audiobook)
|
||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
return {
|
||||
updated: hasUpdated,
|
||||
audiobook: audiobook.toJSONExpanded()
|
||||
libraryItem: libraryItem.toJSONExpanded()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const bookKeyMap = {
|
|||
subtitle: 'subtitle',
|
||||
author: 'authorFL',
|
||||
narrator: 'narratorFL',
|
||||
publishYear: 'publishYear',
|
||||
publishedYear: 'publishedYear',
|
||||
publisher: 'publisher',
|
||||
description: 'description',
|
||||
isbn: 'isbn',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ function makeSeriesFromOldAb({ series, volumeNumber }) {
|
|||
function getRelativePath(srcPath, basePath) {
|
||||
srcPath = srcPath.replace(/\\/g, '/')
|
||||
basePath = basePath.replace(/\\/g, '/')
|
||||
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1)
|
||||
return srcPath.replace(basePath, '')
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +155,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||
|
||||
var bookEntity = new Book()
|
||||
var bookMetadata = new BookMetadata(audiobook.book)
|
||||
bookMetadata.publishedYear = audiobook.book.publishYear || null
|
||||
if (audiobook.book.narrator) {
|
||||
bookMetadata.narrators = audiobook.book._narratorsList
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ async function writeMetadataFile(audiobook, outputPath) {
|
|||
`title=${audiobook.title}`,
|
||||
`artist=${audiobook.authorFL}`,
|
||||
`album_artist=${audiobook.authorFL}`,
|
||||
`date=${audiobook.book.publishYear || ''}`,
|
||||
`date=${audiobook.book.publishedYear || ''}`,
|
||||
`description=${audiobook.book.description}`,
|
||||
`genre=${audiobook.book._genres.join(';')}`,
|
||||
`comment=Audiobookshelf v${package.version}`
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async function generate(audiobook, nfoFilename = 'metadata.nfo') {
|
|||
'Narrator': book.narrator,
|
||||
'Series': book.series,
|
||||
'Volume Number': book.volumeNumber,
|
||||
'Publish Year': book.publishYear,
|
||||
'Publish Year': book.publishedYear,
|
||||
'Genre': book.genres ? book.genres.join(', ') : '',
|
||||
'Duration': audiobook.durationPretty,
|
||||
'Chapters': jsonObj.chapters.length
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
|||
title: fetchTitle(metadata),
|
||||
author: fetchCreator(creators, 'aut'),
|
||||
narrator: fetchNarrators(creators, metadata),
|
||||
publishYear: fetchDate(metadata),
|
||||
publishedYear: fetchDate(metadata),
|
||||
publisher: fetchPublisher(metadata),
|
||||
isbn: fetchISBN(metadata),
|
||||
description: fetchDescription(metadata),
|
||||
|
|
|
|||
|
|
@ -119,28 +119,16 @@ function groupFileItemsIntoLibraryItemDirs(fileItems) {
|
|||
return libraryItemGroup
|
||||
}
|
||||
|
||||
function cleanFileObjects(libraryItemPath, libraryItemRelPath, files) {
|
||||
function cleanFileObjects(libraryItemPath, folderPath, files) {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
var filePath = Path.posix.join(libraryItemPath, file)
|
||||
var relFilePath = Path.posix.join(libraryItemRelPath, file)
|
||||
var relFilePath = filePath.replace(folderPath, '')
|
||||
var newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(filePath, relFilePath)
|
||||
return newLibraryFile
|
||||
}))
|
||||
}
|
||||
|
||||
function getFileType(ext) {
|
||||
var ext_cleaned = ext.toLowerCase()
|
||||
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
||||
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
|
||||
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
|
||||
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
|
||||
if (ext_cleaned === 'nfo') return 'info'
|
||||
if (ext_cleaned === 'txt') return 'text'
|
||||
if (ext_cleaned === 'opf') return 'opf'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// Scan folder
|
||||
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
||||
var folderPath = folder.fullPath.replace(/\\/g, '/')
|
||||
|
|
@ -164,7 +152,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
|||
for (const libraryItemPath in libraryItemGrouping) {
|
||||
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
|
||||
|
||||
var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemData.relPath, libraryItemGrouping[libraryItemPath])
|
||||
var fileObjs = await cleanFileObjects(libraryItemData.path, folderPath, libraryItemGrouping[libraryItemPath])
|
||||
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
|
||||
items.push({
|
||||
folderId: folder.id,
|
||||
|
|
@ -236,7 +224,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||
}
|
||||
}
|
||||
|
||||
var publishYear = null
|
||||
var publishedYear = null
|
||||
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
||||
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
||||
if (publishYearMatch && publishYearMatch.length > 2 && publishYearMatch[1]) {
|
||||
|
|
@ -245,7 +233,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||
publishYearMatch[1] = publishYearMatch[1].slice(1, -1)
|
||||
}
|
||||
if (!isNaN(publishYearMatch[1])) {
|
||||
publishYear = publishYearMatch[1]
|
||||
publishedYear = publishYearMatch[1]
|
||||
title = publishYearMatch[2]
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +255,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||
subtitle,
|
||||
series,
|
||||
sequence: volumeNumber,
|
||||
publishYear,
|
||||
publishedYear,
|
||||
},
|
||||
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
|
||||
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||
|
|
@ -281,7 +269,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
|
|||
|
||||
|
||||
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
|
||||
var fileItems = await recurseFiles(libraryItemPath, folder.fullPath)
|
||||
var fileItems = await recurseFiles(libraryItemPath)
|
||||
|
||||
libraryItemPath = libraryItemPath.replace(/\\/g, '/')
|
||||
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue