Merge remote-tracking branch 'origin/master' into auth_passportjs

This commit is contained in:
lukeIam 2023-09-10 13:11:35 +00:00
commit f0f03efe17
138 changed files with 11777 additions and 7343 deletions

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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')
}

View file

@ -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')
}

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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({

View file

@ -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')
}

View file

@ -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)

View file

@ -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) {

View file

@ -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
})

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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)) {

View file

@ -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)
}