mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-04 10:09:37 +00:00
New data model Book media type contains Audiobooks updates
This commit is contained in:
parent
1dde02b170
commit
c4eeb1cfb7
30 changed files with 347 additions and 247 deletions
|
|
@ -16,6 +16,7 @@ const BackupController = require('./controllers/BackupController')
|
|||
const LibraryItemController = require('./controllers/LibraryItemController')
|
||||
const SeriesController = require('./controllers/SeriesController')
|
||||
const AuthorController = require('./controllers/AuthorController')
|
||||
const AudiobookController = require('./controllers/AudiobookController')
|
||||
|
||||
const BookFinder = require('./finders/BookFinder')
|
||||
const AuthorFinder = require('./finders/AuthorFinder')
|
||||
|
|
@ -70,6 +71,13 @@ class ApiController {
|
|||
|
||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||
|
||||
//
|
||||
// Audiobook Routes
|
||||
//
|
||||
this.router.get('/audiobooks/:id', AudiobookController.middleware.bind(this), AudiobookController.findOne.bind(this))
|
||||
this.router.get('/audiobooks/:id/item', AudiobookController.middleware.bind(this), AudiobookController.findWithItem.bind(this))
|
||||
this.router.patch('/audiobooks/:id/tracks', AudiobookController.middleware.bind(this), AudiobookController.updateTracks.bind(this))
|
||||
|
||||
//
|
||||
// Item Routes
|
||||
//
|
||||
|
|
@ -84,7 +92,6 @@ 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.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.get('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
||||
|
||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ class CacheManager {
|
|||
// Write cache
|
||||
await fs.ensureDir(this.CoverCachePath)
|
||||
|
||||
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
||||
if (!writtenFile) return res.sendStatus(400)
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ class Server {
|
|||
|
||||
// Client dynamic routes
|
||||
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('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
|
|
|
|||
57
server/controllers/AudiobookController.js
Normal file
57
server/controllers/AudiobookController.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const Logger = require('../Logger')
|
||||
|
||||
class AudiobookController {
|
||||
constructor() { }
|
||||
|
||||
async findOne(req, res) {
|
||||
if (req.query.expanded == 1) return res.json(req.audiobook.toJSONExpanded())
|
||||
return res.json(req.audiobook)
|
||||
}
|
||||
|
||||
async findWithItem(req, res) {
|
||||
if (req.query.expanded == 1) {
|
||||
return res.json({
|
||||
libraryItem: req.libraryItem.toJSONExpanded(),
|
||||
audiobook: req.audiobook.toJSONExpanded()
|
||||
})
|
||||
}
|
||||
res.json({
|
||||
libraryItem: req.libraryItem.toJSON(),
|
||||
audiobook: req.audiobook.toJSON()
|
||||
})
|
||||
}
|
||||
|
||||
// PATCH: api/audiobooks/:id/tracks
|
||||
async updateTracks(req, res) {
|
||||
var libraryItem = req.libraryItem
|
||||
var audiobook = req.audiobook
|
||||
var orderedFileData = req.body.orderedFileData
|
||||
audiobook.updateAudioTracks(orderedFileData)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var audiobook = null
|
||||
var libraryItem = this.db.libraryItems.find(li => {
|
||||
if (li.mediaType != 'book') return false
|
||||
audiobook = li.media.getAudiobookById(req.params.id)
|
||||
return !!audiobook
|
||||
})
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[AudiobookController] User attempted to delete without permission`, req.user)
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||
Logger.warn('[AudiobookController] User attempted to update without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
req.libraryItem = libraryItem
|
||||
req.audiobook = audiobook
|
||||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new AudiobookController()
|
||||
|
|
@ -146,7 +146,7 @@ class LibraryController {
|
|||
if (payload.sortBy) {
|
||||
var sortKey = payload.sortBy
|
||||
|
||||
// old sort key
|
||||
// old sort key TODO: should be mutated in dbMigration
|
||||
if (sortKey.startsWith('book.')) {
|
||||
sortKey = sortKey.replace('book.', 'media.metadata.')
|
||||
}
|
||||
|
|
@ -263,26 +263,26 @@ class LibraryController {
|
|||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
var minified = req.query.minified === '1'
|
||||
|
||||
var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, libraryItems)
|
||||
var itemsWithUserProgress = libraryHelpers.getItemsWithUserProgress(req.user, libraryItems)
|
||||
|
||||
var categories = [
|
||||
{
|
||||
id: 'continue-reading',
|
||||
label: 'Continue Reading',
|
||||
type: 'books',
|
||||
entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
||||
},
|
||||
{
|
||||
id: 'recently-added',
|
||||
label: 'Recently Added',
|
||||
type: 'books',
|
||||
entities: libraryHelpers.getBooksMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||
},
|
||||
{
|
||||
id: 'read-again',
|
||||
label: 'Read Again',
|
||||
type: 'books',
|
||||
entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
||||
}
|
||||
].filter(cats => { // Remove categories with no items
|
||||
return cats.entities.length
|
||||
|
|
@ -299,7 +299,7 @@ class LibraryController {
|
|||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
var minified = req.query.minified === '1'
|
||||
|
||||
var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, books)
|
||||
var booksWithUserAb = libraryHelpers.getItemsWithUserProgress(req.user, books)
|
||||
var series = libraryHelpers.getSeriesFromBooks(books, minified)
|
||||
var seriesWithUserAb = libraryHelpers.getSeriesWithProgressFromBooks(req.user, books)
|
||||
|
||||
|
|
|
|||
|
|
@ -156,17 +156,6 @@ class LibraryItemController {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ class LibraryItem {
|
|||
isInvalid: !!this.isInvalid,
|
||||
mediaType: this.mediaType,
|
||||
media: this.media.toJSONMinified(),
|
||||
numFiles: this.libraryFiles.length
|
||||
numFiles: this.libraryFiles.length,
|
||||
size: this.size
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class Book {
|
|||
return hasUpdated
|
||||
}
|
||||
try {
|
||||
var { authorLF, authorFL } = parseAuthors(author)
|
||||
var { authorLF, authorFL } = parseAuthors.parse(author)
|
||||
var hasUpdated = authorLF !== this.authorLF || authorFL !== this.authorFL
|
||||
this.authorFL = authorFL || null
|
||||
this.authorLF = authorLF || null
|
||||
|
|
@ -155,7 +155,7 @@ class Book {
|
|||
return hasUpdated
|
||||
}
|
||||
try {
|
||||
var { authorFL } = parseAuthors(narrator)
|
||||
var { authorFL } = parseAuthors.parse(narrator)
|
||||
var hasUpdated = authorFL !== this.narratorFL
|
||||
this.narratorFL = authorFL || null
|
||||
return hasUpdated
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ class Book {
|
|||
return true
|
||||
}
|
||||
|
||||
getAudiobookById(audiobookId) {
|
||||
return this.audiobooks.find(ab => ab.id === audiobookId)
|
||||
}
|
||||
|
||||
removeFileWithInode(inode) {
|
||||
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
||||
if (audiobookWithIno) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class BookMetadata {
|
|||
language: this.language,
|
||||
explicit: this.explicit,
|
||||
authorName: this.authorName,
|
||||
authorNameLF: this.authorNameLF,
|
||||
narratorName: this.narratorName
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +96,10 @@ class BookMetadata {
|
|||
if (!this.authors.length) return ''
|
||||
return this.authors.map(au => au.name).join(', ')
|
||||
}
|
||||
get authorNameLF() { // Last, First
|
||||
if (!this.authors.length) return ''
|
||||
return this.authors.map(au => parseNameString.nameToLastFirst(au.name)).join(', ')
|
||||
}
|
||||
get seriesName() {
|
||||
if (!this.series.length) return ''
|
||||
return this.series.map(se => {
|
||||
|
|
@ -243,13 +248,13 @@ class BookMetadata {
|
|||
|
||||
// Returns array of names in First Last format
|
||||
parseNarratorsTag(narratorsTag) {
|
||||
var parsed = parseNameString(narratorsTag)
|
||||
var parsed = parseNameString.parse(narratorsTag)
|
||||
return parsed ? parsed.names : []
|
||||
}
|
||||
|
||||
// Return array of authors minified with placeholder id
|
||||
parseAuthorsTag(authorsTag) {
|
||||
var parsed = parseNameString(authorsTag)
|
||||
var parsed = parseNameString.parse(authorsTag)
|
||||
if (!parsed) return []
|
||||
return (parsed.names || []).map((au) => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ const Logger = require('../../Logger')
|
|||
class LibraryItemProgress {
|
||||
constructor(progress) {
|
||||
this.id = null // Same as library item id
|
||||
this.libararyItemId = null
|
||||
this.libraryItemId = null
|
||||
|
||||
this.totalDuration = null // seconds
|
||||
this.progress = null // 0 to 1
|
||||
this.currentTime = null // seconds
|
||||
this.isRead = false
|
||||
this.isFinished = false
|
||||
|
||||
this.lastUpdate = null
|
||||
this.startedAt = null
|
||||
|
|
@ -22,11 +22,11 @@ class LibraryItemProgress {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libararyItemId: this.libararyItemId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
totalDuration: this.totalDuration,
|
||||
progress: this.progress,
|
||||
currentTime: this.currentTime,
|
||||
isRead: this.isRead,
|
||||
isFinished: this.isFinished,
|
||||
lastUpdate: this.lastUpdate,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt
|
||||
|
|
@ -35,11 +35,11 @@ class LibraryItemProgress {
|
|||
|
||||
construct(progress) {
|
||||
this.id = progress.id
|
||||
this.libararyItemId = progress.libararyItemId
|
||||
this.libraryItemId = progress.libraryItemId
|
||||
this.totalDuration = progress.totalDuration
|
||||
this.progress = progress.progress
|
||||
this.currentTime = progress.currentTime
|
||||
this.isRead = !!progress.isRead
|
||||
this.isFinished = !!progress.isFinished
|
||||
this.lastUpdate = progress.lastUpdate
|
||||
this.startedAt = progress.startedAt
|
||||
this.finishedAt = progress.finishedAt || null
|
||||
|
|
@ -59,11 +59,11 @@ class LibraryItemProgress {
|
|||
// If has < 10 seconds remaining mark as read
|
||||
var timeRemaining = this.totalDuration - this.currentTime
|
||||
if (timeRemaining < 10) {
|
||||
this.isRead = true
|
||||
this.isFinished = true
|
||||
this.progress = 1
|
||||
this.finishedAt = Date.now()
|
||||
} else {
|
||||
this.isRead = false
|
||||
this.isFinished = false
|
||||
this.finishedAt = null
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ class LibraryItemProgress {
|
|||
var hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (this[key] !== undefined && payload[key] !== this[key]) {
|
||||
if (key === 'isRead') {
|
||||
if (key === 'isFinished') {
|
||||
if (!payload[key]) { // Updating to Not Read - Reset progress and current time
|
||||
this.finishedAt = null
|
||||
this.progress = 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { isObject } = require('../../utils')
|
||||
const AudioBookmark = require('./AudioBookmark')
|
||||
const LibraryItemProgress = require('./LibraryItemProgress')
|
||||
|
||||
|
|
|
|||
|
|
@ -245,6 +245,27 @@ async function migrateLibraryItems(db) {
|
|||
|
||||
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
||||
|
||||
// User library item progress was using the auidobook ID when migrated
|
||||
// now that library items are created the LibraryItemProgress objects
|
||||
// need the library item id to be set
|
||||
for (const user of db.users) {
|
||||
if (user.libraryItemProgress.length) {
|
||||
user.libraryItemProgress = user.libraryItemProgress.map(lip => {
|
||||
var audiobookId = lip.id
|
||||
var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
|
||||
if (!libraryItemWithAudiobook) {
|
||||
Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
|
||||
return null
|
||||
}
|
||||
lip.id = libraryItemWithAudiobook.id
|
||||
lip.libraryItemId = libraryItemWithAudiobook.id
|
||||
return lip
|
||||
}).filter(lip => !!lip)
|
||||
await db.updateEntity('user', user)
|
||||
Logger.debug(`>>> User ${user.username} with ${user.libraryItemProgress.length} progress entries were updated`)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
||||
await db.insertEntities('libraryItem', libraryItems)
|
||||
if (authorsToAdd.length) {
|
||||
|
|
@ -286,8 +307,9 @@ function cleanUserObject(db, userObj) {
|
|||
|
||||
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
|
||||
var liProgress = new LibraryItemProgress() // New Progress Object
|
||||
liProgress.id = userAudiobookData.audiobookId
|
||||
liProgress.id = userAudiobookData.audiobookId // This ID is INCORRECT, will be updated when library item is created
|
||||
liProgress.libraryItemId = userAudiobookData.audiobookId
|
||||
liProgress.isFinished = !!userAudiobookData.isRead
|
||||
Object.keys(liProgress.toJSON()).forEach((key) => {
|
||||
if (userAudiobookData[key] !== undefined) {
|
||||
liProgress[key] = userAudiobookData[key]
|
||||
|
|
|
|||
|
|
@ -194,21 +194,21 @@ module.exports = {
|
|||
})
|
||||
},
|
||||
|
||||
getBooksWithUserAudiobook(user, books) {
|
||||
return books.map(book => {
|
||||
getItemsWithUserProgress(user, libraryItems) {
|
||||
return libraryItems.map(li => {
|
||||
return {
|
||||
userAudiobook: user.getLibraryItemProgress(book.id),
|
||||
book
|
||||
userProgress: user.getLibraryItemProgress(li.id),
|
||||
libraryItem: li
|
||||
}
|
||||
}).filter(b => !!b.userAudiobook)
|
||||
}).filter(b => !!b.userProgress)
|
||||
},
|
||||
|
||||
getBooksMostRecentlyRead(booksWithUserAb, limit, minified = false) {
|
||||
var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.progress > 0 && !data.userAudiobook.isRead)
|
||||
booksWithProgress.sort((a, b) => {
|
||||
return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
||||
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
||||
var itemsInProgress = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.progress > 0 && !data.userProgress.isFinished)
|
||||
itemsInProgress.sort((a, b) => {
|
||||
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
||||
})
|
||||
return booksWithProgress.map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||
return itemsInProgress.map(b => minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
||||
|
|
@ -223,17 +223,17 @@ module.exports = {
|
|||
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getBooksMostRecentlyAdded(books, limit, minified = false) {
|
||||
var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
|
||||
return booksSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
||||
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
||||
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
||||
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getBooksMostRecentlyFinished(booksWithUserAb, limit, minified = false) {
|
||||
var booksRead = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.isRead)
|
||||
booksRead.sort((a, b) => {
|
||||
return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
|
||||
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
||||
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
||||
itemsFinished.sort((a, b) => {
|
||||
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
||||
})
|
||||
return booksRead.map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||
return itemsFinished.map(i => minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getSeriesMostRecentlyAdded(series, limit) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,16 @@ function checkIsALastName(name) {
|
|||
return false
|
||||
}
|
||||
|
||||
module.exports = (nameString) => {
|
||||
// Handle name already in First Last format and return Last, First
|
||||
module.exports.nameToLastFirst = (firstLast) => {
|
||||
var nameObj = parseName(firstLast)
|
||||
if (!nameObj.last_name) return nameObj.first_name
|
||||
else if (!nameObj.first_name) return nameObj.last_name
|
||||
return `${nameObj.last_name}, ${nameObj.first_name}`
|
||||
}
|
||||
|
||||
// Handle any name string
|
||||
module.exports.parse = (nameString) => {
|
||||
if (!nameString) return null
|
||||
|
||||
var splitNames = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue