Update collection API routes to load libraryItems from DB

This commit is contained in:
advplyr 2023-08-12 15:01:27 -05:00
parent 38029d1202
commit db80cec168
8 changed files with 344 additions and 139 deletions

View File

@ -275,42 +275,11 @@ class Database {
}
}
updateCollection(oldCollection) {
if (!this.sequelize) return false
const collectionBooks = []
let order = 1
oldCollection.books.forEach((libraryItemId) => {
const libraryItem = this.getLibraryItem(libraryItemId)
if (!libraryItem) return
collectionBooks.push({
collectionId: oldCollection.id,
bookId: libraryItem.media.id,
order: order++
})
})
return this.models.collection.fullUpdateFromOld(oldCollection, collectionBooks)
}
async removeCollection(collectionId) {
if (!this.sequelize) return false
await this.models.collection.removeById(collectionId)
}
createCollectionBook(collectionBook) {
if (!this.sequelize) return false
return this.models.collectionBook.create(collectionBook)
}
createBulkCollectionBooks(collectionBooks) {
if (!this.sequelize) return false
return this.models.collectionBook.bulkCreate(collectionBooks)
}
removeCollectionBook(collectionId, bookId) {
if (!this.sequelize) return false
return this.models.collectionBook.removeByIds(collectionId, bookId)
}
async createPlaylist(oldPlaylist) {
if (!this.sequelize) return false
const newPlaylist = await this.models.playlist.createFromOld(oldPlaylist)

View File

@ -1,3 +1,4 @@
const Sequelize = require('sequelize')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
@ -7,16 +8,43 @@ const Collection = require('../objects/Collection')
class CollectionController {
constructor() { }
/**
* POST: /api/collections
* Create new collection
* @param {*} req
* @param {*} res
*/
async create(req, res) {
const newCollection = new Collection()
req.body.userId = req.user.id
if (!newCollection.setData(req.body)) {
return res.status(500).send('Invalid collection data')
return res.status(400).send('Invalid collection data')
}
// Create collection record
await Database.models.collection.createFromOld(newCollection)
// Get library items in collection
const libraryItemsInCollection = await Database.models.libraryItem.getForCollection(newCollection)
// Create collectionBook records
let order = 1
const collectionBooksToAdd = []
for (const libraryItemId of newCollection.books) {
const libraryItem = libraryItemsInCollection.find(li => li.id === libraryItemId)
if (libraryItem) {
collectionBooksToAdd.push({
collectionId: newCollection.id,
bookId: libraryItem.media.id,
order: order++
})
}
}
if (collectionBooksToAdd.length) {
await Database.createBulkCollectionBooks(collectionBooksToAdd)
}
const jsonExpanded = newCollection.toJSONExpanded(libraryItemsInCollection)
await Database.createCollection(newCollection)
SocketAuthority.emitter('collection_added', jsonExpanded)
res.json(jsonExpanded)
}
@ -31,140 +59,275 @@ class CollectionController {
async findOne(req, res) {
const includeEntities = (req.query.include || '').split(',')
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
if (includeEntities.includes('rssfeed')) {
const feedData = await this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
collectionExpanded.rssFeed = feedData?.toJSONMinified() || null
const collectionExpanded = await req.collection.getOldJsonExpanded(req.user, includeEntities)
if (!collectionExpanded) {
// This may happen if the user is restricted from all books
return res.sendStatus(404)
}
res.json(collectionExpanded)
}
/**
* PATCH: /api/collections/:id
* Update collection
* @param {*} req
* @param {*} res
*/
async update(req, res) {
const collection = req.collection
const wasUpdated = collection.update(req.body)
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
let wasUpdated = false
// Update description and name if defined
const collectionUpdatePayload = {}
if (req.body.description !== undefined && req.body.description !== req.collection.description) {
collectionUpdatePayload.description = req.body.description
wasUpdated = true
}
if (req.body.name !== undefined && req.body.name !== req.collection.name) {
collectionUpdatePayload.name = req.body.name
wasUpdated = true
}
if (wasUpdated) {
await req.collection.update(collectionUpdatePayload)
}
// If books array is passed in then update order in collection
if (req.body.books?.length) {
const collectionBooks = await req.collection.getCollectionBooks({
include: {
model: Database.models.book,
include: Database.models.libraryItem
},
order: [['order', 'ASC']]
})
collectionBooks.sort((a, b) => {
const aIndex = req.body.books.findIndex(lid => lid === a.book.libraryItem.id)
const bIndex = req.body.books.findIndex(lid => lid === b.book.libraryItem.id)
return aIndex - bIndex
})
for (let i = 0; i < collectionBooks.length; i++) {
if (collectionBooks[i].order !== i + 1) {
await collectionBooks[i].update({
order: i + 1
})
wasUpdated = true
}
}
}
const jsonExpanded = await req.collection.getOldJsonExpanded()
if (wasUpdated) {
await Database.updateCollection(collection)
SocketAuthority.emitter('collection_updated', jsonExpanded)
}
res.json(jsonExpanded)
}
async delete(req, res) {
const collection = req.collection
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
const jsonExpanded = await req.collection.getOldJsonExpanded()
// Close rss feed - remove from db and emit socket event
await this.rssFeedManager.closeFeedForEntityId(collection.id)
await this.rssFeedManager.closeFeedForEntityId(req.collection.id)
await req.collection.destroy()
await Database.removeCollection(collection.id)
SocketAuthority.emitter('collection_removed', jsonExpanded)
res.sendStatus(200)
}
/**
* POST: /api/collections/:id/book
* Add a single book to a collection
* Req.body { id: <library item id> }
* @param {*} req
* @param {*} res
*/
async addBook(req, res) {
const collection = req.collection
const libraryItem = Database.libraryItems.find(li => li.id === req.body.id)
const libraryItem = await Database.models.libraryItem.getOldById(req.body.id)
if (!libraryItem) {
return res.status(500).send('Book not found')
return res.status(404).send('Book not found')
}
if (libraryItem.libraryId !== collection.libraryId) {
return res.status(500).send('Book in different library')
if (libraryItem.libraryId !== req.collection.libraryId) {
return res.status(400).send('Book in different library')
}
if (collection.books.includes(req.body.id)) {
return res.status(500).send('Book already in collection')
}
collection.addBook(req.body.id)
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
const collectionBook = {
collectionId: collection.id,
bookId: libraryItem.media.id,
order: collection.books.length
// Check if book is already in collection
const collectionBooks = await req.collection.getCollectionBooks()
if (collectionBooks.some(cb => cb.bookId === libraryItem.media.id)) {
return res.status(400).send('Book already in collection')
}
await Database.createCollectionBook(collectionBook)
// Create collectionBook record
await Database.models.collectionBook.create({
collectionId: req.collection.id,
bookId: libraryItem.media.id,
order: collectionBooks.length + 1
})
const jsonExpanded = await req.collection.getOldJsonExpanded()
SocketAuthority.emitter('collection_updated', jsonExpanded)
res.json(jsonExpanded)
}
// DELETE: api/collections/:id/book/:bookId
/**
* DELETE: /api/collections/:id/book/:bookId
* Remove a single book from a collection. Re-order books
* TODO: bookId is actually libraryItemId. Clients need updating to use bookId
* @param {*} req
* @param {*} res
*/
async removeBook(req, res) {
const collection = req.collection
const libraryItem = Database.libraryItems.find(li => li.id === req.params.bookId)
const libraryItem = await Database.models.libraryItem.getOldById(req.params.bookId)
if (!libraryItem) {
return res.sendStatus(404)
}
if (collection.books.includes(req.params.bookId)) {
collection.removeBook(req.params.bookId)
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
// Get books in collection ordered
const collectionBooks = await req.collection.getCollectionBooks({
order: [['order', 'ASC']]
})
let jsonExpanded = null
const collectionBookToRemove = collectionBooks.find(cb => cb.bookId === libraryItem.media.id)
if (collectionBookToRemove) {
// Remove collection book record
await collectionBookToRemove.destroy()
// Update order on collection books
let order = 1
for (const collectionBook of collectionBooks) {
if (collectionBook.bookId === libraryItem.media.id) continue
if (collectionBook.order !== order) {
await collectionBook.update({
order
})
}
order++
}
jsonExpanded = await req.collection.getOldJsonExpanded()
SocketAuthority.emitter('collection_updated', jsonExpanded)
await Database.updateCollection(collection)
} else {
jsonExpanded = await req.collection.getOldJsonExpanded()
}
res.json(collection.toJSONExpanded(Database.libraryItems))
res.json(jsonExpanded)
}
// POST: api/collections/:id/batch/add
/**
* POST: /api/collections/:id/batch/add
* Add multiple books to collection
* Req.body { books: <Array of library item ids> }
* @param {*} req
* @param {*} res
*/
async addBatch(req, res) {
const collection = req.collection
if (!req.body.books || !req.body.books.length) {
// filter out invalid libraryItemIds
const bookIdsToAdd = (req.body.books || []).filter(b => !!b && typeof b == 'string')
if (!bookIdsToAdd.length) {
return res.status(500).send('Invalid request body')
}
const bookIdsToAdd = req.body.books
// Get library items associated with ids
const libraryItems = await Database.models.libraryItem.findAll({
where: {
id: {
[Sequelize.Op.in]: bookIdsToAdd
}
},
include: {
model: Database.models.book
}
})
// Get collection books already in collection
const collectionBooks = await req.collection.getCollectionBooks()
let order = collectionBooks.length + 1
const collectionBooksToAdd = []
let hasUpdated = false
let order = collection.books.length
for (const libraryItemId of bookIdsToAdd) {
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
if (!libraryItem) continue
if (!collection.books.includes(libraryItemId)) {
collection.addBook(libraryItemId)
// Check and set new collection books to add
for (const libraryItem of libraryItems) {
if (!collectionBooks.some(cb => cb.bookId === libraryItem.media.id)) {
collectionBooksToAdd.push({
collectionId: collection.id,
collectionId: req.collection.id,
bookId: libraryItem.media.id,
order: order++
})
hasUpdated = true
} else {
Logger.warn(`[CollectionController] addBatch: Library item ${libraryItem.id} already in collection`)
}
}
let jsonExpanded = null
if (hasUpdated) {
await Database.createBulkCollectionBooks(collectionBooksToAdd)
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
jsonExpanded = await req.collection.getOldJsonExpanded()
SocketAuthority.emitter('collection_updated', jsonExpanded)
} else {
jsonExpanded = await req.collection.getOldJsonExpanded()
}
res.json(collection.toJSONExpanded(Database.libraryItems))
res.json(jsonExpanded)
}
// POST: api/collections/:id/batch/remove
/**
* POST: /api/collections/:id/batch/remove
* Remove multiple books from collection
* Req.body { books: <Array of library item ids> }
* @param {*} req
* @param {*} res
*/
async removeBatch(req, res) {
const collection = req.collection
if (!req.body.books || !req.body.books.length) {
// filter out invalid libraryItemIds
const bookIdsToRemove = (req.body.books || []).filter(b => !!b && typeof b == 'string')
if (!bookIdsToRemove.length) {
return res.status(500).send('Invalid request body')
}
var bookIdsToRemove = req.body.books
let hasUpdated = false
for (const libraryItemId of bookIdsToRemove) {
const libraryItem = Database.libraryItems.find(li => li.id === libraryItemId)
if (!libraryItem) continue
if (collection.books.includes(libraryItemId)) {
collection.removeBook(libraryItemId)
// Get library items associated with ids
const libraryItems = await Database.models.libraryItem.findAll({
where: {
id: {
[Sequelize.Op.in]: bookIdsToRemove
}
},
include: {
model: Database.models.book
}
})
// Get collection books already in collection
const collectionBooks = await req.collection.getCollectionBooks({
order: [['order', 'ASC']]
})
// Remove collection books and update order
let order = 1
let hasUpdated = false
for (const collectionBook of collectionBooks) {
if (libraryItems.some(li => li.media.id === collectionBook.bookId)) {
await collectionBook.destroy()
hasUpdated = true
continue
} else if (collectionBook.order !== order) {
await collectionBook.update({
order
})
hasUpdated = true
}
order++
}
let jsonExpanded = await req.collection.getOldJsonExpanded()
if (hasUpdated) {
await Database.updateCollection(collection)
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
SocketAuthority.emitter('collection_updated', jsonExpanded)
}
res.json(collection.toJSONExpanded(Database.libraryItems))
res.json(jsonExpanded)
}
async middleware(req, res, next) {
if (req.params.id) {
const collection = await Database.models.collection.getById(req.params.id)
const collection = await Database.models.collection.findByPk(req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}

View File

@ -201,7 +201,7 @@ class PlaylistController {
// POST: api/playlists/collection/:collectionId
async createFromCollection(req, res) {
let collection = await Database.models.collection.getById(req.params.collectionId)
let collection = await Database.models.collection.getOldById(req.params.collectionId)
if (!collection) {
return res.status(404).send('Collection not found')
}

View File

@ -45,7 +45,7 @@ class RSSFeedController {
async openRSSFeedForCollection(req, res) {
const options = req.body || {}
const collection = await Database.models.collection.getById(req.params.collectionId)
const collection = await Database.models.collection.getOldById(req.params.collectionId)
if (!collection) return res.sendStatus(404)
// Check request body options exist

View File

@ -12,7 +12,7 @@ class RssFeedManager {
async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
const collection = await Database.models.collection.getById(feedObj.entityId)
const collection = await Database.models.collection.getOldById(feedObj.entityId)
if (!collection) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
return false
@ -102,7 +102,7 @@ class RssFeedManager {
await Database.updateFeed(feed)
}
} else if (feed.entityType === 'collection') {
const collection = await Database.models.collection.getById(feed.entityId)
const collection = await Database.models.collection.getOldById(feed.entityId)
if (collection) {
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)

View File

@ -1,4 +1,4 @@
const { DataTypes, Model } = require('sequelize')
const { DataTypes, Model, Sequelize } = require('sequelize')
const oldCollection = require('../objects/Collection')
const { areEquivalent } = require('../utils/index')
@ -112,6 +112,76 @@ module.exports = (sequelize) => {
}).filter(c => c)
}
/**
* Get old collection toJSONExpanded, items filtered for user permissions
* @param {[oldUser]} user
* @param {[string[]]} include
* @returns {Promise<object>} oldCollection.toJSONExpanded
*/
async getOldJsonExpanded(user, include) {
this.books = await this.getBooks({
include: [
{
model: sequelize.models.libraryItem
},
{
model: sequelize.models.author,
through: {
attributes: []
}
},
{
model: sequelize.models.series,
through: {
attributes: ['sequence']
}
},
],
order: [Sequelize.literal('`collectionBook.order` ASC')]
}) || []
const oldCollection = sequelize.models.collection.getOldCollection(this)
// Filter books using user permissions
// TODO: Handle user permission restrictions on initial query
const books = this.books?.filter(b => {
if (user) {
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
return false
}
if (b.explicit === true && !user.canAccessExplicitContent) {
return false
}
}
return true
}) || []
// Map to library items
const libraryItems = books.map(b => {
const libraryItem = b.libraryItem
delete b.libraryItem
libraryItem.media = b
return sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
})
// Users with restricted permissions will not see this collection
if (!books.length && oldCollection.books.length) {
return null
}
const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
if (include?.includes('rssfeed')) {
const feeds = await this.getFeeds()
if (feeds?.length) {
collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(feeds[0])
}
}
return collectionExpanded
}
/**
* Get old collection from Collection
* @param {Collection} collectionExpanded
@ -195,11 +265,11 @@ module.exports = (sequelize) => {
}
/**
* Get collection by id
* Get old collection by id
* @param {string} collectionId
* @returns {Promise<oldCollection|null>} returns null if not found
*/
static async getById(collectionId) {
static async getOldById(collectionId) {
if (!collectionId) return null
const collection = await this.findByPk(collectionId, {
include: {
@ -212,6 +282,36 @@ module.exports = (sequelize) => {
return this.getOldCollection(collection)
}
/**
* Get old collection from current
* @returns {Promise<oldCollection>}
*/
async getOld() {
this.books = await this.getBooks({
include: [
{
model: sequelize.models.libraryItem
},
{
model: sequelize.models.author,
through: {
attributes: []
}
},
{
model: sequelize.models.series,
through: {
attributes: ['sequence']
}
},
],
order: [Sequelize.literal('`collectionBook.order` ASC')]
}) || []
return sequelize.models.collection.getOldCollection(this)
}
/**
* Remove all collections belonging to library
* @param {string} libraryId
@ -226,26 +326,6 @@ module.exports = (sequelize) => {
})
}
/**
* Get all collections for a library
* @param {string} libraryId
* @returns {Promise<oldCollection[]>}
*/
static async getAllForLibrary(libraryId) {
if (!libraryId) return []
const collections = await this.findAll({
where: {
libraryId
},
include: {
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']]
})
return collections.map(c => this.getOldCollection(c))
}
static async getAllForBook(bookId) {
const collections = await this.findAll({
include: {

View File

@ -393,14 +393,6 @@ class ApiRouter {
// TODO: Remove open sessions for library item
let mediaItemIds = []
if (libraryItem.isBook) {
// remove book from collections
const collectionsWithBook = await Database.models.collection.getAllForBook(libraryItem.media.id)
for (const collection of collectionsWithBook) {
collection.removeBook(libraryItem.id)
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
}
// Check remove empty series
await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id)

View File

@ -882,24 +882,25 @@ module.exports = {
Logger.error(`[libraryItemsBookFilters] Invalid collection`, collection)
return []
}
const books = await Database.models.book.findAll({
where: {
id: {
[Sequelize.Op.in]: collection.books
}
},
include: [
{
model: Database.models.libraryItem
model: Database.models.libraryItem,
where: {
id: {
[Sequelize.Op.in]: collection.books
}
}
},
{
model: sequelize.models.author,
model: Database.models.author,
through: {
attributes: []
}
},
{
model: sequelize.models.series,
model: Database.models.series,
through: {
attributes: ['sequence']
}