New data model batch routes and batch editor

This commit is contained in:
advplyr 2022-03-13 17:10:48 -05:00
parent 6597fca576
commit 4bdef893af
19 changed files with 743 additions and 604 deletions

View file

@ -21,6 +21,9 @@ const AuthorController = require('./controllers/AuthorController')
const BookFinder = require('./finders/BookFinder')
const AuthorFinder = require('./finders/AuthorFinder')
const PodcastFinder = require('./finders/PodcastFinder')
const Author = require('./objects/entities/Author')
const Series = require('./objects/entities/Series')
const FileSystemController = require('./controllers/FileSystemController')
class ApiController {
@ -65,11 +68,8 @@ class ApiController {
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
this.router.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.bind(this))
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
// Legacy
this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
//
// Item Routes
@ -84,6 +84,10 @@ class ApiController {
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/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
//
@ -93,9 +97,6 @@ class ApiController {
this.router.delete('/books/:id', BookController.delete.bind(this))
this.router.delete('/books/all', BookController.deleteAll.bind(this))
this.router.post('/books/batch/delete', BookController.batchDelete.bind(this))
this.router.post('/books/batch/update', BookController.batchUpdate.bind(this))
this.router.post('/books/batch/get', BookController.batchGet.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))
@ -162,17 +163,16 @@ class ApiController {
//
// Author Routes
//
this.router.get('/authors/search', AuthorController.search.bind(this))
this.router.get('/authors/:id', AuthorController.middleware.bind(this), AuthorController.findOne.bind(this))
this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
this.router.get('/authors/search', AuthorController.search.bind(this))
//
// Series Routes
//
this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this))
this.router.get('/series/search', SeriesController.search.bind(this))
this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this))
//
// Misc Routes
@ -488,6 +488,60 @@ class ApiController {
res.sendStatus(200)
}
async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
if (mediaPayload.metadata) {
var mediaMetadata = mediaPayload.metadata
// Create new authors if in payload
if (mediaMetadata.authors && mediaMetadata.authors.length) {
// TODO: validate authors
var newAuthors = []
for (let i = 0; i < mediaMetadata.authors.length; i++) {
if (mediaMetadata.authors[i].id.startsWith('new')) {
var author = this.db.authors.find(au => au.checkNameEquals(mediaMetadata.authors[i].name))
if (!author) {
author = new Author()
author.setData(mediaMetadata.authors[i])
Logger.debug(`[ApiController] Created new author "${author.name}"`)
newAuthors.push(author)
}
// Update ID in original payload
mediaMetadata.authors[i].id = author.id
}
}
if (newAuthors.length) {
await this.db.insertEntities('author', newAuthors)
this.emitter('authors_added', newAuthors)
}
}
// Create new series if in payload
if (mediaMetadata.series && mediaMetadata.series.length) {
// TODO: validate series
var newSeries = []
for (let i = 0; i < mediaMetadata.series.length; i++) {
if (mediaMetadata.series[i].id.startsWith('new')) {
var seriesItem = this.db.series.find(se => se.checkNameEquals(mediaMetadata.series[i].name))
if (!seriesItem) {
seriesItem = new Series()
seriesItem.setData(mediaMetadata.series[i])
Logger.debug(`[ApiController] Created new series "${seriesItem.name}"`)
newSeries.push(seriesItem)
}
// Update ID in original payload
mediaMetadata.series[i].id = seriesItem.id
}
}
if (newSeries.length) {
await this.db.insertEntities('series', newSeries)
this.emitter('authors_added', newSeries)
}
}
}
}
getPodcastFeed(req, res) {
var url = req.body.rssFeed
if (!url) {

View file

@ -226,14 +226,6 @@ class LibraryController {
res.json(payload)
}
// LEGACY
// api/libraries/:id/books/filters
async getLibraryFilters(req, res) {
var library = req.library
var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
res.json(libraryHelpers.getDistinctFilterData(books))
}
async getLibraryFilterData(req, res) {
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
}

View file

@ -1,6 +1,4 @@
const Logger = require('../Logger')
const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
const { reqSupportsWebp } = require('../utils/index')
class LibraryItemController {
@ -43,49 +41,7 @@ class LibraryItemController {
await this.cacheManager.purgeCoverCache(libraryItem.id)
}
if (mediaPayload.metadata) {
var mediaMetadata = mediaPayload.metadata
// Create new authors if in payload
if (mediaMetadata.authors && mediaMetadata.authors.length) {
// TODO: validate authors
var newAuthors = []
for (let i = 0; i < mediaMetadata.authors.length; i++) {
if (mediaMetadata.authors[i].id.startsWith('new')) {
var newAuthor = new Author()
newAuthor.setData(mediaMetadata.authors[i])
Logger.debug(`[LibraryItemController] Created new author "${newAuthor.name}"`)
newAuthors.push(newAuthor)
// Update ID in original payload
mediaMetadata.authors[i].id = newAuthor.id
}
}
if (newAuthors.length) {
await this.db.insertEntities('author', newAuthors)
this.emitter('authors_added', newAuthors)
}
}
// Create new series if in payload
if (mediaMetadata.series && mediaMetadata.series.length) {
// TODO: validate series
var newSeries = []
for (let i = 0; i < mediaMetadata.series.length; i++) {
if (mediaMetadata.series[i].id.startsWith('new')) {
var newSeriesItem = new Series()
newSeriesItem.setData(mediaMetadata.series[i])
Logger.debug(`[LibraryItemController] Created new series "${newSeriesItem.name}"`)
newSeries.push(newSeriesItem)
// Update ID in original payload
mediaMetadata.series[i].id = newSeriesItem.id
}
}
if (newSeries.length) {
await this.db.insertEntities('series', newSeries)
this.emitter('authors_added', newSeries)
}
}
}
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
var hasUpdates = libraryItem.media.update(mediaPayload)
if (hasUpdates) {
@ -182,11 +138,76 @@ class LibraryItemController {
this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem)
}
// POST: api/items/batch/delete
async batchDelete(req, res) {
if (!req.user.canDelete) {
Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user)
return res.sendStatus(403)
}
var { libraryItemIds } = req.body
if (!libraryItemIds || !libraryItemIds.length) {
return res.sendStatus(500)
}
var itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id))
if (!itemsToDelete.length) {
return res.sendStatus(404)
}
for (let i = 0; i < itemsToDelete.length; i++) {
Logger.info(`[LibraryItemController] Deleting Library Item "${itemsToDelete[i].media.metadata.title}"`)
await this.handleDeleteLibraryItem(itemsToDelete[i])
}
res.sendStatus(200)
}
// POST: api/items/batch/update
async batchUpdate(req, res) {
var updatePayloads = req.body
if (!updatePayloads || !updatePayloads.length) {
return res.sendStatus(500)
}
var itemsUpdated = 0
for (let i = 0; i < updatePayloads.length; i++) {
var mediaPayload = updatePayloads[i].mediaPayload
var libraryItem = this.db.libraryItems.find(_li => _li.id === updatePayloads[i].id)
if (!libraryItem) return null
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
var hasUpdates = libraryItem.media.update(mediaPayload)
if (hasUpdates) {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded())
itemsUpdated++
}
}
res.json({
success: true,
updates: itemsUpdated
})
}
// POST: api/items/batch/get
async batchGet(req, res) {
var libraryItemIds = req.body.libraryItemIds || []
if (!libraryItemIds.length) {
return res.status(403).send('Invalid payload')
}
var libraryItems = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id)).map((li) => li.toJSONExpanded())
res.json(libraryItems)
}
middleware(req, res, next) {
var item = this.db.libraryItems.find(li => li.id === req.params.id)
if (!item || !item.media) return res.sendStatus(404)
// Check user can access this audiobooks library
// Check user can access this library
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
return res.sendStatus(403)
}

View file

@ -133,50 +133,6 @@ module.exports = {
return data
},
// TODO: Remove legacy
getDistinctFilterData(audiobooks) {
var data = {
authors: [],
genres: [],
tags: [],
series: [],
narrators: [],
languages: []
}
audiobooks.forEach((ab) => {
if (ab.book._authorsList.length) {
ab.book._authorsList.forEach((author) => {
if (author && !data.authors.includes(author)) data.authors.push(author)
})
}
if (ab.book._genres.length) {
ab.book._genres.forEach((genre) => {
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
})
}
if (ab.tags.length) {
ab.tags.forEach((tag) => {
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
})
}
if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
if (ab.book._narratorsList.length) {
ab.book._narratorsList.forEach((narrator) => {
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
})
}
if (ab.book._language && !data.languages.includes(ab.book._language)) data.languages.push(ab.book._language)
})
data.authors = naturalSort(data.authors).asc()
data.genres = naturalSort(data.genres).asc()
data.tags = naturalSort(data.tags).asc()
data.series = naturalSort(data.series).asc()
data.narrators = naturalSort(data.narrators).asc()
data.languages = naturalSort(data.languages).asc()
return data
},
getSeriesFromBooks(books, minified = false) {
var _series = {}
books.forEach((libraryItem) => {