2022-03-18 17:51:55 +01:00
|
|
|
const Path = require('path')
|
2022-07-06 02:53:01 +02:00
|
|
|
const fs = require('../libs/fsExtra')
|
2022-03-18 17:51:55 +01:00
|
|
|
const Logger = require('../Logger')
|
2022-12-18 21:17:52 +01:00
|
|
|
const SocketAuthority = require('../SocketAuthority')
|
|
|
|
|
2022-04-15 01:24:24 +02:00
|
|
|
const filePerms = require('../utils/filePerms')
|
2022-08-02 01:06:22 +02:00
|
|
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
2022-03-18 17:51:55 +01:00
|
|
|
const { isObject } = require('../utils/index')
|
|
|
|
|
|
|
|
//
|
|
|
|
// This is a controller for routes that don't have a home yet :(
|
|
|
|
//
|
|
|
|
class MiscController {
|
|
|
|
constructor() { }
|
|
|
|
|
|
|
|
// POST: api/upload
|
|
|
|
async handleUpload(req, res) {
|
|
|
|
if (!req.user.canUpload) {
|
|
|
|
Logger.warn('User attempted to upload without permission', req.user)
|
|
|
|
return res.sendStatus(403)
|
|
|
|
}
|
|
|
|
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
|
|
|
|
|
|
|
|
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
|
|
|
if (!library) {
|
2022-11-21 14:52:33 +01:00
|
|
|
return res.status(404).send(`Library not found with id ${libraryId}`)
|
2022-03-18 17:51:55 +01:00
|
|
|
}
|
|
|
|
var folder = library.folders.find(fold => fold.id === folderId)
|
|
|
|
if (!folder) {
|
2022-11-21 14:52:33 +01:00
|
|
|
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
|
2022-03-18 17:51:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!files.length || !title) {
|
|
|
|
return res.status(500).send(`Invalid post data`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// For setting permissions recursively
|
|
|
|
var outputDirectory = ''
|
2022-04-15 01:24:24 +02:00
|
|
|
var firstDirPath = ''
|
|
|
|
|
|
|
|
if (library.isPodcast) { // Podcasts only in 1 folder
|
2022-03-18 17:51:55 +01:00
|
|
|
outputDirectory = Path.join(folder.fullPath, title)
|
2022-04-15 01:24:24 +02:00
|
|
|
firstDirPath = outputDirectory
|
|
|
|
} else {
|
|
|
|
firstDirPath = Path.join(folder.fullPath, author)
|
|
|
|
if (series && author) {
|
|
|
|
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
|
|
|
} else if (author) {
|
|
|
|
outputDirectory = Path.join(folder.fullPath, author, title)
|
|
|
|
} else {
|
|
|
|
outputDirectory = Path.join(folder.fullPath, title)
|
|
|
|
}
|
2022-03-18 17:51:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var exists = await fs.pathExists(outputDirectory)
|
|
|
|
if (exists) {
|
|
|
|
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
|
|
|
return res.status(500).send(`Directory "${outputDirectory}" already exists`)
|
|
|
|
}
|
|
|
|
|
|
|
|
await fs.ensureDir(outputDirectory)
|
|
|
|
|
|
|
|
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
|
|
|
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
var file = files[i]
|
|
|
|
|
|
|
|
var path = Path.join(outputDirectory, file.name)
|
|
|
|
await file.mv(path).then(() => {
|
|
|
|
return true
|
|
|
|
}).catch((error) => {
|
|
|
|
Logger.error('Failed to move file', path, error)
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
await filePerms.setDefault(firstDirPath)
|
|
|
|
|
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
|
2022-10-02 21:16:17 +02:00
|
|
|
// GET: api/tasks
|
|
|
|
getTasks(req, res) {
|
|
|
|
res.json({
|
|
|
|
tasks: this.taskManager.tasks.map(t => t.toJSON())
|
2022-03-18 17:51:55 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-04 02:16:16 +02:00
|
|
|
// PATCH: api/settings (admin)
|
2022-03-18 17:51:55 +01:00
|
|
|
async updateServerSettings(req, res) {
|
2022-05-04 02:16:16 +02:00
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error('User other than admin attempting to update server settings', req.user)
|
2022-03-18 17:51:55 +01:00
|
|
|
return res.sendStatus(403)
|
|
|
|
}
|
|
|
|
var settingsUpdate = req.body
|
|
|
|
if (!settingsUpdate || !isObject(settingsUpdate)) {
|
|
|
|
return res.status(500).send('Invalid settings update object')
|
|
|
|
}
|
|
|
|
|
|
|
|
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
|
|
|
if (madeUpdates) {
|
|
|
|
// If backup schedule is updated - update backup manager
|
|
|
|
if (settingsUpdate.backupSchedule !== undefined) {
|
|
|
|
this.backupManager.updateCronSchedule()
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.db.updateServerSettings()
|
|
|
|
}
|
|
|
|
return res.json({
|
|
|
|
success: true,
|
2022-11-21 14:52:33 +01:00
|
|
|
serverSettings: this.db.serverSettings.toJSONForBrowser()
|
2022-03-18 17:51:55 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
authorize(req, res) {
|
|
|
|
if (!req.user) {
|
|
|
|
Logger.error('Invalid user in authorize')
|
|
|
|
return res.sendStatus(401)
|
|
|
|
}
|
2022-08-06 02:23:18 +02:00
|
|
|
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
|
2022-04-30 00:43:46 +02:00
|
|
|
res.json(userResponse)
|
2022-03-18 17:51:55 +01:00
|
|
|
}
|
2022-03-20 12:29:08 +01:00
|
|
|
|
2022-12-18 21:17:52 +01:00
|
|
|
// GET: api/tags
|
2022-03-20 12:29:08 +01:00
|
|
|
getAllTags(req, res) {
|
2022-05-04 02:16:16 +02:00
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
|
2022-03-20 12:29:08 +01:00
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
2022-12-18 21:17:52 +01:00
|
|
|
const tags = []
|
2022-03-20 12:29:08 +01:00
|
|
|
this.db.libraryItems.forEach((li) => {
|
|
|
|
if (li.media.tags && li.media.tags.length) {
|
|
|
|
li.media.tags.forEach((tag) => {
|
|
|
|
if (!tags.includes(tag)) tags.push(tag)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2022-11-29 19:26:59 +01:00
|
|
|
res.json({
|
|
|
|
tags: tags
|
|
|
|
})
|
2022-03-20 12:29:08 +01:00
|
|
|
}
|
2022-08-02 01:06:22 +02:00
|
|
|
|
2022-12-18 21:17:52 +01:00
|
|
|
// POST: api/tags/rename
|
|
|
|
async renameTag(req, res) {
|
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to renameTag`)
|
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
|
|
|
|
|
|
|
const tag = req.body.tag
|
|
|
|
const newTag = req.body.newTag
|
|
|
|
if (!tag || !newTag) {
|
|
|
|
Logger.error(`[MiscController] Invalid request body for renameTag`)
|
|
|
|
return res.sendStatus(400)
|
|
|
|
}
|
|
|
|
|
|
|
|
let tagMerged = false
|
|
|
|
let numItemsUpdated = 0
|
|
|
|
|
|
|
|
for (const li of this.db.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
|
|
|
|
|
|
|
|
if (li.media.tags.includes(tag)) {
|
|
|
|
li.media.tags = li.media.tags.filter(t => t !== tag) // Remove old tag
|
|
|
|
if (!li.media.tags.includes(newTag)) {
|
|
|
|
li.media.tags.push(newTag) // Add new tag
|
|
|
|
}
|
|
|
|
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
|
|
|
await this.db.updateLibraryItem(li)
|
|
|
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
|
|
|
numItemsUpdated++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
tagMerged,
|
|
|
|
numItemsUpdated
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// DELETE: api/tags/:tag
|
|
|
|
async deleteTag(req, res) {
|
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to deleteTag`)
|
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
|
|
|
|
|
|
|
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
|
|
|
|
|
|
|
let numItemsUpdated = 0
|
|
|
|
for (const li of this.db.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)
|
|
|
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
|
|
|
numItemsUpdated++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
numItemsUpdated
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-12-18 21:52:53 +01:00
|
|
|
// GET: api/genres
|
|
|
|
getAllGenres(req, res) {
|
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`)
|
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
|
|
|
const genres = []
|
|
|
|
this.db.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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
res.json({
|
|
|
|
genres
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// POST: api/genres/rename
|
|
|
|
async renameGenre(req, res) {
|
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to renameGenre`)
|
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
|
|
|
|
|
|
|
const genre = req.body.genre
|
|
|
|
const newGenre = req.body.newGenre
|
|
|
|
if (!genre || !newGenre) {
|
|
|
|
Logger.error(`[MiscController] Invalid request body for renameGenre`)
|
|
|
|
return res.sendStatus(400)
|
|
|
|
}
|
|
|
|
|
|
|
|
let genreMerged = false
|
|
|
|
let numItemsUpdated = 0
|
|
|
|
|
|
|
|
for (const li of this.db.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
|
|
|
|
|
|
|
|
if (li.media.metadata.genres.includes(genre)) {
|
|
|
|
li.media.metadata.genres = li.media.metadata.genres.filter(g => g !== genre) // Remove old genre
|
|
|
|
if (!li.media.metadata.genres.includes(newGenre)) {
|
|
|
|
li.media.metadata.genres.push(newGenre) // Add new genre
|
|
|
|
}
|
|
|
|
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
|
|
|
await this.db.updateLibraryItem(li)
|
|
|
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
|
|
|
numItemsUpdated++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
genreMerged,
|
|
|
|
numItemsUpdated
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// DELETE: api/genres/:genre
|
|
|
|
async deleteGenre(req, res) {
|
|
|
|
if (!req.user.isAdminOrUp) {
|
|
|
|
Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`)
|
|
|
|
return res.sendStatus(404)
|
|
|
|
}
|
|
|
|
|
|
|
|
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
|
|
|
|
|
|
|
let numItemsUpdated = 0
|
|
|
|
for (const li of this.db.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)
|
|
|
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
|
|
|
numItemsUpdated++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
numItemsUpdated
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-08-02 01:06:22 +02:00
|
|
|
validateCronExpression(req, res) {
|
|
|
|
const expression = req.body.expression
|
|
|
|
if (!expression) {
|
|
|
|
return res.sendStatus(400)
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
patternValidation(expression)
|
|
|
|
res.sendStatus(200)
|
|
|
|
} catch (error) {
|
|
|
|
Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message)
|
|
|
|
res.status(400).send(error.message)
|
|
|
|
}
|
|
|
|
}
|
2022-03-18 17:51:55 +01:00
|
|
|
}
|
|
|
|
module.exports = new MiscController()
|