mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-04 10:14:36 +02:00
Merge remote-tracking branch 'origin/master' into auth_passportjs
This commit is contained in:
commit
f0f03efe17
138 changed files with 11777 additions and 7343 deletions
|
@ -1,10 +1,13 @@
|
|||
|
||||
const sequelize = require('sequelize')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const CacheManager = require('../managers/CacheManager')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const AuthorFinder = require('../finders/AuthorFinder')
|
||||
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
|
||||
|
@ -21,7 +24,7 @@ class AuthorController {
|
|||
|
||||
// Used on author landing page to include library items and items grouped in series
|
||||
if (include.includes('items')) {
|
||||
authorJson.libraryItems = await Database.models.libraryItem.getForAuthor(req.author, req.user)
|
||||
authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.user)
|
||||
|
||||
if (include.includes('series')) {
|
||||
const seriesMap = {}
|
||||
|
@ -67,13 +70,13 @@ class AuthorController {
|
|||
// Updating/removing cover image
|
||||
if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) {
|
||||
if (!payload.imagePath && req.author.imagePath) { // If removing image then remove file
|
||||
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
await this.coverManager.removeFile(req.author.imagePath)
|
||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
await CoverManager.removeFile(req.author.imagePath)
|
||||
} else if (payload.imagePath.startsWith('http')) { // Check if image path is a url
|
||||
const imageData = await this.authorFinder.saveAuthorImage(req.author.id, payload.imagePath)
|
||||
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath)
|
||||
if (imageData) {
|
||||
if (req.author.imagePath) {
|
||||
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
}
|
||||
payload.imagePath = imageData.path
|
||||
hasUpdated = true
|
||||
|
@ -85,7 +88,7 @@ class AuthorController {
|
|||
}
|
||||
|
||||
if (req.author.imagePath) {
|
||||
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,10 +96,21 @@ class AuthorController {
|
|||
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||
|
||||
// Check if author name matches another author and merge the authors
|
||||
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||
let existingAuthor = null
|
||||
if (authorNameUpdate) {
|
||||
const author = await Database.authorModel.findOne({
|
||||
where: {
|
||||
id: {
|
||||
[sequelize.Op.not]: req.author.id
|
||||
},
|
||||
name: payload.name
|
||||
}
|
||||
})
|
||||
existingAuthor = author?.getOldAuthor()
|
||||
}
|
||||
if (existingAuthor) {
|
||||
const bookAuthorsToCreate = []
|
||||
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
|
||||
const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
|
||||
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||
bookAuthorsToCreate.push({
|
||||
|
@ -113,9 +127,11 @@ class AuthorController {
|
|||
// Remove old author
|
||||
await Database.removeAuthor(req.author.id)
|
||||
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
||||
// Update filter data
|
||||
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
||||
|
||||
// Send updated num books for merged author
|
||||
const numBooks = await Database.models.libraryItem.getForAuthor(existingAuthor).length
|
||||
const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length
|
||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||
|
||||
res.json({
|
||||
|
@ -130,7 +146,7 @@ class AuthorController {
|
|||
if (hasUpdated) {
|
||||
req.author.updatedAt = Date.now()
|
||||
|
||||
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
|
||||
const itemsWithAuthor = await Database.libraryItemModel.getForAuthor(req.author)
|
||||
if (authorNameUpdate) { // Update author name on all books
|
||||
itemsWithAuthor.forEach(libraryItem => {
|
||||
libraryItem.media.metadata.updateAuthor(req.author)
|
||||
|
@ -151,24 +167,13 @@ class AuthorController {
|
|||
}
|
||||
}
|
||||
|
||||
async search(req, res) {
|
||||
var q = (req.query.q || '').toLowerCase()
|
||||
if (!q) return res.json([])
|
||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||
var authors = Database.authors.filter(au => au.name?.toLowerCase().includes(q))
|
||||
authors = authors.slice(0, limit)
|
||||
res.json({
|
||||
results: authors
|
||||
})
|
||||
}
|
||||
|
||||
async match(req, res) {
|
||||
let authorData = null
|
||||
const region = req.body.region || 'us'
|
||||
if (req.body.asin) {
|
||||
authorData = await this.authorFinder.findAuthorByASIN(req.body.asin, region)
|
||||
authorData = await AuthorFinder.findAuthorByASIN(req.body.asin, region)
|
||||
} else {
|
||||
authorData = await this.authorFinder.findAuthorByName(req.body.q, region)
|
||||
authorData = await AuthorFinder.findAuthorByName(req.body.q, region)
|
||||
}
|
||||
if (!authorData) {
|
||||
return res.status(404).send('Author not found')
|
||||
|
@ -183,9 +188,9 @@ class AuthorController {
|
|||
|
||||
// Only updates image if there was no image before or the author ASIN was updated
|
||||
if (authorData.image && (!req.author.imagePath || hasUpdates)) {
|
||||
this.cacheManager.purgeImageCache(req.author.id)
|
||||
await CacheManager.purgeImageCache(req.author.id)
|
||||
|
||||
const imageData = await this.authorFinder.saveAuthorImage(req.author.id, authorData.image)
|
||||
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image)
|
||||
if (imageData) {
|
||||
req.author.imagePath = imageData.path
|
||||
hasUpdates = true
|
||||
|
@ -202,7 +207,7 @@ class AuthorController {
|
|||
|
||||
await Database.updateAuthor(req.author)
|
||||
|
||||
const numBooks = await Database.models.libraryItem.getForAuthor(req.author).length
|
||||
const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||
}
|
||||
|
||||
|
@ -229,11 +234,11 @@ class AuthorController {
|
|||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
}
|
||||
return this.cacheManager.handleAuthorCache(res, author, options)
|
||||
return CacheManager.handleAuthorCache(res, author, options)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const author = Database.authors.find(au => au.id === req.params.id)
|
||||
async middleware(req, res, next) {
|
||||
const author = await Database.authorModel.getOldById(req.params.id)
|
||||
if (!author) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const Logger = require('../Logger')
|
||||
const CacheManager = require('../managers/CacheManager')
|
||||
|
||||
class CacheController {
|
||||
constructor() { }
|
||||
|
@ -8,7 +8,7 @@ class CacheController {
|
|||
if (!req.user.isAdminOrUp) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
await this.cacheManager.purgeAll()
|
||||
await CacheManager.purgeAll()
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ class CacheController {
|
|||
if (!req.user.isAdminOrUp) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
await this.cacheManager.purgeItems()
|
||||
await CacheManager.purgeItems()
|
||||
res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const Sequelize = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
@ -7,22 +8,49 @@ 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.collectionModel.createFromOld(newCollection)
|
||||
|
||||
// Get library items in collection
|
||||
const libraryItemsInCollection = await Database.libraryItemModel.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 libraryItemsInCollection = await Database.models.libraryItem.getForCollection(newCollection)
|
||||
const jsonExpanded = newCollection.toJSONExpanded(libraryItemsInCollection)
|
||||
await Database.createCollection(newCollection)
|
||||
SocketAuthority.emitter('collection_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
async findAll(req, res) {
|
||||
const collectionsExpanded = await Database.models.collection.getOldCollectionsJsonExpanded(req.user)
|
||||
const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user)
|
||||
res.json({
|
||||
collections: collectionsExpanded
|
||||
})
|
||||
|
@ -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.bookModel,
|
||||
include: Database.libraryItemModel
|
||||
},
|
||||
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.libraryItemModel.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.collectionBookModel.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.libraryItemModel.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.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Sequelize.Op.in]: bookIdsToAdd
|
||||
}
|
||||
},
|
||||
include: {
|
||||
model: Database.bookModel
|
||||
}
|
||||
})
|
||||
|
||||
// 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.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Sequelize.Op.in]: bookIdsToRemove
|
||||
}
|
||||
},
|
||||
include: {
|
||||
model: Database.bookModel
|
||||
}
|
||||
})
|
||||
|
||||
// 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.collectionModel.findByPk(req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class EmailController {
|
|||
async sendEBookToDevice(req, res) {
|
||||
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
||||
|
||||
const libraryItem = Database.getLibraryItem(req.body.libraryItemId)
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(req.body.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Library item not found')
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class FileSystemController {
|
|||
})
|
||||
|
||||
// Do not include existing mapped library paths in response
|
||||
const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
|
||||
const libraryFoldersPaths = await Database.libraryFolderModel.getAllLibraryFolderPaths()
|
||||
libraryFoldersPaths.forEach((path) => {
|
||||
let dir = path || ''
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,11 +8,24 @@ const zipHelpers = require('../utils/zipHelpers')
|
|||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { ScanResult } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||
const AudioFileScanner = require('../scanner/AudioFileScanner')
|
||||
const Scanner = require('../scanner/Scanner')
|
||||
const CacheManager = require('../managers/CacheManager')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
|
||||
class LibraryItemController {
|
||||
constructor() { }
|
||||
|
||||
// Example expand with authors: api/items/:id?expanded=1&include=authors
|
||||
/**
|
||||
* GET: /api/items/:id
|
||||
* Optional query params:
|
||||
* ?include=progress,rssfeed,downloads
|
||||
* ?expanded=1
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
const includeEntities = (req.query.include || '').split(',')
|
||||
if (req.query.expanded == 1) {
|
||||
|
@ -29,17 +42,7 @@ class LibraryItemController {
|
|||
item.rssFeed = feedData?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
if (item.mediaType == 'book') {
|
||||
if (includeEntities.includes('authors')) {
|
||||
item.media.metadata.authors = item.media.metadata.authors.map(au => {
|
||||
var author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (!author) return null
|
||||
return {
|
||||
...author
|
||||
}
|
||||
}).filter(au => au)
|
||||
}
|
||||
} else if (includeEntities.includes('downloads')) {
|
||||
if (item.mediaType === 'podcast' && includeEntities.includes('downloads')) {
|
||||
const downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
|
||||
item.episodeDownloadsQueued = downloadsInQueue.map(d => d.toJSONForClient())
|
||||
if (this.podcastManager.currentDownload?.libraryItemId === req.libraryItem.id) {
|
||||
|
@ -56,7 +59,7 @@ class LibraryItemController {
|
|||
var libraryItem = req.libraryItem
|
||||
// Item has cover and update is removing cover so purge it from cache
|
||||
if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) {
|
||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||
}
|
||||
|
||||
const hasUpdates = libraryItem.update(req.body)
|
||||
|
@ -71,13 +74,14 @@ class LibraryItemController {
|
|||
async delete(req, res) {
|
||||
const hardDelete = req.query.hard == 1 // Delete from file system
|
||||
const libraryItemPath = req.libraryItem.path
|
||||
await this.handleDeleteLibraryItem(req.libraryItem)
|
||||
await this.handleDeleteLibraryItem(req.libraryItem.mediaType, req.libraryItem.id, [req.libraryItem.media.id])
|
||||
if (hardDelete) {
|
||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||
await fs.remove(libraryItemPath).catch((error) => {
|
||||
Logger.error(`[LibraryItemController] Failed to delete library item from file system at "${libraryItemPath}"`, error)
|
||||
})
|
||||
}
|
||||
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
|
@ -103,7 +107,7 @@ class LibraryItemController {
|
|||
|
||||
// Item has cover and update is removing cover so purge it from cache
|
||||
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||
}
|
||||
|
||||
// Book specific
|
||||
|
@ -124,7 +128,7 @@ class LibraryItemController {
|
|||
// Book specific - Get all series being removed from this item
|
||||
let seriesRemoved = []
|
||||
if (libraryItem.isBook && mediaPayload.metadata?.series) {
|
||||
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
|
||||
const seriesIdsInUpdate = mediaPayload.metadata.series?.map(se => se.id) || []
|
||||
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
||||
}
|
||||
|
||||
|
@ -135,7 +139,7 @@ class LibraryItemController {
|
|||
if (seriesRemoved.length) {
|
||||
// Check remove empty series
|
||||
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
|
||||
await this.checkRemoveEmptySeries(seriesRemoved)
|
||||
await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
|
||||
}
|
||||
|
||||
if (isPodcastAutoDownloadUpdated) {
|
||||
|
@ -164,10 +168,10 @@ class LibraryItemController {
|
|||
var result = null
|
||||
if (req.body && req.body.url) {
|
||||
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
||||
result = await this.coverManager.downloadCoverFromUrl(libraryItem, req.body.url)
|
||||
result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url)
|
||||
} else if (req.files && req.files.cover) {
|
||||
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
||||
result = await this.coverManager.uploadCover(libraryItem, req.files.cover)
|
||||
result = await CoverManager.uploadCover(libraryItem, req.files.cover)
|
||||
} else {
|
||||
return res.status(400).send('Invalid request no file or url')
|
||||
}
|
||||
|
@ -193,7 +197,7 @@ class LibraryItemController {
|
|||
return res.status(400).send('Invalid request no cover path')
|
||||
}
|
||||
|
||||
const validationResult = await this.coverManager.validateCoverPath(req.body.cover, libraryItem)
|
||||
const validationResult = await CoverManager.validateCoverPath(req.body.cover, libraryItem)
|
||||
if (validationResult.error) {
|
||||
return res.status(500).send(validationResult.error)
|
||||
}
|
||||
|
@ -213,7 +217,7 @@ class LibraryItemController {
|
|||
|
||||
if (libraryItem.media.coverPath) {
|
||||
libraryItem.updateMediaCover('')
|
||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
@ -242,7 +246,7 @@ class LibraryItemController {
|
|||
height: height ? parseInt(height) : null,
|
||||
width: width ? parseInt(width) : null
|
||||
}
|
||||
return this.cacheManager.handleCoverCache(res, libraryItem, options)
|
||||
return CacheManager.handleCoverCache(res, libraryItem, options)
|
||||
}
|
||||
|
||||
// GET: api/items/:id/stream
|
||||
|
@ -296,7 +300,7 @@ class LibraryItemController {
|
|||
var libraryItem = req.libraryItem
|
||||
|
||||
var options = req.body || {}
|
||||
var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
var matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
res.json(matchResult)
|
||||
}
|
||||
|
||||
|
@ -309,18 +313,23 @@ class LibraryItemController {
|
|||
const hardDelete = req.query.hard == 1 // Delete files from filesystem
|
||||
|
||||
const { libraryItemIds } = req.body
|
||||
if (!libraryItemIds || !libraryItemIds.length) {
|
||||
return res.sendStatus(500)
|
||||
if (!libraryItemIds?.length) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
|
||||
const itemsToDelete = Database.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||
const itemsToDelete = await Database.libraryItemModel.getAllOldLibraryItems({
|
||||
id: libraryItemIds
|
||||
})
|
||||
|
||||
if (!itemsToDelete.length) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
for (let i = 0; i < itemsToDelete.length; i++) {
|
||||
const libraryItemPath = itemsToDelete[i].path
|
||||
Logger.info(`[LibraryItemController] Deleting Library Item "${itemsToDelete[i].media.metadata.title}"`)
|
||||
await this.handleDeleteLibraryItem(itemsToDelete[i])
|
||||
|
||||
const libraryId = itemsToDelete[0].libraryId
|
||||
for (const libraryItem of itemsToDelete) {
|
||||
const libraryItemPath = libraryItem.path
|
||||
Logger.info(`[LibraryItemController] Deleting Library Item "${libraryItem.media.metadata.title}"`)
|
||||
await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, [libraryItem.media.id])
|
||||
if (hardDelete) {
|
||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||
await fs.remove(libraryItemPath).catch((error) => {
|
||||
|
@ -328,28 +337,42 @@ class LibraryItemController {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
await Database.resetLibraryIssuesFilterData(libraryId)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/items/batch/update
|
||||
async batchUpdate(req, res) {
|
||||
var updatePayloads = req.body
|
||||
if (!updatePayloads || !updatePayloads.length) {
|
||||
const updatePayloads = req.body
|
||||
if (!updatePayloads?.length) {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var itemsUpdated = 0
|
||||
let itemsUpdated = 0
|
||||
|
||||
for (let i = 0; i < updatePayloads.length; i++) {
|
||||
var mediaPayload = updatePayloads[i].mediaPayload
|
||||
var libraryItem = Database.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||
for (const updatePayload of updatePayloads) {
|
||||
const mediaPayload = updatePayload.mediaPayload
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(updatePayload.id)
|
||||
if (!libraryItem) return null
|
||||
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
||||
|
||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||
if (hasUpdates) {
|
||||
let seriesRemoved = []
|
||||
if (libraryItem.isBook && mediaPayload.metadata?.series) {
|
||||
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
|
||||
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
||||
}
|
||||
|
||||
if (libraryItem.media.update(mediaPayload)) {
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
|
||||
if (seriesRemoved.length) {
|
||||
// Check remove empty series
|
||||
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
|
||||
await this.checkRemoveEmptySeries(libraryItem.media.id, seriesRemoved.map(se => se.id))
|
||||
}
|
||||
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
itemsUpdated++
|
||||
|
@ -368,13 +391,11 @@ class LibraryItemController {
|
|||
if (!libraryItemIds.length) {
|
||||
return res.status(403).send('Invalid payload')
|
||||
}
|
||||
const libraryItems = []
|
||||
libraryItemIds.forEach((lid) => {
|
||||
const li = Database.libraryItems.find(_li => _li.id === lid)
|
||||
if (li) libraryItems.push(li.toJSONExpanded())
|
||||
const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({
|
||||
id: libraryItemIds
|
||||
})
|
||||
res.json({
|
||||
libraryItems
|
||||
libraryItems: libraryItems.map(li => li.toJSONExpanded())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -393,7 +414,9 @@ class LibraryItemController {
|
|||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({
|
||||
id: req.body.libraryItemIds
|
||||
})
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
@ -401,7 +424,7 @@ class LibraryItemController {
|
|||
res.sendStatus(200)
|
||||
|
||||
for (const libraryItem of libraryItems) {
|
||||
const matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
const matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
if (matchResult.updated) {
|
||||
itemsUpdated++
|
||||
} else if (matchResult.warning) {
|
||||
|
@ -428,23 +451,31 @@ class LibraryItemController {
|
|||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: req.body.libraryItemIds
|
||||
},
|
||||
attributes: ['id', 'libraryId', 'isFile']
|
||||
})
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
|
||||
const libraryId = libraryItems[0].libraryId
|
||||
for (const libraryItem of libraryItems) {
|
||||
if (libraryItem.isFile) {
|
||||
Logger.warn(`[LibraryItemController] Re-scanning file library items not yet supported`)
|
||||
} else {
|
||||
await this.scanner.scanLibraryItemByRequest(libraryItem)
|
||||
await LibraryItemScanner.scanLibraryItem(libraryItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
await Database.resetLibraryIssuesFilterData(libraryId)
|
||||
}
|
||||
|
||||
// POST: api/items/:id/scan (admin)
|
||||
// POST: api/items/:id/scan
|
||||
async scan(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user)
|
||||
|
@ -456,7 +487,8 @@ class LibraryItemController {
|
|||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
||||
const result = await LibraryItemScanner.scanLibraryItem(req.libraryItem.id)
|
||||
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
|
||||
res.json({
|
||||
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
||||
})
|
||||
|
@ -529,7 +561,7 @@ class LibraryItemController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const ffprobeData = await this.scanner.probeAudioFile(audioFile)
|
||||
const ffprobeData = await AudioFileScanner.probeAudioFile(audioFile)
|
||||
res.json(ffprobeData)
|
||||
}
|
||||
|
||||
|
@ -680,7 +712,7 @@ class LibraryItemController {
|
|||
}
|
||||
|
||||
async middleware(req, res, next) {
|
||||
req.libraryItem = await Database.models.libraryItem.getOldById(req.params.id)
|
||||
req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
|
|
@ -59,7 +59,7 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/progress/:id
|
||||
async createUpdateMediaProgress(req, res) {
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ class MeController {
|
|||
// PATCH: api/me/progress/:id/:episodeId
|
||||
async createUpdateEpisodeMediaProgress(req, res) {
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ class MeController {
|
|||
|
||||
let shouldUpdate = false
|
||||
for (const itemProgress of itemProgressPayloads) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(itemProgress.libraryItemId)
|
||||
if (libraryItem) {
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
|
||||
|
@ -122,10 +122,10 @@ class MeController {
|
|||
|
||||
// POST: api/me/item/:id/bookmark
|
||||
async createBookmark(req, res) {
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
||||
|
||||
const { time, title } = req.body
|
||||
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
|
||||
const bookmark = req.user.createBookmark(req.params.id, time, title)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
|
@ -133,15 +133,17 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/item/:id/bookmark
|
||||
async updateBookmark(req, res) {
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
||||
|
||||
const { time, title } = req.body
|
||||
if (!req.user.findBookmark(libraryItem.id, time)) {
|
||||
if (!req.user.findBookmark(req.params.id, time)) {
|
||||
Logger.error(`[MeController] updateBookmark not found`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
|
||||
|
||||
const bookmark = req.user.updateBookmark(req.params.id, time, title)
|
||||
if (!bookmark) return res.sendStatus(500)
|
||||
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
|
@ -149,16 +151,17 @@ class MeController {
|
|||
|
||||
// DELETE: api/me/item/:id/bookmark/:time
|
||||
async removeBookmark(req, res) {
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
var time = Number(req.params.time)
|
||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
||||
|
||||
const time = Number(req.params.time)
|
||||
if (isNaN(time)) return res.sendStatus(500)
|
||||
|
||||
if (!req.user.findBookmark(libraryItem.id, time)) {
|
||||
if (!req.user.findBookmark(req.params.id, time)) {
|
||||
Logger.error(`[MeController] removeBookmark not found`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
req.user.removeBookmark(libraryItem.id, time)
|
||||
|
||||
req.user.removeBookmark(req.params.id, time)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
|
@ -190,7 +193,8 @@ class MeController {
|
|||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
||||
continue
|
||||
}
|
||||
const libraryItem = Database.getLibraryItem(localProgress.libraryItemId)
|
||||
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(localProgress.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
||||
continue
|
||||
|
@ -242,13 +246,15 @@ class MeController {
|
|||
}
|
||||
|
||||
// GET: api/me/items-in-progress
|
||||
getAllLibraryItemsInProgress(req, res) {
|
||||
async getAllLibraryItemsInProgress(req, res) {
|
||||
const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25
|
||||
|
||||
let itemsInProgress = []
|
||||
// TODO: More efficient to do this in a single query
|
||||
for (const mediaProgress of req.user.mediaProgress) {
|
||||
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||
const libraryItem = Database.getLibraryItem(mediaProgress.libraryItemId)
|
||||
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(mediaProgress.libraryItemId)
|
||||
if (libraryItem) {
|
||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||
|
@ -278,7 +284,7 @@ class MeController {
|
|||
|
||||
// GET: api/me/series/:id/remove-from-continue-listening
|
||||
async removeSeriesFromContinueListening(req, res) {
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
const series = await Database.seriesModel.getOldById(req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -294,7 +300,7 @@ class MeController {
|
|||
|
||||
// GET: api/me/series/:id/readd-to-continue-listening
|
||||
async readdSeriesFromContinueListening(req, res) {
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
const series = await Database.seriesModel.getOldById(req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -310,9 +316,19 @@ class MeController {
|
|||
|
||||
// GET: api/me/progress/:id/remove-from-continue-listening
|
||||
async removeItemFromContinueListening(req, res) {
|
||||
const mediaProgress = req.user.mediaProgress.find(mp => mp.id === req.params.id)
|
||||
if (!mediaProgress) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await Database.updateUser(req.user)
|
||||
await Database.mediaProgressModel.update({
|
||||
hideFromContinueListening: true
|
||||
}, {
|
||||
where: {
|
||||
id: mediaProgress.id
|
||||
}
|
||||
})
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const Sequelize = require('sequelize')
|
||||
const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const filePerms = require('../utils/filePerms')
|
||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||
const { isObject } = require('../utils/index')
|
||||
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
||||
|
||||
//
|
||||
// This is a controller for routes that don't have a home yet :(
|
||||
|
@ -14,7 +15,12 @@ const { isObject } = require('../utils/index')
|
|||
class MiscController {
|
||||
constructor() { }
|
||||
|
||||
// POST: api/upload
|
||||
/**
|
||||
* POST: /api/upload
|
||||
* Update library item
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async handleUpload(req, res) {
|
||||
if (!req.user.canUpload) {
|
||||
Logger.warn('User attempted to upload without permission', req.user)
|
||||
|
@ -31,7 +37,7 @@ class MiscController {
|
|||
const libraryId = req.body.library
|
||||
const folderId = req.body.folder
|
||||
|
||||
const library = await Database.models.library.getOldById(libraryId)
|
||||
const library = await Database.libraryModel.getOldById(libraryId)
|
||||
if (!library) {
|
||||
return res.status(404).send(`Library not found with id ${libraryId}`)
|
||||
}
|
||||
|
@ -83,12 +89,15 @@ class MiscController {
|
|||
})
|
||||
}
|
||||
|
||||
await filePerms.setDefault(firstDirPath)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// GET: api/tasks
|
||||
/**
|
||||
* GET: /api/tasks
|
||||
* Get tasks for task manager
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
getTasks(req, res) {
|
||||
const includeArray = (req.query.include || '').split(',')
|
||||
|
||||
|
@ -105,7 +114,12 @@ class MiscController {
|
|||
res.json(data)
|
||||
}
|
||||
|
||||
// PATCH: api/settings (admin)
|
||||
/**
|
||||
* PATCH: /api/settings
|
||||
* Update server settings
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async updateServerSettings(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error('User other than admin attempting to update server settings', req.user)
|
||||
|
@ -113,7 +127,7 @@ class MiscController {
|
|||
}
|
||||
const settingsUpdate = req.body
|
||||
if (!settingsUpdate || !isObject(settingsUpdate)) {
|
||||
return res.status(500).send('Invalid settings update object')
|
||||
return res.status(400).send('Invalid settings update object')
|
||||
}
|
||||
|
||||
const madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||
|
@ -131,6 +145,103 @@ class MiscController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH: /api/sorting-prefixes
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async updateSortingPrefixes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error('User other than admin attempting to update server sorting prefixes', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
let sortingPrefixes = req.body.sortingPrefixes
|
||||
if (!sortingPrefixes?.length || !Array.isArray(sortingPrefixes)) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
sortingPrefixes = [...new Set(sortingPrefixes.map(p => p?.trim?.().toLowerCase()).filter(p => p))]
|
||||
if (!sortingPrefixes.length) {
|
||||
return res.status(400).send('Invalid sortingPrefixes in request body')
|
||||
}
|
||||
|
||||
Logger.debug(`[MiscController] Updating sorting prefixes ${sortingPrefixes.join(', ')}`)
|
||||
Database.serverSettings.sortingPrefixes = sortingPrefixes
|
||||
await Database.updateServerSettings()
|
||||
|
||||
let rowsUpdated = 0
|
||||
// Update titleIgnorePrefix column on books
|
||||
const books = await Database.bookModel.findAll({
|
||||
attributes: ['id', 'title', 'titleIgnorePrefix']
|
||||
})
|
||||
const bulkUpdateBooks = []
|
||||
books.forEach((book) => {
|
||||
const titleIgnorePrefix = getTitleIgnorePrefix(book.title)
|
||||
if (titleIgnorePrefix !== book.titleIgnorePrefix) {
|
||||
bulkUpdateBooks.push({
|
||||
id: book.id,
|
||||
titleIgnorePrefix
|
||||
})
|
||||
}
|
||||
})
|
||||
if (bulkUpdateBooks.length) {
|
||||
Logger.info(`[MiscController] Updating titleIgnorePrefix on ${bulkUpdateBooks.length} books`)
|
||||
rowsUpdated += bulkUpdateBooks.length
|
||||
await Database.bookModel.bulkCreate(bulkUpdateBooks, {
|
||||
updateOnDuplicate: ['titleIgnorePrefix']
|
||||
})
|
||||
}
|
||||
|
||||
// Update titleIgnorePrefix column on podcasts
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
attributes: ['id', 'title', 'titleIgnorePrefix']
|
||||
})
|
||||
const bulkUpdatePodcasts = []
|
||||
podcasts.forEach((podcast) => {
|
||||
const titleIgnorePrefix = getTitleIgnorePrefix(podcast.title)
|
||||
if (titleIgnorePrefix !== podcast.titleIgnorePrefix) {
|
||||
bulkUpdatePodcasts.push({
|
||||
id: podcast.id,
|
||||
titleIgnorePrefix
|
||||
})
|
||||
}
|
||||
})
|
||||
if (bulkUpdatePodcasts.length) {
|
||||
Logger.info(`[MiscController] Updating titleIgnorePrefix on ${bulkUpdatePodcasts.length} podcasts`)
|
||||
rowsUpdated += bulkUpdatePodcasts.length
|
||||
await Database.podcastModel.bulkCreate(bulkUpdatePodcasts, {
|
||||
updateOnDuplicate: ['titleIgnorePrefix']
|
||||
})
|
||||
}
|
||||
|
||||
// Update nameIgnorePrefix column on series
|
||||
const allSeries = await Database.seriesModel.findAll({
|
||||
attributes: ['id', 'name', 'nameIgnorePrefix']
|
||||
})
|
||||
const bulkUpdateSeries = []
|
||||
allSeries.forEach((series) => {
|
||||
const nameIgnorePrefix = getTitleIgnorePrefix(series.name)
|
||||
if (nameIgnorePrefix !== series.nameIgnorePrefix) {
|
||||
bulkUpdateSeries.push({
|
||||
id: series.id,
|
||||
nameIgnorePrefix
|
||||
})
|
||||
}
|
||||
})
|
||||
if (bulkUpdateSeries.length) {
|
||||
Logger.info(`[MiscController] Updating nameIgnorePrefix on ${bulkUpdateSeries.length} series`)
|
||||
rowsUpdated += bulkUpdateSeries.length
|
||||
await Database.seriesModel.bulkCreate(bulkUpdateSeries, {
|
||||
updateOnDuplicate: ['nameIgnorePrefix']
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
rowsUpdated,
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/authorize
|
||||
* Used to authorize an API token
|
||||
|
@ -147,26 +258,55 @@ class MiscController {
|
|||
res.json(userResponse)
|
||||
}
|
||||
|
||||
// GET: api/tags
|
||||
getAllTags(req, res) {
|
||||
/**
|
||||
* GET: /api/tags
|
||||
* Get all tags
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async getAllTags(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const tags = []
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.media.tags && li.media.tags.length) {
|
||||
li.media.tags.forEach((tag) => {
|
||||
if (!tags.includes(tag)) tags.push(tag)
|
||||
})
|
||||
}
|
||||
const books = await Database.bookModel.findAll({
|
||||
attributes: ['tags'],
|
||||
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
})
|
||||
})
|
||||
for (const book of books) {
|
||||
for (const tag of book.tags) {
|
||||
if (!tags.includes(tag)) tags.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
attributes: ['tags'],
|
||||
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
})
|
||||
})
|
||||
for (const podcast of podcasts) {
|
||||
for (const tag of podcast.tags) {
|
||||
if (!tags.includes(tag)) tags.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
tags: tags
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/tags/rename
|
||||
/**
|
||||
* POST: /api/tags/rename
|
||||
* Rename tag
|
||||
* Req.body { tag, newTag }
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async renameTag(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to renameTag`)
|
||||
|
@ -183,19 +323,26 @@ class MiscController {
|
|||
let tagMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
// Update filter data
|
||||
Database.replaceTagInFilterData(tag, newTag)
|
||||
|
||||
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
||||
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag, newTag])
|
||||
for (const libraryItem of libraryItemsWithTag) {
|
||||
if (libraryItem.media.tags.includes(newTag)) {
|
||||
tagMerged = true // new tag is an existing tag so this is a merge
|
||||
}
|
||||
|
||||
if (li.media.tags.includes(tag)) {
|
||||
li.media.tags = li.media.tags.filter(t => t !== tag) // Remove old tag
|
||||
if (!li.media.tags.includes(newTag)) {
|
||||
li.media.tags.push(newTag) // Add new tag
|
||||
if (libraryItem.media.tags.includes(tag)) {
|
||||
libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag) // Remove old tag
|
||||
if (!libraryItem.media.tags.includes(newTag)) {
|
||||
libraryItem.media.tags.push(newTag)
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${libraryItem.media.title}"`)
|
||||
await libraryItem.media.update({
|
||||
tags: libraryItem.media.tags
|
||||
})
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
}
|
||||
|
@ -206,7 +353,13 @@ class MiscController {
|
|||
})
|
||||
}
|
||||
|
||||
// DELETE: api/tags/:tag
|
||||
/**
|
||||
* DELETE: /api/tags/:tag
|
||||
* Remove a tag
|
||||
* :tag param is base64 encoded
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async deleteTag(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to deleteTag`)
|
||||
|
@ -215,17 +368,23 @@ class MiscController {
|
|||
|
||||
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
// Get all items with tag
|
||||
const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag])
|
||||
|
||||
if (li.media.tags.includes(tag)) {
|
||||
li.media.tags = li.media.tags.filter(t => t !== tag)
|
||||
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
// Update filterdata
|
||||
Database.removeTagFromFilterData(tag)
|
||||
|
||||
let numItemsUpdated = 0
|
||||
// Remove tag from items
|
||||
for (const libraryItem of libraryItemsWithTag) {
|
||||
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${libraryItem.media.title}"`)
|
||||
libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag)
|
||||
await libraryItem.media.update({
|
||||
tags: libraryItem.media.tags
|
||||
})
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
@ -233,26 +392,54 @@ class MiscController {
|
|||
})
|
||||
}
|
||||
|
||||
// GET: api/genres
|
||||
getAllGenres(req, res) {
|
||||
/**
|
||||
* GET: /api/genres
|
||||
* Get all genres
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async getAllGenres(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
const genres = []
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.media.metadata.genres && li.media.metadata.genres.length) {
|
||||
li.media.metadata.genres.forEach((genre) => {
|
||||
if (!genres.includes(genre)) genres.push(genre)
|
||||
})
|
||||
}
|
||||
const books = await Database.bookModel.findAll({
|
||||
attributes: ['genres'],
|
||||
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
})
|
||||
})
|
||||
for (const book of books) {
|
||||
for (const tag of book.genres) {
|
||||
if (!genres.includes(tag)) genres.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
attributes: ['genres'],
|
||||
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
})
|
||||
})
|
||||
for (const podcast of podcasts) {
|
||||
for (const tag of podcast.genres) {
|
||||
if (!genres.includes(tag)) genres.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
genres
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/genres/rename
|
||||
/**
|
||||
* POST: /api/genres/rename
|
||||
* Rename genres
|
||||
* Req.body { genre, newGenre }
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async renameGenre(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to renameGenre`)
|
||||
|
@ -269,19 +456,26 @@ class MiscController {
|
|||
let genreMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
// Update filter data
|
||||
Database.replaceGenreInFilterData(genre, newGenre)
|
||||
|
||||
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
||||
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre, newGenre])
|
||||
for (const libraryItem of libraryItemsWithGenre) {
|
||||
if (libraryItem.media.genres.includes(newGenre)) {
|
||||
genreMerged = true // new genre is an existing genre so this is a merge
|
||||
}
|
||||
|
||||
if (li.media.metadata.genres.includes(genre)) {
|
||||
li.media.metadata.genres = li.media.metadata.genres.filter(g => g !== genre) // Remove old genre
|
||||
if (!li.media.metadata.genres.includes(newGenre)) {
|
||||
li.media.metadata.genres.push(newGenre) // Add new genre
|
||||
if (libraryItem.media.genres.includes(genre)) {
|
||||
libraryItem.media.genres = libraryItem.media.genres.filter(t => t !== genre) // Remove old genre
|
||||
if (!libraryItem.media.genres.includes(newGenre)) {
|
||||
libraryItem.media.genres.push(newGenre)
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${libraryItem.media.title}"`)
|
||||
await libraryItem.media.update({
|
||||
genres: libraryItem.media.genres
|
||||
})
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
}
|
||||
|
@ -292,7 +486,13 @@ class MiscController {
|
|||
})
|
||||
}
|
||||
|
||||
// DELETE: api/genres/:genre
|
||||
/**
|
||||
* DELETE: /api/genres/:genre
|
||||
* Remove a genre
|
||||
* :genre param is base64 encoded
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async deleteGenre(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`)
|
||||
|
@ -301,17 +501,23 @@ class MiscController {
|
|||
|
||||
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
// Update filter data
|
||||
Database.removeGenreFromFilterData(genre)
|
||||
|
||||
if (li.media.metadata.genres.includes(genre)) {
|
||||
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre)
|
||||
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
// Get all items with genre
|
||||
const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre])
|
||||
|
||||
let numItemsUpdated = 0
|
||||
// Remove genre from items
|
||||
for (const libraryItem of libraryItemsWithGenre) {
|
||||
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${libraryItem.media.title}"`)
|
||||
libraryItem.media.genres = libraryItem.media.genres.filter(g => g !== genre)
|
||||
await libraryItem.media.update({
|
||||
genres: libraryItem.media.genres
|
||||
})
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
|
|
@ -7,71 +7,187 @@ const Playlist = require('../objects/Playlist')
|
|||
class PlaylistController {
|
||||
constructor() { }
|
||||
|
||||
// POST: api/playlists
|
||||
/**
|
||||
* POST: /api/playlists
|
||||
* Create playlist
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async create(req, res) {
|
||||
const newPlaylist = new Playlist()
|
||||
const oldPlaylist = new Playlist()
|
||||
req.body.userId = req.user.id
|
||||
const success = newPlaylist.setData(req.body)
|
||||
const success = oldPlaylist.setData(req.body)
|
||||
if (!success) {
|
||||
return res.status(400).send('Invalid playlist request data')
|
||||
}
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
|
||||
// Create Playlist record
|
||||
const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
|
||||
|
||||
// Lookup all library items in playlist
|
||||
const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId).filter(i => i)
|
||||
const libraryItemsInPlaylist = await Database.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: libraryItemIds
|
||||
}
|
||||
})
|
||||
|
||||
// Create playlistMediaItem records
|
||||
const mediaItemsToAdd = []
|
||||
let order = 1
|
||||
for (const mediaItemObj of oldPlaylist.items) {
|
||||
const libraryItem = libraryItemsInPlaylist.find(li => li.id === mediaItemObj.libraryItemId)
|
||||
if (!libraryItem) continue
|
||||
|
||||
mediaItemsToAdd.push({
|
||||
mediaItemId: mediaItemObj.episodeId || libraryItem.mediaId,
|
||||
mediaItemType: mediaItemObj.episodeId ? 'podcastEpisode' : 'book',
|
||||
playlistId: oldPlaylist.id,
|
||||
order: order++
|
||||
})
|
||||
}
|
||||
if (mediaItemsToAdd.length) {
|
||||
await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)
|
||||
}
|
||||
|
||||
const jsonExpanded = await newPlaylist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// GET: api/playlists
|
||||
/**
|
||||
* GET: /api/playlists
|
||||
* Get all playlists for user
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async findAllForUser(req, res) {
|
||||
const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id)
|
||||
const playlistsForUser = await Database.playlistModel.findAll({
|
||||
where: {
|
||||
userId: req.user.id
|
||||
}
|
||||
})
|
||||
const playlists = []
|
||||
for (const playlist of playlistsForUser) {
|
||||
const jsonExpanded = await playlist.getOldJsonExpanded()
|
||||
playlists.push(jsonExpanded)
|
||||
}
|
||||
res.json({
|
||||
playlists: playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
|
||||
playlists
|
||||
})
|
||||
}
|
||||
|
||||
// GET: api/playlists/:id
|
||||
findOne(req, res) {
|
||||
res.json(req.playlist.toJSONExpanded(Database.libraryItems))
|
||||
/**
|
||||
* GET: /api/playlists/:id
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// PATCH: api/playlists/:id
|
||||
/**
|
||||
* PATCH: /api/playlists/:id
|
||||
* Update playlist
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async update(req, res) {
|
||||
const playlist = req.playlist
|
||||
let wasUpdated = playlist.update(req.body)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
const updatedPlaylist = req.playlist.set(req.body)
|
||||
let wasUpdated = false
|
||||
const changed = updatedPlaylist.changed()
|
||||
if (changed?.length) {
|
||||
await req.playlist.save()
|
||||
Logger.debug(`[PlaylistController] Updated playlist ${req.playlist.id} keys [${changed.join(',')}]`)
|
||||
wasUpdated = true
|
||||
}
|
||||
|
||||
// If array of items is passed in then update order of playlist media items
|
||||
const libraryItemIds = req.body.items?.map(i => i.libraryItemId).filter(i => i) || []
|
||||
if (libraryItemIds.length) {
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: libraryItemIds
|
||||
}
|
||||
})
|
||||
const existingPlaylistMediaItems = await updatedPlaylist.getPlaylistMediaItems({
|
||||
order: [['order', 'ASC']]
|
||||
})
|
||||
|
||||
// Set an array of mediaItemId
|
||||
const newMediaItemIdOrder = []
|
||||
for (const item of req.body.items) {
|
||||
const libraryItem = libraryItems.find(li => li.id === item.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
continue
|
||||
}
|
||||
const mediaItemId = item.episodeId || libraryItem.mediaId
|
||||
newMediaItemIdOrder.push(mediaItemId)
|
||||
}
|
||||
|
||||
// Sort existing playlist media items into new order
|
||||
existingPlaylistMediaItems.sort((a, b) => {
|
||||
const aIndex = newMediaItemIdOrder.findIndex(i => i === a.mediaItemId)
|
||||
const bIndex = newMediaItemIdOrder.findIndex(i => i === b.mediaItemId)
|
||||
return aIndex - bIndex
|
||||
})
|
||||
|
||||
// Update order on playlistMediaItem records
|
||||
let order = 1
|
||||
for (const playlistMediaItem of existingPlaylistMediaItems) {
|
||||
if (playlistMediaItem.order !== order) {
|
||||
await playlistMediaItem.update({
|
||||
order
|
||||
})
|
||||
wasUpdated = true
|
||||
}
|
||||
order++
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = await updatedPlaylist.getOldJsonExpanded()
|
||||
if (wasUpdated) {
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
SocketAuthority.clientEmitter(updatedPlaylist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// DELETE: api/playlists/:id
|
||||
/**
|
||||
* DELETE: /api/playlists/:id
|
||||
* Remove playlist
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async delete(req, res) {
|
||||
const playlist = req.playlist
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
await req.playlist.destroy()
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/playlists/:id/item
|
||||
/**
|
||||
* POST: /api/playlists/:id/item
|
||||
* Add item to playlist
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async addItem(req, res) {
|
||||
const playlist = req.playlist
|
||||
const oldPlaylist = await Database.playlistModel.getById(req.playlist.id)
|
||||
const itemToAdd = req.body
|
||||
|
||||
if (!itemToAdd.libraryItemId) {
|
||||
return res.status(400).send('Request body has no libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(itemToAdd.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(400).send('Library item not found')
|
||||
}
|
||||
if (libraryItem.libraryId !== playlist.libraryId) {
|
||||
if (libraryItem.libraryId !== oldPlaylist.libraryId) {
|
||||
return res.status(400).send('Library item in different library')
|
||||
}
|
||||
if (playlist.containsItem(itemToAdd)) {
|
||||
if (oldPlaylist.containsItem(itemToAdd)) {
|
||||
return res.status(400).send('Item already in playlist')
|
||||
}
|
||||
if ((itemToAdd.episodeId && !libraryItem.isPodcast) || (libraryItem.isPodcast && !itemToAdd.episodeId)) {
|
||||
|
@ -81,160 +197,248 @@ class PlaylistController {
|
|||
return res.status(400).send('Episode not found in library item')
|
||||
}
|
||||
|
||||
playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId)
|
||||
|
||||
const playlistMediaItem = {
|
||||
playlistId: playlist.id,
|
||||
playlistId: oldPlaylist.id,
|
||||
mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
|
||||
mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: playlist.items.length
|
||||
order: oldPlaylist.items.length + 1
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylistMediaItem(playlistMediaItem)
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// DELETE: api/playlists/:id/item/:libraryItemId/:episodeId?
|
||||
/**
|
||||
* DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId?
|
||||
* Remove item from playlist
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async removeItem(req, res) {
|
||||
const playlist = req.playlist
|
||||
const itemToRemove = {
|
||||
libraryItemId: req.params.libraryItemId,
|
||||
episodeId: req.params.episodeId || null
|
||||
}
|
||||
if (!playlist.containsItem(itemToRemove)) {
|
||||
return res.sendStatus(404)
|
||||
const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId)
|
||||
if (!oldLibraryItem) {
|
||||
return res.status(404).send('Library item not found')
|
||||
}
|
||||
|
||||
playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId)
|
||||
// Get playlist media items
|
||||
const mediaItemId = req.params.episodeId || oldLibraryItem.media.id
|
||||
const playlistMediaItems = await req.playlist.getPlaylistMediaItems({
|
||||
order: [['order', 'ASC']]
|
||||
})
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
// Check if media item to delete is in playlist
|
||||
const mediaItemToRemove = playlistMediaItems.find(pmi => pmi.mediaItemId === mediaItemId)
|
||||
if (!mediaItemToRemove) {
|
||||
return res.status(404).send('Media item not found in playlist')
|
||||
}
|
||||
|
||||
// Remove record
|
||||
await mediaItemToRemove.destroy()
|
||||
|
||||
// Update playlist media items order
|
||||
let order = 1
|
||||
for (const mediaItem of playlistMediaItems) {
|
||||
if (mediaItem.mediaItemId === mediaItemId) continue
|
||||
if (mediaItem.order !== order) {
|
||||
await mediaItem.update({
|
||||
order
|
||||
})
|
||||
}
|
||||
order++
|
||||
}
|
||||
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
|
||||
// Playlist is removed when there are no items
|
||||
if (!playlist.items.length) {
|
||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
if (!jsonExpanded.items.length) {
|
||||
Logger.info(`[PlaylistController] Playlist "${jsonExpanded.name}" has no more items - removing it`)
|
||||
await req.playlist.destroy()
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// POST: api/playlists/:id/batch/add
|
||||
/**
|
||||
* POST: /api/playlists/:id/batch/add
|
||||
* Batch add playlist items
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async addBatch(req, res) {
|
||||
const playlist = req.playlist
|
||||
if (!req.body.items || !req.body.items.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
if (!req.body.items?.length) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
const itemsToAdd = req.body.items
|
||||
let hasUpdated = false
|
||||
|
||||
let order = playlist.items.length
|
||||
const playlistMediaItems = []
|
||||
const libraryItemIds = itemsToAdd.map(i => i.libraryItemId).filter(i => i)
|
||||
if (!libraryItemIds.length) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
|
||||
// Find all library items
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: libraryItemIds
|
||||
}
|
||||
})
|
||||
|
||||
// Get all existing playlist media items
|
||||
const existingPlaylistMediaItems = await req.playlist.getPlaylistMediaItems({
|
||||
order: [['order', 'ASC']]
|
||||
})
|
||||
|
||||
const mediaItemsToAdd = []
|
||||
|
||||
// Setup array of playlistMediaItem records to add
|
||||
let order = existingPlaylistMediaItems.length + 1
|
||||
for (const item of itemsToAdd) {
|
||||
if (!item.libraryItemId) {
|
||||
return res.status(400).send('Item does not have libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = Database.getLibraryItem(item.libraryItemId)
|
||||
const libraryItem = libraryItems.find(li => li.id === item.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(400).send('Item not found with id ' + item.libraryItemId)
|
||||
}
|
||||
|
||||
if (!playlist.containsItem(item)) {
|
||||
playlistMediaItems.push({
|
||||
playlistId: playlist.id,
|
||||
mediaItemId: item.episodeId || libraryItem.media.id, // podcastEpisodeId or bookId
|
||||
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: order++
|
||||
})
|
||||
playlist.addItem(item.libraryItemId, item.episodeId)
|
||||
hasUpdated = true
|
||||
return res.status(404).send('Item not found with id ' + item.libraryItemId)
|
||||
} else {
|
||||
const mediaItemId = item.episodeId || libraryItem.mediaId
|
||||
if (existingPlaylistMediaItems.some(pmi => pmi.mediaItemId === mediaItemId)) {
|
||||
// Already exists in playlist
|
||||
continue
|
||||
} else {
|
||||
mediaItemsToAdd.push({
|
||||
playlistId: req.playlist.id,
|
||||
mediaItemId,
|
||||
mediaItemType: item.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: order++
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (hasUpdated) {
|
||||
await Database.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
let jsonExpanded = null
|
||||
if (mediaItemsToAdd.length) {
|
||||
await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)
|
||||
jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(req.playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
} else {
|
||||
jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// POST: api/playlists/:id/batch/remove
|
||||
/**
|
||||
* POST: /api/playlists/:id/batch/remove
|
||||
* Batch remove playlist items
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async removeBatch(req, res) {
|
||||
const playlist = req.playlist
|
||||
if (!req.body.items || !req.body.items.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
if (!req.body.items?.length) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
|
||||
const itemsToRemove = req.body.items
|
||||
const libraryItemIds = itemsToRemove.map(i => i.libraryItemId).filter(i => i)
|
||||
if (!libraryItemIds.length) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
|
||||
// Find all library items
|
||||
const libraryItems = await Database.libraryItemModel.findAll({
|
||||
where: {
|
||||
id: libraryItemIds
|
||||
}
|
||||
})
|
||||
|
||||
// Get all existing playlist media items for playlist
|
||||
const existingPlaylistMediaItems = await req.playlist.getPlaylistMediaItems({
|
||||
order: [['order', 'ASC']]
|
||||
})
|
||||
let numMediaItems = existingPlaylistMediaItems.length
|
||||
|
||||
// Remove playlist media items
|
||||
let hasUpdated = false
|
||||
for (const item of itemsToRemove) {
|
||||
if (!item.libraryItemId) {
|
||||
return res.status(400).send('Item does not have libraryItemId')
|
||||
}
|
||||
|
||||
if (playlist.containsItem(item)) {
|
||||
playlist.removeItem(item.libraryItemId, item.episodeId)
|
||||
hasUpdated = true
|
||||
}
|
||||
const libraryItem = libraryItems.find(li => li.id === item.libraryItemId)
|
||||
if (!libraryItem) continue
|
||||
const mediaItemId = item.episodeId || libraryItem.mediaId
|
||||
const existingMediaItem = existingPlaylistMediaItems.find(pmi => pmi.mediaItemId === mediaItemId)
|
||||
if (!existingMediaItem) continue
|
||||
await existingMediaItem.destroy()
|
||||
hasUpdated = true
|
||||
numMediaItems--
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
if (hasUpdated) {
|
||||
// Playlist is removed when there are no items
|
||||
if (!playlist.items.length) {
|
||||
Logger.info(`[PlaylistController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
if (!numMediaItems) {
|
||||
Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
|
||||
await req.playlist.destroy()
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// POST: api/playlists/collection/:collectionId
|
||||
/**
|
||||
* POST: /api/playlists/collection/:collectionId
|
||||
* Create a playlist from a collection
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async createFromCollection(req, res) {
|
||||
let collection = await Database.models.collection.getById(req.params.collectionId)
|
||||
const collection = await Database.collectionModel.findByPk(req.params.collectionId)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
// Expand collection to get library items
|
||||
collection = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Filter out library items not accessible to user
|
||||
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||
|
||||
if (!libraryItems.length) {
|
||||
return res.status(400).send('Collection has no books accessible to user')
|
||||
const collectionExpanded = await collection.getOldJsonExpanded(req.user)
|
||||
if (!collectionExpanded) {
|
||||
// This can happen if the user has no access to all items in collection
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
||||
const newPlaylist = new Playlist()
|
||||
// Playlists cannot be empty
|
||||
if (!collectionExpanded.books.length) {
|
||||
return res.status(400).send('Collection has no books')
|
||||
}
|
||||
|
||||
const newPlaylistData = {
|
||||
const oldPlaylist = new Playlist()
|
||||
oldPlaylist.setData({
|
||||
userId: req.user.id,
|
||||
libraryId: collection.libraryId,
|
||||
name: collection.name,
|
||||
description: collection.description || null,
|
||||
items: libraryItems.map(li => ({ libraryItemId: li.id }))
|
||||
}
|
||||
newPlaylist.setData(newPlaylistData)
|
||||
description: collection.description || null
|
||||
})
|
||||
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
// Create Playlist record
|
||||
const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist)
|
||||
|
||||
// Create PlaylistMediaItem records
|
||||
const mediaItemsToAdd = []
|
||||
let order = 1
|
||||
for (const libraryItem of collectionExpanded.books) {
|
||||
mediaItemsToAdd.push({
|
||||
playlistId: newPlaylist.id,
|
||||
mediaItemId: libraryItem.media.id,
|
||||
mediaItemType: 'book',
|
||||
order: order++
|
||||
})
|
||||
}
|
||||
await Database.createBulkPlaylistMediaItems(mediaItemsToAdd)
|
||||
|
||||
const jsonExpanded = await newPlaylist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
async middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
const playlist = await Database.models.playlist.getById(req.params.id)
|
||||
const playlist = await Database.playlistModel.findByPk(req.params.id)
|
||||
if (!playlist) {
|
||||
return res.status(404).send('Playlist not found')
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@ const fs = require('../libs/fsExtra')
|
|||
|
||||
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
||||
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
|
||||
const Scanner = require('../scanner/Scanner')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
|
||||
const LibraryItem = require('../objects/LibraryItem')
|
||||
|
||||
|
@ -19,7 +21,7 @@ class PodcastController {
|
|||
}
|
||||
const payload = req.body
|
||||
|
||||
const library = await Database.models.library.getOldById(payload.libraryId)
|
||||
const library = await Database.libraryModel.getOldById(payload.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||
return res.status(404).send('Library not found')
|
||||
|
@ -34,9 +36,13 @@ class PodcastController {
|
|||
const podcastPath = filePathToPOSIX(payload.path)
|
||||
|
||||
// Check if a library item with this podcast folder exists already
|
||||
const existingLibraryItem = Database.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||
const existingLibraryItem = (await Database.libraryItemModel.count({
|
||||
where: {
|
||||
path: podcastPath
|
||||
}
|
||||
})) > 0
|
||||
if (existingLibraryItem) {
|
||||
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
||||
Logger.error(`[PodcastController] Podcast already exists at path "${podcastPath}"`)
|
||||
return res.status(400).send('Podcast already exists')
|
||||
}
|
||||
|
||||
|
@ -45,7 +51,6 @@ class PodcastController {
|
|||
return false
|
||||
})
|
||||
if (!success) return res.status(400).send('Invalid podcast path')
|
||||
await filePerms.setDefault(podcastPath)
|
||||
|
||||
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||
|
||||
|
@ -71,7 +76,7 @@ class PodcastController {
|
|||
if (payload.media.metadata.imageUrl) {
|
||||
// TODO: Scan cover image to library files
|
||||
// Podcast cover will always go into library item folder
|
||||
const coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true)
|
||||
const coverResponse = await CoverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true)
|
||||
if (coverResponse) {
|
||||
if (coverResponse.error) {
|
||||
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
||||
|
@ -198,7 +203,7 @@ class PodcastController {
|
|||
}
|
||||
|
||||
const overrideDetails = req.query.override === '1'
|
||||
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||
const episodesUpdated = await Scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||
if (episodesUpdated) {
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
|
@ -268,23 +273,32 @@ class PodcastController {
|
|||
}
|
||||
|
||||
// Update/remove playlists that had this podcast episode
|
||||
const playlistsWithEpisode = await Database.models.playlist.getPlaylistsForMediaItemIds([episodeId])
|
||||
for (const playlist of playlistsWithEpisode) {
|
||||
playlist.removeItem(libraryItem.id, episodeId)
|
||||
const playlistMediaItems = await Database.playlistMediaItemModel.findAll({
|
||||
where: {
|
||||
mediaItemId: episodeId
|
||||
},
|
||||
include: {
|
||||
model: Database.playlistModel,
|
||||
include: Database.playlistMediaItemModel
|
||||
}
|
||||
})
|
||||
for (const pmi of playlistMediaItems) {
|
||||
const numItems = pmi.playlist.playlistMediaItems.length - 1
|
||||
|
||||
// If playlist is now empty then remove it
|
||||
if (!playlist.items.length) {
|
||||
if (!numItems) {
|
||||
Logger.info(`[PodcastController] Playlist "${playlist.name}" has no more items - removing it`)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems))
|
||||
const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
await pmi.playlist.destroy()
|
||||
} else {
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
|
||||
await pmi.destroy()
|
||||
const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove media progress for this episode
|
||||
const mediaProgressRemoved = await Database.models.mediaProgress.destroy({
|
||||
const mediaProgressRemoved = await Database.mediaProgressModel.destroy({
|
||||
where: {
|
||||
mediaItemId: episode.id
|
||||
}
|
||||
|
@ -298,9 +312,9 @@ class PodcastController {
|
|||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
async middleware(req, res, next) {
|
||||
const item = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!item?.media) return res.sendStatus(404)
|
||||
|
||||
if (!item.isPodcast) {
|
||||
return res.sendStatus(500)
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||
|
||||
class RSSFeedController {
|
||||
constructor() { }
|
||||
|
||||
async getAll(req, res) {
|
||||
const feeds = await this.rssFeedManager.getFeeds()
|
||||
res.json({
|
||||
feeds: feeds.map(f => f.toJSON()),
|
||||
minified: feeds.map(f => f.toJSONMinified())
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/item/:itemId/open
|
||||
async openRSSFeedForItem(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.itemId)
|
||||
const item = await Database.libraryItemModel.getOldById(req.params.itemId)
|
||||
if (!item) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
@ -45,7 +54,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.collectionModel.findByPk(req.params.collectionId)
|
||||
if (!collection) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
|
@ -60,7 +69,7 @@ class RSSFeedController {
|
|||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
const collectionExpanded = await collection.getOldJsonExpanded()
|
||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||
|
||||
// Check collection has audio tracks
|
||||
|
@ -79,7 +88,7 @@ class RSSFeedController {
|
|||
async openRSSFeedForSeries(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||
const series = await Database.seriesModel.getOldById(req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
|
@ -95,8 +104,9 @@ class RSSFeedController {
|
|||
}
|
||||
|
||||
const seriesJson = series.toJSON()
|
||||
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
|
||||
|
||||
// Check series has audio tracks
|
||||
if (!seriesJson.books.length) {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
const Logger = require("../Logger")
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const PodcastFinder = require('../finders/PodcastFinder')
|
||||
const AuthorFinder = require('../finders/AuthorFinder')
|
||||
const MusicFinder = require('../finders/MusicFinder')
|
||||
|
||||
class SearchController {
|
||||
constructor() { }
|
||||
|
@ -7,7 +11,7 @@ class SearchController {
|
|||
const provider = req.query.provider || 'google'
|
||||
const title = req.query.title || ''
|
||||
const author = req.query.author || ''
|
||||
const results = await this.bookFinder.search(provider, title, author)
|
||||
const results = await BookFinder.search(provider, title, author)
|
||||
res.json(results)
|
||||
}
|
||||
|
||||
|
@ -21,8 +25,8 @@ class SearchController {
|
|||
}
|
||||
|
||||
let results = null
|
||||
if (podcast) results = await this.podcastFinder.findCovers(query.title)
|
||||
else results = await this.bookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
||||
if (podcast) results = await PodcastFinder.findCovers(query.title)
|
||||
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
||||
res.json({
|
||||
results
|
||||
})
|
||||
|
@ -30,20 +34,20 @@ class SearchController {
|
|||
|
||||
async findPodcasts(req, res) {
|
||||
const term = req.query.term
|
||||
const results = await this.podcastFinder.search(term)
|
||||
const results = await PodcastFinder.search(term)
|
||||
res.json(results)
|
||||
}
|
||||
|
||||
async findAuthor(req, res) {
|
||||
const query = req.query.q
|
||||
const author = await this.authorFinder.findAuthorByName(query)
|
||||
const author = await AuthorFinder.findAuthorByName(query)
|
||||
res.json(author)
|
||||
}
|
||||
|
||||
async findChapters(req, res) {
|
||||
const asin = req.query.asin
|
||||
const region = (req.query.region || 'us').toLowerCase()
|
||||
const chapterData = await this.bookFinder.findChapters(asin, region)
|
||||
const chapterData = await BookFinder.findChapters(asin, region)
|
||||
if (!chapterData) {
|
||||
return res.json({ error: 'Chapters not found' })
|
||||
}
|
||||
|
@ -51,7 +55,7 @@ class SearchController {
|
|||
}
|
||||
|
||||
async findMusicTrack(req, res) {
|
||||
const tracks = await this.musicFinder.searchTrack(req.query || {})
|
||||
const tracks = await MusicFinder.searchTrack(req.query || {})
|
||||
res.json({
|
||||
tracks
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||
|
||||
class SeriesController {
|
||||
constructor() { }
|
||||
|
@ -25,7 +26,7 @@ class SeriesController {
|
|||
const libraryItemsInSeries = req.libraryItemsInSeries
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||
return mediaProgress && mediaProgress.isFinished
|
||||
return mediaProgress?.isFinished
|
||||
})
|
||||
seriesJson.progress = {
|
||||
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
||||
|
@ -42,17 +43,6 @@ class SeriesController {
|
|||
res.json(seriesJson)
|
||||
}
|
||||
|
||||
async search(req, res) {
|
||||
var q = (req.query.q || '').toLowerCase()
|
||||
if (!q) return res.json([])
|
||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||
var series = Database.series.filter(se => se.name.toLowerCase().includes(q))
|
||||
series = series.slice(0, limit)
|
||||
res.json({
|
||||
results: series
|
||||
})
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
const hasUpdated = req.series.update(req.body)
|
||||
if (hasUpdated) {
|
||||
|
@ -62,18 +52,17 @@ class SeriesController {
|
|||
res.json(req.series.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
async middleware(req, res, next) {
|
||||
const series = await Database.seriesModel.getOldById(req.params.id)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
/**
|
||||
* Filter out any library items not accessible to user
|
||||
*/
|
||||
const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||
const libraryItemsAccessible = libraryItems.filter(li => req.user.checkCanAccessLibraryItem(li))
|
||||
if (libraryItems.length && !libraryItemsAccessible.length) {
|
||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user)
|
||||
return res.sendStatus(403)
|
||||
const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
||||
if (!libraryItems.length) {
|
||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
@ -85,7 +74,7 @@ class SeriesController {
|
|||
}
|
||||
|
||||
req.series = series
|
||||
req.libraryItemsInSeries = libraryItemsAccessible
|
||||
req.libraryItemsInSeries = libraryItems
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ class SessionController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
|
||||
const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects()
|
||||
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
||||
return {
|
||||
...se.toJSON(),
|
||||
|
@ -62,9 +62,9 @@ class SessionController {
|
|||
})
|
||||
}
|
||||
|
||||
getOpenSession(req, res) {
|
||||
var libraryItem = Database.getLibraryItem(req.session.libraryItemId)
|
||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
async getOpenSession(req, res) {
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(req.session.libraryItemId)
|
||||
const sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
res.json(sessionForClient)
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class ToolsController {
|
|||
|
||||
const libraryItems = []
|
||||
for (const libraryItemId of libraryItemIds) {
|
||||
const libraryItem = Database.getLibraryItem(libraryItemId)
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -99,15 +99,15 @@ class ToolsController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
async middleware(req, res, next) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
const item = await Database.libraryItemModel.getOldById(req.params.id)
|
||||
if (!item?.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||
|
|
|
@ -17,7 +17,7 @@ class UserController {
|
|||
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
||||
|
||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||
const allUsers = await Database.models.user.getOldUsers()
|
||||
const allUsers = await Database.userModel.getOldUsers()
|
||||
const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||
|
||||
if (includes.includes('latestSession')) {
|
||||
|
@ -32,20 +32,67 @@ class UserController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/users/:id
|
||||
* Get a single user toJSONForBrowser
|
||||
* Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
|
||||
*
|
||||
* @param {import("express").Request} req
|
||||
* @param {import("express").Response} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error('User other than admin attempting to get user', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
|
||||
// Get user media progress with associated mediaItem
|
||||
const mediaProgresses = await Database.mediaProgressModel.findAll({
|
||||
where: {
|
||||
userId: req.reqUser.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Database.bookModel,
|
||||
attributes: ['id', 'title', 'coverPath', 'updatedAt']
|
||||
},
|
||||
{
|
||||
model: Database.podcastEpisodeModel,
|
||||
attributes: ['id', 'title'],
|
||||
include: {
|
||||
model: Database.podcastModel,
|
||||
attributes: ['id', 'title', 'coverPath', 'updatedAt']
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const oldMediaProgresses = mediaProgresses.map(mp => {
|
||||
const oldMediaProgress = mp.getOldMediaProgress()
|
||||
oldMediaProgress.displayTitle = mp.mediaItem?.title
|
||||
if (mp.mediaItem?.podcast) {
|
||||
oldMediaProgress.displaySubtitle = mp.mediaItem.podcast?.title
|
||||
oldMediaProgress.coverPath = mp.mediaItem.podcast?.coverPath
|
||||
oldMediaProgress.mediaUpdatedAt = mp.mediaItem.podcast?.updatedAt
|
||||
} else if (mp.mediaItem) {
|
||||
oldMediaProgress.coverPath = mp.mediaItem.coverPath
|
||||
oldMediaProgress.mediaUpdatedAt = mp.mediaItem.updatedAt
|
||||
}
|
||||
return oldMediaProgress
|
||||
})
|
||||
|
||||
const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot)
|
||||
|
||||
userJson.mediaProgress = oldMediaProgresses
|
||||
|
||||
res.json(userJson)
|
||||
}
|
||||
|
||||
async create(req, res) {
|
||||
const account = req.body
|
||||
const username = account.username
|
||||
|
||||
const usernameExists = await Database.models.user.getUserByUsername(username)
|
||||
const usernameExists = await Database.userModel.getUserByUsername(username)
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
|
@ -80,7 +127,7 @@ class UserController {
|
|||
var shouldUpdateToken = false
|
||||
|
||||
if (account.username !== undefined && account.username !== user.username) {
|
||||
const usernameExists = await Database.models.user.getUserByUsername(account.username)
|
||||
const usernameExists = await Database.userModel.getUserByUsername(account.username)
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
|
@ -122,9 +169,13 @@ class UserController {
|
|||
// Todo: check if user is logged in and cancel streams
|
||||
|
||||
// Remove user playlists
|
||||
const userPlaylists = await Database.models.playlist.getPlaylistsForUserAndLibrary(user.id)
|
||||
const userPlaylists = await Database.playlistModel.findAll({
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
for (const playlist of userPlaylists) {
|
||||
await Database.removePlaylist(playlist.id)
|
||||
await playlist.destroy()
|
||||
}
|
||||
|
||||
const userJson = user.toJSONForBrowser()
|
||||
|
@ -182,7 +233,7 @@ class UserController {
|
|||
}
|
||||
|
||||
if (req.params.id) {
|
||||
req.reqUser = await Database.models.user.getUserById(req.params.id)
|
||||
req.reqUser = await Database.userModel.getUserById(req.params.id)
|
||||
if (!req.reqUser) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue