mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-03 09:44:41 +02:00
Merge remote-tracking branch 'origin/master' into auth_passportjs
This commit is contained in:
commit
dd9a3858d7
249 changed files with 15582 additions and 7835 deletions
|
@ -4,6 +4,7 @@ const { createNewSortInstance } = require('../libs/fastSort')
|
|||
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
|
||||
|
@ -14,18 +15,13 @@ class AuthorController {
|
|||
constructor() { }
|
||||
|
||||
async findOne(req, res) {
|
||||
const libraryId = req.query.library
|
||||
const include = (req.query.include || '').split(',')
|
||||
|
||||
const authorJson = req.author.toJSON()
|
||||
|
||||
// Used on author landing page to include library items and items grouped in series
|
||||
if (include.includes('items')) {
|
||||
authorJson.libraryItems = this.db.libraryItems.filter(li => {
|
||||
if (libraryId && li.libraryId !== libraryId) return false
|
||||
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
})
|
||||
authorJson.libraryItems = await Database.models.libraryItem.getForAuthor(req.author, req.user)
|
||||
|
||||
if (include.includes('series')) {
|
||||
const seriesMap = {}
|
||||
|
@ -97,25 +93,29 @@ 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 ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||
const existingAuthor = authorNameUpdate ? Database.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||
if (existingAuthor) {
|
||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
const bookAuthorsToCreate = []
|
||||
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
|
||||
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||
bookAuthorsToCreate.push({
|
||||
bookId: libraryItem.media.id,
|
||||
authorId: existingAuthor.id
|
||||
})
|
||||
})
|
||||
if (itemsWithAuthor.length) {
|
||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||
await Database.removeBulkBookAuthors(req.author.id) // Remove all old BookAuthor
|
||||
await Database.createBulkBookAuthors(bookAuthorsToCreate) // Create all new BookAuthor
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
// Remove old author
|
||||
await this.db.removeEntity('author', req.author.id)
|
||||
await Database.removeAuthor(req.author.id)
|
||||
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
||||
|
||||
// Send updated num books for merged author
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
|
||||
}).length
|
||||
const numBooks = await Database.models.libraryItem.getForAuthor(existingAuthor).length
|
||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||
|
||||
res.json({
|
||||
|
@ -130,22 +130,18 @@ class AuthorController {
|
|||
if (hasUpdated) {
|
||||
req.author.updatedAt = Date.now()
|
||||
|
||||
const itemsWithAuthor = await Database.models.libraryItem.getForAuthor(req.author)
|
||||
if (authorNameUpdate) { // Update author name on all books
|
||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
itemsWithAuthor.forEach(libraryItem => {
|
||||
libraryItem.media.metadata.updateAuthor(req.author)
|
||||
})
|
||||
if (itemsWithAuthor.length) {
|
||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||
SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.updateEntity('author', req.author)
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
}).length
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||
await Database.updateAuthor(req.author)
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(itemsWithAuthor.length))
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
@ -159,7 +155,7 @@ class AuthorController {
|
|||
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 = this.db.authors.filter(au => au.name.toLowerCase().includes(q))
|
||||
var authors = Database.authors.filter(au => au.name?.toLowerCase().includes(q))
|
||||
authors = authors.slice(0, limit)
|
||||
res.json({
|
||||
results: authors
|
||||
|
@ -204,10 +200,9 @@ class AuthorController {
|
|||
if (hasUpdates) {
|
||||
req.author.updatedAt = Date.now()
|
||||
|
||||
await this.db.updateEntity('author', req.author)
|
||||
const numBooks = this.db.libraryItems.filter(li => {
|
||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||
}).length
|
||||
await Database.updateAuthor(req.author)
|
||||
|
||||
const numBooks = await Database.models.libraryItem.getForAuthor(req.author).length
|
||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||
}
|
||||
|
||||
|
@ -238,7 +233,7 @@ class AuthorController {
|
|||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var author = this.db.authors.find(au => au.id === req.params.id)
|
||||
const author = Database.authors.find(au => au.id === req.params.id)
|
||||
if (!author) return res.sendStatus(404)
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
|
|
@ -14,18 +14,14 @@ class BackupController {
|
|||
}
|
||||
|
||||
async delete(req, res) {
|
||||
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||
if (!backup) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
await this.backupManager.removeBackup(backup)
|
||||
await this.backupManager.removeBackup(req.backup)
|
||||
|
||||
res.json({
|
||||
backups: this.backupManager.backups.map(b => b.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async upload(req, res) {
|
||||
upload(req, res) {
|
||||
if (!req.files.file) {
|
||||
Logger.error('[BackupController] Upload backup invalid')
|
||||
return res.sendStatus(500)
|
||||
|
@ -33,13 +29,22 @@ class BackupController {
|
|||
this.backupManager.uploadBackup(req, res)
|
||||
}
|
||||
|
||||
async apply(req, res) {
|
||||
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||
if (!backup) {
|
||||
return res.sendStatus(404)
|
||||
/**
|
||||
* api/backups/:id/download
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
download(req, res) {
|
||||
if (global.XAccel) {
|
||||
Logger.debug(`Use X-Accel to serve static file ${req.backup.fullPath}`)
|
||||
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + req.backup.fullPath }).send()
|
||||
}
|
||||
await this.backupManager.requestApplyBackup(backup)
|
||||
res.sendStatus(200)
|
||||
res.sendFile(req.backup.fullPath)
|
||||
}
|
||||
|
||||
apply(req, res) {
|
||||
this.backupManager.requestApplyBackup(req.backup, res)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
|
@ -47,6 +52,14 @@ class BackupController {
|
|||
Logger.error(`[BackupController] Non-admin user attempting to access backups`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
req.backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||
if (!req.backup) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ class CacheController {
|
|||
if (!req.user.isAdminOrUp) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info(`[MiscController] Purging all cache`)
|
||||
await this.cacheManager.purgeAll()
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
@ -18,7 +17,6 @@ class CacheController {
|
|||
if (!req.user.isAdminOrUp) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info(`[MiscController] Purging items cache`)
|
||||
await this.cacheManager.purgeItems()
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const Collection = require('../objects/Collection')
|
||||
|
||||
|
@ -7,32 +8,34 @@ class CollectionController {
|
|||
constructor() { }
|
||||
|
||||
async create(req, res) {
|
||||
var newCollection = new Collection()
|
||||
const newCollection = new Collection()
|
||||
req.body.userId = req.user.id
|
||||
var success = newCollection.setData(req.body)
|
||||
if (!success) {
|
||||
if (!newCollection.setData(req.body)) {
|
||||
return res.status(500).send('Invalid collection data')
|
||||
}
|
||||
var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('collection', newCollection)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
findAll(req, res) {
|
||||
async findAll(req, res) {
|
||||
const collectionsExpanded = await Database.models.collection.getOldCollectionsJsonExpanded(req.user)
|
||||
res.json({
|
||||
collections: this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems))
|
||||
collections: collectionsExpanded
|
||||
})
|
||||
}
|
||||
|
||||
findOne(req, res) {
|
||||
async findOne(req, res) {
|
||||
const includeEntities = (req.query.include || '').split(',')
|
||||
|
||||
const collectionExpanded = req.collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
if (includeEntities.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||
collectionExpanded.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||
const feedData = await this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||
collectionExpanded.rssFeed = feedData?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
res.json(collectionExpanded)
|
||||
|
@ -41,9 +44,9 @@ class CollectionController {
|
|||
async update(req, res) {
|
||||
const collection = req.collection
|
||||
const wasUpdated = collection.update(req.body)
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
await Database.updateCollection(collection)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
|
@ -51,19 +54,19 @@ class CollectionController {
|
|||
|
||||
async delete(req, res) {
|
||||
const collection = req.collection
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Close rss feed - remove from db and emit socket event
|
||||
await this.rssFeedManager.closeFeedForEntityId(collection.id)
|
||||
|
||||
await this.db.removeEntity('collection', collection.id)
|
||||
await Database.removeCollection(collection.id)
|
||||
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async addBook(req, res) {
|
||||
const collection = req.collection
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === req.body.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(500).send('Book not found')
|
||||
}
|
||||
|
@ -74,8 +77,14 @@ class CollectionController {
|
|||
return res.status(500).send('Book already in collection')
|
||||
}
|
||||
collection.addBook(req.body.id)
|
||||
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
const collectionBook = {
|
||||
collectionId: collection.id,
|
||||
bookId: libraryItem.media.id,
|
||||
order: collection.books.length
|
||||
}
|
||||
await Database.createCollectionBook(collectionBook)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
@ -83,13 +92,18 @@ class CollectionController {
|
|||
// DELETE: api/collections/:id/book/:bookId
|
||||
async removeBook(req, res) {
|
||||
const collection = req.collection
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === req.params.bookId)
|
||||
if (!libraryItem) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (collection.books.includes(req.params.bookId)) {
|
||||
collection.removeBook(req.params.bookId)
|
||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('collection', collection)
|
||||
const jsonExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||
await Database.updateCollection(collection)
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// POST: api/collections/:id/batch/add
|
||||
|
@ -98,19 +112,30 @@ class CollectionController {
|
|||
if (!req.body.books || !req.body.books.length) {
|
||||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
var bookIdsToAdd = req.body.books
|
||||
var hasUpdated = false
|
||||
for (let i = 0; i < bookIdsToAdd.length; i++) {
|
||||
if (!collection.books.includes(bookIdsToAdd[i])) {
|
||||
collection.addBook(bookIdsToAdd[i])
|
||||
const bookIdsToAdd = req.body.books
|
||||
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)
|
||||
collectionBooksToAdd.push({
|
||||
collectionId: collection.id,
|
||||
bookId: libraryItem.media.id,
|
||||
order: order++
|
||||
})
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.createBulkCollectionBooks(collectionBooksToAdd)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// POST: api/collections/:id/batch/remove
|
||||
|
@ -120,23 +145,26 @@ class CollectionController {
|
|||
return res.status(500).send('Invalid request body')
|
||||
}
|
||||
var bookIdsToRemove = req.body.books
|
||||
var hasUpdated = false
|
||||
for (let i = 0; i < bookIdsToRemove.length; i++) {
|
||||
if (collection.books.includes(bookIdsToRemove[i])) {
|
||||
collection.removeBook(bookIdsToRemove[i])
|
||||
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)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('collection', collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
|
||||
await Database.updateCollection(collection)
|
||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
async middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
const collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
const collection = await Database.models.collection.getById(req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
|
87
server/controllers/EmailController.js
Normal file
87
server/controllers/EmailController.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class EmailController {
|
||||
constructor() { }
|
||||
|
||||
getSettings(req, res) {
|
||||
res.json({
|
||||
settings: Database.emailSettings
|
||||
})
|
||||
}
|
||||
|
||||
async updateSettings(req, res) {
|
||||
const updated = Database.emailSettings.update(req.body)
|
||||
if (updated) {
|
||||
await Database.updateSetting(Database.emailSettings)
|
||||
}
|
||||
res.json({
|
||||
settings: Database.emailSettings
|
||||
})
|
||||
}
|
||||
|
||||
async sendTest(req, res) {
|
||||
this.emailManager.sendTest(res)
|
||||
}
|
||||
|
||||
async updateEReaderDevices(req, res) {
|
||||
if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) {
|
||||
return res.status(400).send('Invalid payload. ereaderDevices array required')
|
||||
}
|
||||
|
||||
const ereaderDevices = req.body.ereaderDevices
|
||||
for (const device of ereaderDevices) {
|
||||
if (!device.name || !device.email) {
|
||||
return res.status(400).send('Invalid payload. ereaderDevices array items must have name and email')
|
||||
}
|
||||
}
|
||||
|
||||
const updated = Database.emailSettings.update({
|
||||
ereaderDevices
|
||||
})
|
||||
if (updated) {
|
||||
await Database.updateSetting(Database.emailSettings)
|
||||
SocketAuthority.adminEmitter('ereader-devices-updated', {
|
||||
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||
})
|
||||
}
|
||||
res.json({
|
||||
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Library item not found')
|
||||
}
|
||||
|
||||
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const ebookFile = libraryItem.media.ebookFile
|
||||
if (!ebookFile) {
|
||||
return res.status(404).send('EBook file not found')
|
||||
}
|
||||
|
||||
const device = Database.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||
if (!device) {
|
||||
return res.status(404).send('E-reader device not found')
|
||||
}
|
||||
|
||||
this.emailManager.sendEBookToDevice(ebookFile, device, res)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new EmailController()
|
|
@ -1,27 +1,50 @@
|
|||
const Logger = require('../Logger')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
class FileSystemController {
|
||||
constructor() { }
|
||||
|
||||
async getPaths(req, res) {
|
||||
var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
||||
return Path.sep + dirname
|
||||
})
|
||||
|
||||
// Do not include existing mapped library paths in response
|
||||
this.db.libraries.forEach(lib => {
|
||||
lib.folders.forEach((folder) => {
|
||||
var dir = folder.fullPath
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
excludedDirs.push(dir)
|
||||
})
|
||||
const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
|
||||
libraryFoldersPaths.forEach((path) => {
|
||||
let dir = path || ''
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
excludedDirs.push(dir)
|
||||
})
|
||||
|
||||
Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
|
||||
res.json({
|
||||
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/filesystem/pathexists
|
||||
async checkPathExists(req, res) {
|
||||
if (!req.user.canUpload) {
|
||||
Logger.error(`[FileSystemController] Non-admin user attempting to check path exists`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const filepath = req.body.filepath
|
||||
if (!filepath?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const exists = await fs.pathExists(filepath)
|
||||
res.json({
|
||||
exists
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = new FileSystemController()
|
|
@ -9,6 +9,9 @@ const { sort, createNewSortInstance } = require('../libs/fastSort')
|
|||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
||||
const Database = require('../Database')
|
||||
|
||||
class LibraryController {
|
||||
constructor() { }
|
||||
|
||||
|
@ -40,13 +43,16 @@ class LibraryController {
|
|||
}
|
||||
|
||||
const library = new Library()
|
||||
newLibraryPayload.displayOrder = this.db.libraries.length + 1
|
||||
|
||||
let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder()
|
||||
if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
|
||||
newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
|
||||
library.setData(newLibraryPayload)
|
||||
await this.db.insertEntity('library', library)
|
||||
await Database.createLibrary(library)
|
||||
|
||||
// Only emit to users with access to library
|
||||
const userFilter = (user) => {
|
||||
return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id)
|
||||
return user.checkCanAccessLibrary?.(library.id)
|
||||
}
|
||||
SocketAuthority.emitter('library_added', library.toJSON(), userFilter)
|
||||
|
||||
|
@ -56,16 +62,18 @@ class LibraryController {
|
|||
res.json(library)
|
||||
}
|
||||
|
||||
findAll(req, res) {
|
||||
async findAll(req, res) {
|
||||
const libraries = await Database.models.library.getAllOldLibraries()
|
||||
|
||||
const librariesAccessible = req.user.librariesAccessible || []
|
||||
if (librariesAccessible && librariesAccessible.length) {
|
||||
if (librariesAccessible.length) {
|
||||
return res.json({
|
||||
libraries: this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
||||
libraries: libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||
libraries: libraries.map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -75,7 +83,7 @@ class LibraryController {
|
|||
return res.json({
|
||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
||||
numUserPlaylists: this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
|
||||
numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
||||
library: req.library
|
||||
})
|
||||
}
|
||||
|
@ -128,14 +136,14 @@ class LibraryController {
|
|||
this.cronManager.updateLibraryScanCron(library)
|
||||
|
||||
// Remove libraryItems no longer in library
|
||||
const itemsToRemove = this.db.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||
const itemsToRemove = Database.libraryItems.filter(li => li.libraryId === library.id && !library.checkFullPathInLibrary(li.path))
|
||||
if (itemsToRemove.length) {
|
||||
Logger.info(`[Scanner] Updating library, removing ${itemsToRemove.length} items`)
|
||||
for (let i = 0; i < itemsToRemove.length; i++) {
|
||||
await this.handleDeleteLibraryItem(itemsToRemove[i])
|
||||
}
|
||||
}
|
||||
await this.db.updateEntity('library', library)
|
||||
await Database.updateLibrary(library)
|
||||
|
||||
// Only emit to users with access to library
|
||||
const userFilter = (user) => {
|
||||
|
@ -146,6 +154,12 @@ class LibraryController {
|
|||
return res.json(library.toJSON())
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE: /api/libraries/:id
|
||||
* Delete a library
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async delete(req, res) {
|
||||
const library = req.library
|
||||
|
||||
|
@ -153,28 +167,56 @@ class LibraryController {
|
|||
this.watcher.removeLibrary(library)
|
||||
|
||||
// Remove collections for library
|
||||
const collections = this.db.collections.filter(c => c.libraryId === library.id)
|
||||
for (const collection of collections) {
|
||||
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
|
||||
await this.db.removeEntity('collection', collection.id)
|
||||
const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id)
|
||||
if (numCollectionsRemoved) {
|
||||
Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
|
||||
}
|
||||
|
||||
// Remove items in this library
|
||||
const libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
||||
const libraryItems = Database.libraryItems.filter(li => li.libraryId === library.id)
|
||||
Logger.info(`[Server] deleting library "${library.name}" with ${libraryItems.length} items"`)
|
||||
for (let i = 0; i < libraryItems.length; i++) {
|
||||
await this.handleDeleteLibraryItem(libraryItems[i])
|
||||
}
|
||||
|
||||
const libraryJson = library.toJSON()
|
||||
await this.db.removeEntity('library', library.id)
|
||||
await Database.removeLibrary(library.id)
|
||||
|
||||
// Re-order libraries
|
||||
await Database.models.library.resetDisplayOrder()
|
||||
|
||||
SocketAuthority.emitter('library_removed', libraryJson)
|
||||
return res.json(libraryJson)
|
||||
}
|
||||
|
||||
async getLibraryItemsNew(req, res) {
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
total: undefined,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
mediaType: req.library.mediaType,
|
||||
minified: req.query.minified === '1',
|
||||
collapseseries: req.query.collapseseries === '1',
|
||||
include: include.join(',')
|
||||
}
|
||||
payload.offset = payload.page * payload.limit
|
||||
|
||||
const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library, req.user, payload)
|
||||
payload.results = libraryItems
|
||||
payload.total = count
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// api/libraries/:id/items
|
||||
// TODO: Optimize this method, items are iterated through several times but can be combined
|
||||
getLibraryItems(req, res) {
|
||||
async getLibraryItems(req, res) {
|
||||
let libraryItems = req.libraryItems
|
||||
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
@ -193,11 +235,12 @@ class LibraryController {
|
|||
include: include.join(',')
|
||||
}
|
||||
const mediaIsBook = payload.mediaType === 'book'
|
||||
const mediaIsPodcast = payload.mediaType === 'podcast'
|
||||
|
||||
// Step 1 - Filter the retrieved library items
|
||||
let filterSeries = null
|
||||
if (payload.filterBy) {
|
||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||
libraryItems = await libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
||||
payload.total = libraryItems.length
|
||||
|
||||
// Determining if we are filtering titles by a series, and if so, which series
|
||||
|
@ -209,7 +252,7 @@ class LibraryController {
|
|||
// If also filtering by series, will not collapse the filtered series as this would lead
|
||||
// to series having a collapsed series that is just that series.
|
||||
if (payload.collapseseries) {
|
||||
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries)
|
||||
let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, Database.series, filterSeries, req.library.settings.hideSingleBookSeries)
|
||||
|
||||
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
||||
libraryItems = collapsedItems
|
||||
|
@ -231,13 +274,12 @@ class LibraryController {
|
|||
const sortArray = []
|
||||
|
||||
// When on the series page, sort by sequence only
|
||||
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
||||
if (filterSeries && !payload.sortBy) {
|
||||
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
||||
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||
sortArray.push({
|
||||
asc: (li) => {
|
||||
if (this.db.serverSettings.sortingIgnorePrefix) {
|
||||
if (Database.serverSettings.sortingIgnorePrefix) {
|
||||
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
||||
} else {
|
||||
return li.collapsedSeries?.name || li.media.metadata.title
|
||||
|
@ -247,15 +289,11 @@ class LibraryController {
|
|||
}
|
||||
|
||||
if (payload.sortBy) {
|
||||
// old sort key TODO: should be mutated in dbMigration
|
||||
let sortKey = payload.sortBy
|
||||
if (sortKey.startsWith('book.')) {
|
||||
sortKey = sortKey.replace('book.', 'media.metadata.')
|
||||
}
|
||||
|
||||
// Handle server setting sortingIgnorePrefix
|
||||
const sortByTitle = sortKey === 'media.metadata.title'
|
||||
if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) {
|
||||
if (sortByTitle && Database.serverSettings.sortingIgnorePrefix) {
|
||||
// BookMetadata.js has titleIgnorePrefix getter
|
||||
sortKey += 'IgnorePrefix'
|
||||
}
|
||||
|
@ -267,7 +305,7 @@ class LibraryController {
|
|||
sortArray.push({
|
||||
asc: (li) => {
|
||||
if (li.collapsedSeries) {
|
||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||
return Database.serverSettings.sortingIgnorePrefix ?
|
||||
li.collapsedSeries.nameIgnorePrefix :
|
||||
li.collapsedSeries.name
|
||||
} else {
|
||||
|
@ -284,7 +322,7 @@ class LibraryController {
|
|||
if (mediaIsBook && sortBySequence) {
|
||||
return li.media.metadata.getSeries(filterSeries).sequence
|
||||
} else if (mediaIsBook && sortByTitle && li.collapsedSeries) {
|
||||
return this.db.serverSettings.sortingIgnorePrefix ?
|
||||
return Database.serverSettings.sortingIgnorePrefix ?
|
||||
li.collapsedSeries.nameIgnorePrefix :
|
||||
li.collapsedSeries.name
|
||||
} else {
|
||||
|
@ -318,7 +356,7 @@ class LibraryController {
|
|||
}
|
||||
|
||||
// Step 4 - Transform the items to pass to the client side
|
||||
payload.results = libraryItems.map(li => {
|
||||
payload.results = await Promise.all(libraryItems.map(async li => {
|
||||
const json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
||||
|
||||
if (li.collapsedSeries) {
|
||||
|
@ -355,10 +393,15 @@ class LibraryController {
|
|||
} else {
|
||||
// add rssFeed object if "include=rssfeed" was put in query string (only for non-collapsed series)
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(json.id)
|
||||
const feedData = await this.rssFeedManager.findFeedForEntityId(json.id)
|
||||
json.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||
}
|
||||
|
||||
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
|
||||
if (mediaIsPodcast && include.includes('numepisodesincomplete')) {
|
||||
json.numEpisodesIncomplete = req.user.getNumEpisodesIncompleteForPodcast(li)
|
||||
}
|
||||
|
||||
if (filterSeries) {
|
||||
// If filtering by series, make sure to include the series metadata
|
||||
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
||||
|
@ -366,7 +409,7 @@ class LibraryController {
|
|||
}
|
||||
|
||||
return json
|
||||
})
|
||||
}))
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
@ -387,7 +430,13 @@ class LibraryController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// api/libraries/:id/series
|
||||
/**
|
||||
* api/libraries/:id/series
|
||||
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async getAllSeriesForLibrary(req, res) {
|
||||
const libraryItems = req.libraryItems
|
||||
|
||||
|
@ -405,7 +454,7 @@ class LibraryController {
|
|||
include: include.join(',')
|
||||
}
|
||||
|
||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified)
|
||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, Database.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries)
|
||||
|
||||
const direction = payload.sortDesc ? 'desc' : 'asc'
|
||||
series = naturalSort(series).by([
|
||||
|
@ -422,7 +471,7 @@ class LibraryController {
|
|||
} else if (payload.sortBy === 'lastBookAdded') {
|
||||
return Math.max(...(se.books).map(x => x.addedAt), 0)
|
||||
} else { // sort by name
|
||||
return this.db.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||
return Database.serverSettings.sortingIgnorePrefix ? se.nameIgnorePrefixSort : se.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -437,21 +486,55 @@ class LibraryController {
|
|||
|
||||
// add rssFeed when "include=rssfeed" is in query string
|
||||
if (include.includes('rssfeed')) {
|
||||
series = series.map((se) => {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(se.id)
|
||||
series = await Promise.all(series.map(async (se) => {
|
||||
const feedData = await this.rssFeedManager.findFeedForEntityId(se.id)
|
||||
se.rssFeed = feedData?.toJSONMinified() || null
|
||||
return se
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
payload.results = series
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* api/libraries/:id/series/:seriesId
|
||||
*
|
||||
* Optional includes (e.g. `?include=rssfeed,progress`)
|
||||
* rssfeed: adds `rssFeed` to series object if a feed is open
|
||||
* progress: adds `progress` to series object with { libraryItemIds:Array<llid>, libraryItemIdsFinished:Array<llid>, isFinished:boolean }
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res - Series
|
||||
*/
|
||||
async getSeriesForLibrary(req, res) {
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
const libraryItemsInSeries = req.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||
|
||||
const seriesJson = series.toJSON()
|
||||
if (include.includes('progress')) {
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => !!req.user.getMediaProgress(li.id)?.isFinished)
|
||||
seriesJson.progress = {
|
||||
libraryItemIds: libraryItemsInSeries.map(li => li.id),
|
||||
libraryItemIdsFinished: libraryItemsFinished.map(li => li.id),
|
||||
isFinished: libraryItemsFinished.length >= libraryItemsInSeries.length
|
||||
}
|
||||
}
|
||||
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
res.json(seriesJson)
|
||||
}
|
||||
|
||||
// api/libraries/:id/collections
|
||||
async getCollectionsForLibrary(req, res) {
|
||||
const libraryItems = req.libraryItems
|
||||
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const payload = {
|
||||
|
@ -466,19 +549,8 @@ class LibraryController {
|
|||
include: include.join(',')
|
||||
}
|
||||
|
||||
let collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
||||
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||
|
||||
// If all books restricted to user in this collection then hide this collection
|
||||
if (!expanded.books.length && c.books.length) return null
|
||||
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(c.id)
|
||||
expanded.rssFeed = feedData?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
return expanded
|
||||
}).filter(c => !!c)
|
||||
// TODO: Create paginated queries
|
||||
let collections = await Database.models.collection.getOldCollectionsJsonExpanded(req.user, req.library.id, include)
|
||||
|
||||
payload.total = collections.length
|
||||
|
||||
|
@ -493,7 +565,8 @@ class LibraryController {
|
|||
|
||||
// api/libraries/:id/playlists
|
||||
async getUserPlaylistsForLibrary(req, res) {
|
||||
let playlistsForUser = this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
||||
let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
|
||||
playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
|
@ -517,7 +590,7 @@ class LibraryController {
|
|||
return res.status(400).send('Invalid library media type')
|
||||
}
|
||||
|
||||
let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||
let libraryItems = Database.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||
let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems)
|
||||
albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
|
||||
|
||||
|
@ -541,48 +614,68 @@ class LibraryController {
|
|||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||
}
|
||||
|
||||
// api/libraries/:id/personalized
|
||||
// New and improved personalized call only loops through library items once
|
||||
/**
|
||||
* GET: /api/libraries/:id/personalized2
|
||||
* TODO: new endpoint
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async getUserPersonalizedShelves(req, res) {
|
||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
const shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user, include, limitPerShelf)
|
||||
res.json(shelves)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/libraries/:id/personalized
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async getLibraryUserPersonalizedOptimal(req, res) {
|
||||
const mediaType = req.library.mediaType
|
||||
const libraryItems = req.libraryItems
|
||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const categories = libraryHelpers.buildPersonalizedShelves(this, req.user, libraryItems, mediaType, limitPerShelf, include)
|
||||
const categories = await libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include)
|
||||
res.json(categories)
|
||||
}
|
||||
|
||||
// PATCH: Change the order of libraries
|
||||
/**
|
||||
* POST: /api/libraries/order
|
||||
* Change the display order of libraries
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async reorder(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const libraries = await Database.models.library.getAllOldLibraries()
|
||||
|
||||
var orderdata = req.body
|
||||
var hasUpdates = false
|
||||
const orderdata = req.body
|
||||
let hasUpdates = false
|
||||
for (let i = 0; i < orderdata.length; i++) {
|
||||
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
||||
const library = libraries.find(lib => lib.id === orderdata[i].id)
|
||||
if (!library) {
|
||||
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
||||
hasUpdates = true
|
||||
await this.db.updateEntity('library', library)
|
||||
await Database.updateLibrary(library)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
this.db.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
libraries.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
Logger.debug(`[LibraryController] Updated library display orders`)
|
||||
} else {
|
||||
Logger.debug(`[LibraryController] Library orders were up to date`)
|
||||
}
|
||||
|
||||
res.json({
|
||||
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||
libraries: libraries.map(lib => lib.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -612,7 +705,7 @@ class LibraryController {
|
|||
if (queryResult.series?.length) {
|
||||
queryResult.series.forEach((se) => {
|
||||
if (!seriesMatches[se.id]) {
|
||||
const _series = this.db.series.find(_se => _se.id === se.id)
|
||||
const _series = Database.series.find(_se => _se.id === se.id)
|
||||
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
||||
} else {
|
||||
seriesMatches[se.id].books.push(li.toJSON())
|
||||
|
@ -622,7 +715,7 @@ class LibraryController {
|
|||
if (queryResult.authors?.length) {
|
||||
queryResult.authors.forEach((au) => {
|
||||
if (!authorMatches[au.id]) {
|
||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authorMatches[au.id] = _author.toJSON()
|
||||
authorMatches[au.id].numBooks = 1
|
||||
|
@ -689,7 +782,7 @@ class LibraryController {
|
|||
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
||||
li.media.metadata.authors.forEach((au) => {
|
||||
if (!authors[au.id]) {
|
||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||
const _author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (_author) {
|
||||
authors[au.id] = _author.toJSON()
|
||||
authors[au.id].numBooks = 1
|
||||
|
@ -751,7 +844,7 @@ class LibraryController {
|
|||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
|
@ -776,7 +869,7 @@ class LibraryController {
|
|||
}
|
||||
|
||||
if (itemsUpdated.length) {
|
||||
await this.db.updateLibraryItems(itemsUpdated)
|
||||
await Database.updateBulkBooks(itemsUpdated.map(i => i.media))
|
||||
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||
}
|
||||
|
||||
|
@ -848,21 +941,53 @@ class LibraryController {
|
|||
res.json(payload)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
getOPMLFile(req, res) {
|
||||
const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems)
|
||||
res.type('application/xml')
|
||||
res.send(opmlText)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Replace with middlewareNew
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
*/
|
||||
async middleware(req, res, next) {
|
||||
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
||||
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
||||
return res.sendStatus(404)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||
const library = await Database.models.library.getOldById(req.params.id)
|
||||
if (!library) {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
req.library = library
|
||||
req.libraryItems = this.db.libraryItems.filter(li => {
|
||||
req.libraryItems = Database.libraryItems.filter(li => {
|
||||
return li.libraryId === library.id && req.user.checkCanAccessLibraryItem(li)
|
||||
})
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware that is not using libraryItems from memory
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
*/
|
||||
async middlewareNew(req, res, next) {
|
||||
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
||||
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const library = await Database.models.library.getOldById(req.params.id)
|
||||
if (!library) {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
req.library = library
|
||||
next()
|
||||
}
|
||||
}
|
||||
module.exports = new LibraryController()
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
const Path = require('path')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const zipHelpers = require('../utils/zipHelpers')
|
||||
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { ScanResult } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||
|
||||
class LibraryItemController {
|
||||
constructor() { }
|
||||
|
||||
// Example expand with authors: api/items/:id?expanded=1&include=authors
|
||||
findOne(req, res) {
|
||||
async findOne(req, res) {
|
||||
const includeEntities = (req.query.include || '').split(',')
|
||||
if (req.query.expanded == 1) {
|
||||
var item = req.libraryItem.toJSONExpanded()
|
||||
|
@ -22,14 +25,14 @@ class LibraryItemController {
|
|||
}
|
||||
|
||||
if (includeEntities.includes('rssfeed')) {
|
||||
const feedData = this.rssFeedManager.findFeedForEntityId(item.id)
|
||||
item.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||
const feedData = await this.rssFeedManager.findFeedForEntityId(item.id)
|
||||
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 = this.db.authors.find(_au => _au.id === au.id)
|
||||
var author = Database.authors.find(_au => _au.id === au.id)
|
||||
if (!author) return null
|
||||
return {
|
||||
...author
|
||||
|
@ -59,7 +62,7 @@ class LibraryItemController {
|
|||
const hasUpdates = libraryItem.update(req.body)
|
||||
if (hasUpdates) {
|
||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json(libraryItem.toJSON())
|
||||
|
@ -85,7 +88,9 @@ class LibraryItemController {
|
|||
}
|
||||
|
||||
const libraryItemPath = req.libraryItem.path
|
||||
const filename = `${req.libraryItem.media.metadata.title}.zip`
|
||||
const itemTitle = req.libraryItem.media.metadata.title
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
const filename = `${itemTitle}.zip`
|
||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
}
|
||||
|
||||
|
@ -95,6 +100,7 @@ class LibraryItemController {
|
|||
async updateMedia(req, res) {
|
||||
const libraryItem = req.libraryItem
|
||||
const mediaPayload = req.body
|
||||
|
||||
// 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)
|
||||
|
@ -102,7 +108,7 @@ class LibraryItemController {
|
|||
|
||||
// Book specific
|
||||
if (libraryItem.isBook) {
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
||||
}
|
||||
|
||||
// Podcast specific
|
||||
|
@ -137,7 +143,7 @@ class LibraryItemController {
|
|||
}
|
||||
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json({
|
||||
|
@ -172,7 +178,7 @@ class LibraryItemController {
|
|||
return res.status(500).send('Unknown error occurred')
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json({
|
||||
success: true,
|
||||
|
@ -192,7 +198,7 @@ class LibraryItemController {
|
|||
return res.status(500).send(validationResult.error)
|
||||
}
|
||||
if (validationResult.updated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
res.json({
|
||||
|
@ -208,7 +214,7 @@ class LibraryItemController {
|
|||
if (libraryItem.media.coverPath) {
|
||||
libraryItem.updateMediaCover('')
|
||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
|
@ -280,7 +286,7 @@ class LibraryItemController {
|
|||
return res.sendStatus(500)
|
||||
}
|
||||
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
@ -307,7 +313,7 @@ class LibraryItemController {
|
|||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const itemsToDelete = this.db.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||
const itemsToDelete = Database.libraryItems.filter(li => libraryItemIds.includes(li.id))
|
||||
if (!itemsToDelete.length) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
@ -336,15 +342,15 @@ class LibraryItemController {
|
|||
|
||||
for (let i = 0; i < updatePayloads.length; i++) {
|
||||
var mediaPayload = updatePayloads[i].mediaPayload
|
||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||
var libraryItem = Database.libraryItems.find(_li => _li.id === updatePayloads[i].id)
|
||||
if (!libraryItem) return null
|
||||
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload)
|
||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
||||
|
||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
||||
if (hasUpdates) {
|
||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
itemsUpdated++
|
||||
}
|
||||
|
@ -364,7 +370,7 @@ class LibraryItemController {
|
|||
}
|
||||
const libraryItems = []
|
||||
libraryItemIds.forEach((lid) => {
|
||||
const li = this.db.libraryItems.find(_li => _li.id === lid)
|
||||
const li = Database.libraryItems.find(_li => _li.id === lid)
|
||||
if (li) libraryItems.push(li.toJSONExpanded())
|
||||
})
|
||||
res.json({
|
||||
|
@ -379,20 +385,23 @@ class LibraryItemController {
|
|||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var itemsUpdated = 0
|
||||
var itemsUnmatched = 0
|
||||
let itemsUpdated = 0
|
||||
let itemsUnmatched = 0
|
||||
|
||||
var matchData = req.body
|
||||
var options = matchData.options || {}
|
||||
var items = matchData.libraryItemIds
|
||||
if (!items || !items.length) {
|
||||
return res.sendStatus(500)
|
||||
const options = req.body.options || {}
|
||||
if (!req.body.libraryItemIds?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === items[i])
|
||||
var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
for (const libraryItem of libraryItems) {
|
||||
const matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||
if (matchResult.updated) {
|
||||
itemsUpdated++
|
||||
} else if (matchResult.warning) {
|
||||
|
@ -400,7 +409,7 @@ class LibraryItemController {
|
|||
}
|
||||
}
|
||||
|
||||
var result = {
|
||||
const result = {
|
||||
success: itemsUpdated > 0,
|
||||
updates: itemsUpdated,
|
||||
unmatched: itemsUnmatched
|
||||
|
@ -408,16 +417,31 @@ class LibraryItemController {
|
|||
SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
|
||||
}
|
||||
|
||||
// DELETE: api/items/all
|
||||
async deleteAll(req, res) {
|
||||
// POST: api/items/batch/scan
|
||||
async batchScan(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.warn('User other than admin attempted to delete all library items', req.user)
|
||||
Logger.warn('User other than admin attempted to batch scan library items', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
Logger.info('Removing all Library Items')
|
||||
var success = await this.db.recreateLibraryItemsDb()
|
||||
if (success) res.sendStatus(200)
|
||||
else res.sendStatus(500)
|
||||
|
||||
if (!req.body.libraryItemIds?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const libraryItems = req.body.libraryItemIds.map(lid => Database.getLibraryItem(lid)).filter(li => li)
|
||||
if (!libraryItems?.length) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// POST: api/items/:id/scan (admin)
|
||||
|
@ -432,7 +456,7 @@ class LibraryItemController {
|
|||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var result = await this.scanner.scanLibraryItemById(req.libraryItem.id)
|
||||
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
||||
res.json({
|
||||
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
||||
})
|
||||
|
@ -440,7 +464,7 @@ class LibraryItemController {
|
|||
|
||||
getToneMetadataObject(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-root user attempted to get tone metadata object`, req.user)
|
||||
Logger.error(`[LibraryItemController] Non-admin user attempted to get tone metadata object`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
|
@ -472,7 +496,7 @@ class LibraryItemController {
|
|||
const chapters = req.body.chapters || []
|
||||
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
|
@ -482,55 +506,197 @@ class LibraryItemController {
|
|||
})
|
||||
}
|
||||
|
||||
async toneScan(req, res) {
|
||||
if (!req.libraryItem.media.audioFiles.length) {
|
||||
return res.sendStatus(404)
|
||||
/**
|
||||
* GET api/items/:id/ffprobe/:fileid
|
||||
* FFProbe JSON result from audio file
|
||||
*
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async getFFprobeData(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryItemController] Non-admin user attempted to get ffprobe data`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
if (req.libraryFile.fileType !== 'audio') {
|
||||
Logger.error(`[LibraryItemController] Invalid filetype "${req.libraryFile.fileType}" for fileid "${req.params.fileid}". Expected audio file`)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
const audioFileIndex = isNullOrNaN(req.params.index) ? 1 : Number(req.params.index)
|
||||
const audioFile = req.libraryItem.media.audioFiles.find(af => af.index === audioFileIndex)
|
||||
const audioFile = req.libraryItem.media.findFileWithInode(req.params.fileid)
|
||||
if (!audioFile) {
|
||||
Logger.error(`[LibraryItemController] toneScan: Audio file not found with index ${audioFileIndex}`)
|
||||
Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const toneData = await this.scanner.probeAudioFileWithTone(audioFile)
|
||||
res.json(toneData)
|
||||
const ffprobeData = await this.scanner.probeAudioFile(audioFile)
|
||||
res.json(ffprobeData)
|
||||
}
|
||||
|
||||
async deleteLibraryFile(req, res) {
|
||||
const libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.ino)
|
||||
if (!libraryFile) {
|
||||
Logger.error(`[LibraryItemController] Unable to delete library file. Not found. "${req.params.ino}"`)
|
||||
return res.sendStatus(404)
|
||||
/**
|
||||
* GET api/items/:id/file/:fileid
|
||||
*
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async getLibraryFile(req, res) {
|
||||
const libraryFile = req.libraryFile
|
||||
|
||||
if (global.XAccel) {
|
||||
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
|
||||
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
|
||||
}
|
||||
|
||||
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
|
||||
if (audioMimeType) {
|
||||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
res.sendFile(libraryFile.metadata.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE api/items/:id/file/:fileid
|
||||
*
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async deleteLibraryFile(req, res) {
|
||||
const libraryFile = req.libraryFile
|
||||
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${libraryFile.metadata.path}"`)
|
||||
|
||||
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
||||
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
||||
})
|
||||
req.libraryItem.removeLibraryFile(req.params.ino)
|
||||
req.libraryItem.removeLibraryFile(req.params.fileid)
|
||||
|
||||
if (req.libraryItem.media.removeFileWithInode(req.params.ino)) {
|
||||
if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) {
|
||||
// If book has no more media files then mark it as missing
|
||||
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
|
||||
req.libraryItem.setMissing()
|
||||
}
|
||||
}
|
||||
req.libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
/**
|
||||
* GET api/items/:id/file/:fileid/download
|
||||
* Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async downloadLibraryFile(req, res) {
|
||||
const libraryFile = req.libraryFile
|
||||
|
||||
if (!req.user.canDownload) {
|
||||
Logger.error(`[LibraryItemController] User without download permission attempted to download file "${libraryFile.metadata.path}"`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`)
|
||||
|
||||
if (global.XAccel) {
|
||||
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
|
||||
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
|
||||
}
|
||||
|
||||
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
|
||||
if (audioMimeType) {
|
||||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
|
||||
res.download(libraryFile.metadata.path, libraryFile.metadata.filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET api/items/:id/ebook/:fileid?
|
||||
* fileid is the inode value stored in LibraryFile.ino or EBookFile.ino
|
||||
* fileid is only required when reading a supplementary ebook
|
||||
* when no fileid is passed in the primary ebook will be returned
|
||||
*
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async getEBookFile(req, res) {
|
||||
let ebookFile = null
|
||||
if (req.params.fileid) {
|
||||
ebookFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid)
|
||||
if (!ebookFile?.isEBookFile) {
|
||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||
return res.status(400).send('Invalid ebook file id')
|
||||
}
|
||||
} else {
|
||||
ebookFile = req.libraryItem.media.ebookFile
|
||||
}
|
||||
|
||||
if (!ebookFile) {
|
||||
Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
const ebookFilePath = ebookFile.metadata.path
|
||||
|
||||
if (global.XAccel) {
|
||||
Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`)
|
||||
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send()
|
||||
}
|
||||
|
||||
res.sendFile(ebookFilePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH api/items/:id/ebook/:fileid/status
|
||||
* toggle the status of an ebook file.
|
||||
* if an ebook file is the primary ebook, then it will be changed to supplementary
|
||||
* if an ebook file is supplementary, then it will be changed to primary
|
||||
*
|
||||
* @param {express.Request} req
|
||||
* @param {express.Response} res
|
||||
*/
|
||||
async updateEbookFileStatus(req, res) {
|
||||
const ebookLibraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid)
|
||||
if (!ebookLibraryFile?.isEBookFile) {
|
||||
Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`)
|
||||
return res.status(400).send('Invalid ebook file id')
|
||||
}
|
||||
|
||||
if (ebookLibraryFile.isSupplementary) {
|
||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`)
|
||||
req.libraryItem.setPrimaryEbook(ebookLibraryFile)
|
||||
} else {
|
||||
Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`)
|
||||
ebookLibraryFile.isSupplementary = true
|
||||
req.libraryItem.setPrimaryEbook(null)
|
||||
}
|
||||
|
||||
req.libraryItem.updatedAt = Date.now()
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async middleware(req, res, next) {
|
||||
req.libraryItem = await Database.models.libraryItem.getOldById(req.params.id)
|
||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||
if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
// For library file routes, get the library file
|
||||
if (req.params.fileid) {
|
||||
req.libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid)
|
||||
if (!req.libraryFile) {
|
||||
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
|
||||
if (req.path.includes('/play')) {
|
||||
// allow POST requests using /play and /play/:episodeId
|
||||
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
|
@ -541,7 +707,6 @@ class LibraryItemController {
|
|||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
req.libraryItem = item
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
const { sort } = require('../libs/fastSort')
|
||||
const { isObject, toNumber } = require('../utils/index')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class MeController {
|
||||
constructor() { }
|
||||
|
@ -33,7 +34,7 @@ class MeController {
|
|||
|
||||
// GET: api/me/listening-stats
|
||||
async getListeningStats(req, res) {
|
||||
var listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||
res.json(listeningStats)
|
||||
}
|
||||
|
||||
|
@ -51,21 +52,21 @@ class MeController {
|
|||
if (!req.user.removeMediaProgress(req.params.id)) {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.removeMediaProgress(req.params.id)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// PATCH: api/me/progress/:id
|
||||
async createUpdateMediaProgress(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, req.body)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.sendStatus(200)
|
||||
|
@ -73,8 +74,8 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/progress/:id/:episodeId
|
||||
async createUpdateEpisodeMediaProgress(req, res) {
|
||||
var episodeId = req.params.episodeId
|
||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = Database.libraryItems.find(ab => ab.id === req.params.id)
|
||||
if (!libraryItem) {
|
||||
return res.status(404).send('Item not found')
|
||||
}
|
||||
|
@ -83,9 +84,9 @@ class MeController {
|
|||
return res.status(404).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.sendStatus(200)
|
||||
|
@ -93,24 +94,26 @@ class MeController {
|
|||
|
||||
// PATCH: api/me/progress/batch/update
|
||||
async batchUpdateMediaProgress(req, res) {
|
||||
var itemProgressPayloads = req.body
|
||||
if (!itemProgressPayloads || !itemProgressPayloads.length) {
|
||||
const itemProgressPayloads = req.body
|
||||
if (!itemProgressPayloads?.length) {
|
||||
return res.status(400).send('Missing request payload')
|
||||
}
|
||||
|
||||
var shouldUpdate = false
|
||||
itemProgressPayloads.forEach((itemProgress) => {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
let shouldUpdate = false
|
||||
for (const itemProgress of itemProgressPayloads) {
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||
if (libraryItem) {
|
||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)
|
||||
if (wasUpdated) shouldUpdate = true
|
||||
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
|
||||
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
shouldUpdate = true
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
|
@ -119,18 +122,18 @@ class MeController {
|
|||
|
||||
// POST: api/me/item/:id/bookmark
|
||||
async createBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
const { time, title } = req.body
|
||||
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
}
|
||||
|
||||
// PATCH: api/me/item/:id/bookmark
|
||||
async updateBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
const { time, title } = req.body
|
||||
if (!req.user.findBookmark(libraryItem.id, time)) {
|
||||
|
@ -139,14 +142,14 @@ class MeController {
|
|||
}
|
||||
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
|
||||
if (!bookmark) return res.sendStatus(500)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.json(bookmark)
|
||||
}
|
||||
|
||||
// DELETE: api/me/item/:id/bookmark/:time
|
||||
async removeBookmark(req, res) {
|
||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
var libraryItem = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!libraryItem) return res.sendStatus(404)
|
||||
var time = Number(req.params.time)
|
||||
if (isNaN(time)) return res.sendStatus(500)
|
||||
|
@ -156,7 +159,7 @@ class MeController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
req.user.removeBookmark(libraryItem.id, time)
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
@ -178,19 +181,19 @@ class MeController {
|
|||
return res.sendStatus(500)
|
||||
}
|
||||
const updatedLocalMediaProgress = []
|
||||
var numServerProgressUpdates = 0
|
||||
let numServerProgressUpdates = 0
|
||||
const updatedServerMediaProgress = []
|
||||
const localMediaProgress = req.body.localMediaProgress || []
|
||||
|
||||
localMediaProgress.forEach(localProgress => {
|
||||
for (const localProgress of localMediaProgress) {
|
||||
if (!localProgress.libraryItemId) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
||||
return
|
||||
continue
|
||||
}
|
||||
var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(localProgress.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
let mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
|
@ -199,12 +202,14 @@ class MeController {
|
|||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
||||
|
@ -222,11 +227,10 @@ class MeController {
|
|||
} else {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
|
||||
if (numServerProgressUpdates > 0) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
|
@ -244,7 +248,7 @@ class MeController {
|
|||
let itemsInProgress = []
|
||||
for (const mediaProgress of req.user.mediaProgress) {
|
||||
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||
const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(mediaProgress.libraryItemId)
|
||||
if (libraryItem) {
|
||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||
|
@ -274,7 +278,7 @@ class MeController {
|
|||
|
||||
// GET: api/me/series/:id/remove-from-continue-listening
|
||||
async removeSeriesFromContinueListening(req, res) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -282,7 +286,7 @@ class MeController {
|
|||
|
||||
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
|
@ -290,7 +294,7 @@ class MeController {
|
|||
|
||||
// GET: api/me/series/:id/readd-to-continue-listening
|
||||
async readdSeriesFromContinueListening(req, res) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === req.params.id)
|
||||
if (!series) {
|
||||
Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -298,7 +302,7 @@ class MeController {
|
|||
|
||||
const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
|
@ -308,7 +312,7 @@ class MeController {
|
|||
async removeItemFromContinueListening(req, res) {
|
||||
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('user', req.user)
|
||||
await Database.updateUser(req.user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
|
|
|
@ -2,6 +2,7 @@ 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 patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||
|
@ -23,18 +24,18 @@ class MiscController {
|
|||
Logger.error('Invalid request, no files')
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
var files = Object.values(req.files)
|
||||
var title = req.body.title
|
||||
var author = req.body.author
|
||||
var series = req.body.series
|
||||
var libraryId = req.body.library
|
||||
var folderId = req.body.folder
|
||||
const files = Object.values(req.files)
|
||||
const title = req.body.title
|
||||
const author = req.body.author
|
||||
const series = req.body.series
|
||||
const libraryId = req.body.library
|
||||
const folderId = req.body.folder
|
||||
|
||||
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||
const library = await Database.models.library.getOldById(libraryId)
|
||||
if (!library) {
|
||||
return res.status(404).send(`Library not found with id ${libraryId}`)
|
||||
}
|
||||
var folder = library.folders.find(fold => fold.id === folderId)
|
||||
const folder = library.folders.find(fold => fold.id === folderId)
|
||||
if (!folder) {
|
||||
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
|
||||
}
|
||||
|
@ -44,8 +45,8 @@ class MiscController {
|
|||
}
|
||||
|
||||
// For setting permissions recursively
|
||||
var outputDirectory = ''
|
||||
var firstDirPath = ''
|
||||
let outputDirectory = ''
|
||||
let firstDirPath = ''
|
||||
|
||||
if (library.isPodcast) { // Podcasts only in 1 folder
|
||||
outputDirectory = Path.join(folder.fullPath, title)
|
||||
|
@ -61,8 +62,7 @@ class MiscController {
|
|||
}
|
||||
}
|
||||
|
||||
var exists = await fs.pathExists(outputDirectory)
|
||||
if (exists) {
|
||||
if (await fs.pathExists(outputDirectory)) {
|
||||
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
||||
return res.status(500).send(`Directory "${outputDirectory}" already exists`)
|
||||
}
|
||||
|
@ -111,32 +111,39 @@ class MiscController {
|
|||
Logger.error('User other than admin attempting to update server settings', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var settingsUpdate = req.body
|
||||
const settingsUpdate = req.body
|
||||
if (!settingsUpdate || !isObject(settingsUpdate)) {
|
||||
return res.status(500).send('Invalid settings update object')
|
||||
}
|
||||
|
||||
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
||||
const madeUpdates = Database.serverSettings.update(settingsUpdate)
|
||||
if (madeUpdates) {
|
||||
await Database.updateServerSettings()
|
||||
|
||||
// If backup schedule is updated - update backup manager
|
||||
if (settingsUpdate.backupSchedule !== undefined) {
|
||||
this.backupManager.updateCronSchedule()
|
||||
}
|
||||
|
||||
await this.db.updateServerSettings()
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
serverSettings: this.db.serverSettings.toJSONForBrowser()
|
||||
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||
})
|
||||
}
|
||||
|
||||
authorize(req, res) {
|
||||
/**
|
||||
* POST: /api/authorize
|
||||
* Used to authorize an API token
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async authorize(req, res) {
|
||||
if (!req.user) {
|
||||
Logger.error('Invalid user in authorize')
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
const userResponse = this.auth.getUserLoginResponsePayload(req.user)
|
||||
const userResponse = await this.auth.getUserLoginResponsePayload(req.user)
|
||||
res.json(userResponse)
|
||||
}
|
||||
|
||||
|
@ -147,7 +154,7 @@ class MiscController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
const tags = []
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
Database.libraryItems.forEach((li) => {
|
||||
if (li.media.tags && li.media.tags.length) {
|
||||
li.media.tags.forEach((tag) => {
|
||||
if (!tags.includes(tag)) tags.push(tag)
|
||||
|
@ -176,7 +183,7 @@ class MiscController {
|
|||
let tagMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
|
||||
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
||||
|
@ -187,7 +194,7 @@ class MiscController {
|
|||
li.media.tags.push(newTag) // Add new tag
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
@ -209,13 +216,13 @@ class MiscController {
|
|||
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.tags || !li.media.tags.length) continue
|
||||
|
||||
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 this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
@ -233,7 +240,7 @@ class MiscController {
|
|||
return res.sendStatus(404)
|
||||
}
|
||||
const genres = []
|
||||
this.db.libraryItems.forEach((li) => {
|
||||
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)
|
||||
|
@ -262,7 +269,7 @@ class MiscController {
|
|||
let genreMerged = false
|
||||
let numItemsUpdated = 0
|
||||
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
|
||||
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
||||
|
@ -273,7 +280,7 @@ class MiscController {
|
|||
li.media.metadata.genres.push(newGenre) // Add new genre
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||
await this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
@ -295,13 +302,13 @@ class MiscController {
|
|||
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||
|
||||
let numItemsUpdated = 0
|
||||
for (const li of this.db.libraryItems) {
|
||||
for (const li of Database.libraryItems) {
|
||||
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||
|
||||
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 this.db.updateLibraryItem(li)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { version } = require('../../package.json')
|
||||
|
||||
class NotificationController {
|
||||
|
@ -7,14 +8,14 @@ class NotificationController {
|
|||
get(req, res) {
|
||||
res.json({
|
||||
data: this.notificationManager.getData(),
|
||||
settings: this.db.notificationSettings
|
||||
settings: Database.notificationSettings
|
||||
})
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
const updated = this.db.notificationSettings.update(req.body)
|
||||
const updated = Database.notificationSettings.update(req.body)
|
||||
if (updated) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
@ -29,31 +30,31 @@ class NotificationController {
|
|||
}
|
||||
|
||||
async createNotification(req, res) {
|
||||
const success = this.db.notificationSettings.createNotification(req.body)
|
||||
const success = Database.notificationSettings.createNotification(req.body)
|
||||
|
||||
if (success) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async deleteNotification(req, res) {
|
||||
if (this.db.notificationSettings.removeNotification(req.notification.id)) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
if (Database.notificationSettings.removeNotification(req.notification.id)) {
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async updateNotification(req, res) {
|
||||
const success = this.db.notificationSettings.updateNotification(req.body)
|
||||
const success = Database.notificationSettings.updateNotification(req.body)
|
||||
if (success) {
|
||||
await this.db.updateEntity('settings', this.db.notificationSettings)
|
||||
await Database.updateSetting(Database.notificationSettings)
|
||||
}
|
||||
res.json(this.db.notificationSettings)
|
||||
res.json(Database.notificationSettings)
|
||||
}
|
||||
|
||||
async sendNotificationTest(req, res) {
|
||||
if (!this.db.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
||||
if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
|
||||
|
||||
const success = await this.notificationManager.sendTestNotification(req.notification)
|
||||
if (success) res.sendStatus(200)
|
||||
|
@ -66,7 +67,7 @@ class NotificationController {
|
|||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const notification = this.db.notificationSettings.getNotification(req.params.id)
|
||||
const notification = Database.notificationSettings.getNotification(req.params.id)
|
||||
if (!notification) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const Playlist = require('../objects/Playlist')
|
||||
|
||||
|
@ -14,31 +15,32 @@ class PlaylistController {
|
|||
if (!success) {
|
||||
return res.status(400).send('Invalid playlist request data')
|
||||
}
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('playlist', newPlaylist)
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// GET: api/playlists
|
||||
findAllForUser(req, res) {
|
||||
async findAllForUser(req, res) {
|
||||
const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id)
|
||||
res.json({
|
||||
playlists: this.db.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(this.db.libraryItems))
|
||||
playlists: playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
|
||||
})
|
||||
}
|
||||
|
||||
// GET: api/playlists/:id
|
||||
findOne(req, res) {
|
||||
res.json(req.playlist.toJSONExpanded(this.db.libraryItems))
|
||||
res.json(req.playlist.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
|
||||
// PATCH: api/playlists/:id
|
||||
async update(req, res) {
|
||||
const playlist = req.playlist
|
||||
let wasUpdated = playlist.update(req.body)
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
|
@ -47,8 +49,8 @@ class PlaylistController {
|
|||
// DELETE: api/playlists/:id
|
||||
async delete(req, res) {
|
||||
const playlist = req.playlist
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
@ -62,7 +64,7 @@ class PlaylistController {
|
|||
return res.status(400).send('Request body has no libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||
const libraryItem = Database.libraryItems.find(li => li.id === itemToAdd.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
return res.status(400).send('Library item not found')
|
||||
}
|
||||
|
@ -80,8 +82,16 @@ class PlaylistController {
|
|||
}
|
||||
|
||||
playlist.addItem(itemToAdd.libraryItemId, itemToAdd.episodeId)
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
|
||||
const playlistMediaItem = {
|
||||
playlistId: playlist.id,
|
||||
mediaItemId: itemToAdd.episodeId || libraryItem.media.id,
|
||||
mediaItemType: itemToAdd.episodeId ? 'podcastEpisode' : 'book',
|
||||
order: playlist.items.length
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylistMediaItem(playlistMediaItem)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
@ -99,15 +109,15 @@ class PlaylistController {
|
|||
|
||||
playlist.removeItem(itemToRemove.libraryItemId, itemToRemove.episodeId)
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// 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 this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
|
||||
|
@ -122,20 +132,34 @@ class PlaylistController {
|
|||
}
|
||||
const itemsToAdd = req.body.items
|
||||
let hasUpdated = false
|
||||
|
||||
let order = playlist.items.length
|
||||
const playlistMediaItems = []
|
||||
for (const item of itemsToAdd) {
|
||||
if (!item.libraryItemId) {
|
||||
return res.status(400).send('Item does not have libraryItemId')
|
||||
}
|
||||
|
||||
const libraryItem = Database.getLibraryItem(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
|
||||
}
|
||||
}
|
||||
|
||||
const jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
|
@ -153,21 +177,22 @@ class PlaylistController {
|
|||
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 jsonExpanded = playlist.toJSONExpanded(this.db.libraryItems)
|
||||
const jsonExpanded = playlist.toJSONExpanded(Database.libraryItems)
|
||||
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 this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
await this.db.updateEntity('playlist', playlist)
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
}
|
||||
|
@ -176,12 +201,12 @@ class PlaylistController {
|
|||
|
||||
// POST: api/playlists/collection/:collectionId
|
||||
async createFromCollection(req, res) {
|
||||
let collection = this.db.collections.find(c => c.id === req.params.collectionId)
|
||||
let collection = await Database.models.collection.getById(req.params.collectionId)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
// Expand collection to get library items
|
||||
collection = collection.toJSONExpanded(this.db.libraryItems)
|
||||
collection = collection.toJSONExpanded(Database.libraryItems)
|
||||
|
||||
// Filter out library items not accessible to user
|
||||
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||
|
@ -201,15 +226,15 @@ class PlaylistController {
|
|||
}
|
||||
newPlaylist.setData(newPlaylistData)
|
||||
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('playlist', newPlaylist)
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(Database.libraryItems)
|
||||
await Database.createPlaylist(newPlaylist)
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
async middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
const playlist = this.db.playlists.find(p => p.id === req.params.id)
|
||||
const playlist = await Database.models.playlist.getById(req.params.id)
|
||||
if (!playlist) {
|
||||
return res.status(404).send('Playlist not found')
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const fs = require('../libs/fsExtra')
|
||||
|
||||
|
@ -18,7 +19,7 @@ class PodcastController {
|
|||
}
|
||||
const payload = req.body
|
||||
|
||||
const library = this.db.libraries.find(lib => lib.id === payload.libraryId)
|
||||
const library = await Database.models.library.getOldById(payload.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||
return res.status(404).send('Library not found')
|
||||
|
@ -33,7 +34,7 @@ class PodcastController {
|
|||
const podcastPath = filePathToPOSIX(payload.path)
|
||||
|
||||
// Check if a library item with this podcast folder exists already
|
||||
const existingLibraryItem = this.db.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||
const existingLibraryItem = Database.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||
if (existingLibraryItem) {
|
||||
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
||||
return res.status(400).send('Podcast already exists')
|
||||
|
@ -80,7 +81,7 @@ class PodcastController {
|
|||
}
|
||||
}
|
||||
|
||||
await this.db.insertLibraryItem(libraryItem)
|
||||
await Database.createLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
|
||||
|
||||
res.json(libraryItem.toJSONExpanded())
|
||||
|
@ -109,7 +110,7 @@ class PodcastController {
|
|||
res.json({ podcast })
|
||||
}
|
||||
|
||||
async getOPMLFeeds(req, res) {
|
||||
async getFeedsFromOPMLText(req, res) {
|
||||
if (!req.body.opmlText) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
@ -199,7 +200,7 @@ class PodcastController {
|
|||
const overrideDetails = req.query.override === '1'
|
||||
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||
if (episodesUpdated) {
|
||||
await this.db.updateLibraryItem(req.libraryItem)
|
||||
await Database.updateLibraryItem(req.libraryItem)
|
||||
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
|
@ -216,9 +217,8 @@ class PodcastController {
|
|||
return res.status(404).send('Episode not found')
|
||||
}
|
||||
|
||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
||||
if (wasUpdated) {
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
if (libraryItem.media.updateEpisode(episodeId, req.body)) {
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
}
|
||||
|
||||
|
@ -241,18 +241,18 @@ class PodcastController {
|
|||
|
||||
// DELETE: api/podcasts/:id/episode/:episodeId
|
||||
async removeEpisode(req, res) {
|
||||
var episodeId = req.params.episodeId
|
||||
var libraryItem = req.libraryItem
|
||||
var hardDelete = req.query.hard === '1'
|
||||
const episodeId = req.params.episodeId
|
||||
const libraryItem = req.libraryItem
|
||||
const hardDelete = req.query.hard === '1'
|
||||
|
||||
var episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
|
||||
const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
|
||||
if (!episode) {
|
||||
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
if (hardDelete) {
|
||||
var audioFile = episode.audioFile
|
||||
const audioFile = episode.audioFile
|
||||
// TODO: this will trigger the watcher. should maybe handle this gracefully
|
||||
await fs.remove(audioFile.metadata.path).then(() => {
|
||||
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
|
||||
|
@ -263,17 +263,43 @@ class PodcastController {
|
|||
|
||||
// Remove episode from Podcast and library file
|
||||
const episodeRemoved = libraryItem.media.removeEpisode(episodeId)
|
||||
if (episodeRemoved && episodeRemoved.audioFile) {
|
||||
if (episodeRemoved?.audioFile) {
|
||||
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
// 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)
|
||||
|
||||
// If playlist is now empty then remove it
|
||||
if (!playlist.items.length) {
|
||||
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))
|
||||
} else {
|
||||
await Database.updatePlaylist(playlist)
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove media progress for this episode
|
||||
const mediaProgressRemoved = await Database.models.mediaProgress.destroy({
|
||||
where: {
|
||||
mediaItemId: episode.id
|
||||
}
|
||||
})
|
||||
if (mediaProgressRemoved) {
|
||||
Logger.info(`[PodcastController] Removed ${mediaProgressRemoved} media progress for episode ${episode.id}`)
|
||||
}
|
||||
|
||||
await Database.updateLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
res.json(libraryItem.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
if (!item.isPodcast) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class RSSFeedController {
|
||||
constructor() { }
|
||||
|
@ -8,7 +8,7 @@ class RSSFeedController {
|
|||
async openRSSFeedForItem(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.itemId)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.itemId)
|
||||
if (!item) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
@ -30,7 +30,7 @@ class RSSFeedController {
|
|||
}
|
||||
|
||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||
if (this.rssFeedManager.feeds[options.slug]) {
|
||||
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class RSSFeedController {
|
|||
async openRSSFeedForCollection(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const collection = this.db.collections.find(li => li.id === req.params.collectionId)
|
||||
const collection = await Database.models.collection.getById(req.params.collectionId)
|
||||
if (!collection) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
|
@ -55,12 +55,12 @@ class RSSFeedController {
|
|||
}
|
||||
|
||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||
if (this.rssFeedManager.feeds[options.slug]) {
|
||||
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
||||
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||
|
||||
// Check collection has audio tracks
|
||||
|
@ -79,7 +79,7 @@ class RSSFeedController {
|
|||
async openRSSFeedForSeries(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const series = this.db.series.find(se => se.id === req.params.seriesId)
|
||||
const series = Database.series.find(se => se.id === req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
|
@ -89,14 +89,14 @@ class RSSFeedController {
|
|||
}
|
||||
|
||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||
if (this.rssFeedManager.feeds[options.slug]) {
|
||||
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const seriesJson = series.toJSON()
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
seriesJson.books = Database.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||
|
||||
// Check series has audio tracks
|
||||
if (!seriesJson.books.length) {
|
||||
|
@ -111,10 +111,8 @@ class RSSFeedController {
|
|||
}
|
||||
|
||||
// POST: api/feeds/:id/close
|
||||
async closeRSSFeed(req, res) {
|
||||
await this.rssFeedManager.closeRssFeed(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
closeRSSFeed(req, res) {
|
||||
this.rssFeedManager.closeRssFeed(req, res)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
|
@ -123,14 +121,6 @@ class RSSFeedController {
|
|||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const feed = this.rssFeedManager.findFeed(req.params.id)
|
||||
if (!feed) {
|
||||
Logger.error(`[RSSFeedController] RSS feed not found with id "${req.params.id}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
class SeriesController {
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* /api/series/:id
|
||||
*
|
||||
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
|
||||
* Series are not library specific so we need to know what the library id is
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v)
|
||||
|
||||
|
@ -11,7 +22,7 @@ class SeriesController {
|
|||
|
||||
// Add progress map with isFinished flag
|
||||
if (include.includes('progress')) {
|
||||
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
||||
const libraryItemsInSeries = req.libraryItemsInSeries
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||
return mediaProgress && mediaProgress.isFinished
|
||||
|
@ -24,18 +35,18 @@ class SeriesController {
|
|||
}
|
||||
|
||||
if (include.includes('rssfeed')) {
|
||||
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||
const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||
}
|
||||
|
||||
return res.json(seriesJson)
|
||||
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 = this.db.series.filter(se => se.name.toLowerCase().includes(q))
|
||||
var series = Database.series.filter(se => se.name.toLowerCase().includes(q))
|
||||
series = series.slice(0, limit)
|
||||
res.json({
|
||||
results: series
|
||||
|
@ -45,16 +56,26 @@ class SeriesController {
|
|||
async update(req, res) {
|
||||
const hasUpdated = req.series.update(req.body)
|
||||
if (hasUpdated) {
|
||||
await this.db.updateEntity('series', req.series)
|
||||
await Database.updateSeries(req.series)
|
||||
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||
}
|
||||
res.json(req.series.toJSON())
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
const series = this.db.series.find(se => se.id === req.params.id)
|
||||
const series = Database.series.find(se => se.id === 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)
|
||||
}
|
||||
|
||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||
Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user)
|
||||
return res.sendStatus(403)
|
||||
|
@ -64,6 +85,7 @@ class SeriesController {
|
|||
}
|
||||
|
||||
req.series = series
|
||||
req.libraryItemsInSeries = libraryItemsAccessible
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class SessionController {
|
||||
|
@ -42,17 +43,17 @@ class SessionController {
|
|||
res.json(payload)
|
||||
}
|
||||
|
||||
getOpenSessions(req, res) {
|
||||
async getOpenSessions(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
|
||||
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
||||
const user = this.db.users.find(u => u.id === se.userId) || null
|
||||
return {
|
||||
...se.toJSON(),
|
||||
user: user ? { id: user.id, username: user.username } : null
|
||||
user: minifiedUserObjects.find(u => u.id === se.userId) || null
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -62,7 +63,7 @@ class SessionController {
|
|||
}
|
||||
|
||||
getOpenSession(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||
var libraryItem = Database.getLibraryItem(req.session.libraryItemId)
|
||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
res.json(sessionForClient)
|
||||
}
|
||||
|
@ -74,7 +75,9 @@ class SessionController {
|
|||
|
||||
// POST: api/session/:id/close
|
||||
close(req, res) {
|
||||
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
||||
let syncData = req.body
|
||||
if (syncData && !Object.keys(syncData).length) syncData = null
|
||||
this.playbackSessionManager.closeSessionRequest(req.user, req.session, syncData, res)
|
||||
}
|
||||
|
||||
// DELETE: api/session/:id
|
||||
|
@ -85,13 +88,13 @@ class SessionController {
|
|||
await this.playbackSessionManager.removeSession(req.session.id)
|
||||
}
|
||||
|
||||
await this.db.removeEntity('session', req.session.id)
|
||||
await Database.removePlaybackSession(req.session.id)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// POST: api/session/local
|
||||
syncLocal(req, res) {
|
||||
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
|
||||
this.playbackSessionManager.syncLocalSessionRequest(req, res)
|
||||
}
|
||||
|
||||
// POST: api/session/local-all
|
||||
|
@ -113,7 +116,7 @@ class SessionController {
|
|||
}
|
||||
|
||||
async middleware(req, res, next) {
|
||||
const playbackSession = await this.db.getPlaybackSession(req.params.id)
|
||||
const playbackSession = await Database.getPlaybackSession(req.params.id)
|
||||
if (!playbackSession) {
|
||||
Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`)
|
||||
return res.sendStatus(404)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
|
||||
class ToolsController {
|
||||
constructor() { }
|
||||
|
@ -65,7 +66,7 @@ class ToolsController {
|
|||
|
||||
const libraryItems = []
|
||||
for (const libraryItemId of libraryItemIds) {
|
||||
const libraryItem = this.db.getLibraryItem(libraryItemId)
|
||||
const libraryItem = Database.getLibraryItem(libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
||||
return res.sendStatus(404)
|
||||
|
@ -105,7 +106,7 @@ class ToolsController {
|
|||
}
|
||||
|
||||
if (req.params.id) {
|
||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||
const item = Database.libraryItems.find(li => li.id === req.params.id)
|
||||
if (!item || !item.media) return res.sendStatus(404)
|
||||
|
||||
// Check user can access this library item
|
||||
|
|
|
@ -1,52 +1,63 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const Logger = require('../Logger')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const Database = require('../Database')
|
||||
|
||||
const User = require('../objects/user/User')
|
||||
|
||||
const { getId, toNumber } = require('../utils/index')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class UserController {
|
||||
constructor() { }
|
||||
|
||||
findAll(req, res) {
|
||||
async findAll(req, res) {
|
||||
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
||||
const hideRootToken = !req.user.isRoot
|
||||
|
||||
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 users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||
|
||||
if (includes.includes('latestSession')) {
|
||||
for (const user of users) {
|
||||
const userSessions = await Database.getPlaybackSessions({ userId: user.id })
|
||||
user.latestSession = userSessions.sort((a, b) => b.updatedAt - a.updatedAt).shift() || null
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||
users: this.db.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||
users
|
||||
})
|
||||
}
|
||||
|
||||
findOne(req, 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)
|
||||
}
|
||||
|
||||
const user = this.db.users.find(u => u.id === req.params.id)
|
||||
if (!user) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||
res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
|
||||
}
|
||||
|
||||
async create(req, res) {
|
||||
var account = req.body
|
||||
const account = req.body
|
||||
const username = account.username
|
||||
|
||||
var username = account.username
|
||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
||||
const usernameExists = await Database.models.user.getUserByUsername(username)
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
|
||||
account.id = getId('usr')
|
||||
account.id = uuidv4()
|
||||
account.pash = await this.auth.hashPass(account.password)
|
||||
delete account.password
|
||||
account.token = await this.auth.generateAccessToken(account)
|
||||
account.createdAt = Date.now()
|
||||
var newUser = new User(account)
|
||||
var success = await this.db.insertEntity('user', newUser)
|
||||
const newUser = new User(account)
|
||||
|
||||
const success = await Database.createUser(newUser)
|
||||
if (success) {
|
||||
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
||||
res.json({
|
||||
|
@ -58,7 +69,7 @@ class UserController {
|
|||
}
|
||||
|
||||
async update(req, res) {
|
||||
var user = req.reqUser
|
||||
const user = req.reqUser
|
||||
|
||||
if (user.type === 'root' && !req.user.isRoot) {
|
||||
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
||||
|
@ -69,7 +80,7 @@ class UserController {
|
|||
var shouldUpdateToken = false
|
||||
|
||||
if (account.username !== undefined && account.username !== user.username) {
|
||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||
const usernameExists = await Database.models.user.getUserByUsername(account.username)
|
||||
if (usernameExists) {
|
||||
return res.status(500).send('Username already taken')
|
||||
}
|
||||
|
@ -82,13 +93,12 @@ class UserController {
|
|||
delete account.password
|
||||
}
|
||||
|
||||
var hasUpdated = user.update(account)
|
||||
if (hasUpdated) {
|
||||
if (user.update(account)) {
|
||||
if (shouldUpdateToken) {
|
||||
user.token = await this.auth.generateAccessToken(user)
|
||||
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||
}
|
||||
await this.db.updateEntity('user', user)
|
||||
await Database.updateUser(user)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
|
@ -112,13 +122,13 @@ class UserController {
|
|||
// Todo: check if user is logged in and cancel streams
|
||||
|
||||
// Remove user playlists
|
||||
const userPlaylists = this.db.playlists.filter(p => p.userId === user.id)
|
||||
const userPlaylists = await Database.models.playlist.getPlaylistsForUserAndLibrary(user.id)
|
||||
for (const playlist of userPlaylists) {
|
||||
await this.db.removeEntity('playlist', playlist.id)
|
||||
await Database.removePlaylist(playlist.id)
|
||||
}
|
||||
|
||||
const userJson = user.toJSONForBrowser()
|
||||
await this.db.removeEntity('user', user.id)
|
||||
await Database.removeUser(user.id)
|
||||
SocketAuthority.adminEmitter('user_removed', userJson)
|
||||
res.json({
|
||||
success: true
|
||||
|
@ -152,40 +162,6 @@ class UserController {
|
|||
res.json(listeningStats)
|
||||
}
|
||||
|
||||
// POST: api/users/:id/purge-media-progress
|
||||
async purgeMediaProgress(req, res) {
|
||||
const user = req.reqUser
|
||||
|
||||
if (user.type === 'root' && !req.user.isRoot) {
|
||||
Logger.error(`[UserController] Admin user attempted to purge media progress of root user`, req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var progressPurged = 0
|
||||
user.mediaProgress = user.mediaProgress.filter(mp => {
|
||||
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
progressPurged++
|
||||
return false
|
||||
} else if (mp.episodeId) {
|
||||
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(mp.episodeId) : null
|
||||
if (!episode) { // Episode not found
|
||||
progressPurged++
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (progressPurged) {
|
||||
Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
|
||||
await this.db.updateEntity('user', user)
|
||||
SocketAuthority.adminEmitter('user_updated', user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||
}
|
||||
|
||||
// POST: api/users/online (admin)
|
||||
async getOnlineUsers(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
|
@ -198,7 +174,7 @@ class UserController {
|
|||
})
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
async middleware(req, res, next) {
|
||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||
return res.sendStatus(403)
|
||||
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
|
||||
|
@ -206,7 +182,7 @@ class UserController {
|
|||
}
|
||||
|
||||
if (req.params.id) {
|
||||
req.reqUser = this.db.users.find(u => u.id === req.params.id)
|
||||
req.reqUser = await Database.models.user.getUserById(req.params.id)
|
||||
if (!req.reqUser) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue