Update MiscController api routes to load library items from db

This commit is contained in:
advplyr 2023-08-13 13:10:34 -05:00
parent fc44c801f2
commit 8d03b23f46
3 changed files with 291 additions and 66 deletions

View File

@ -1,9 +1,11 @@
const Sequelize = require('sequelize')
const Path = require('path') const Path = require('path')
const fs = require('../libs/fsExtra') const fs = require('../libs/fsExtra')
const Logger = require('../Logger') const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority') const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database') const Database = require('../Database')
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
const filePerms = require('../utils/filePerms') const filePerms = require('../utils/filePerms')
const patternValidation = require('../libs/nodeCron/pattern-validation') const patternValidation = require('../libs/nodeCron/pattern-validation')
const { isObject } = require('../utils/index') const { isObject } = require('../utils/index')
@ -14,7 +16,12 @@ const { isObject } = require('../utils/index')
class MiscController { class MiscController {
constructor() { } constructor() { }
// POST: api/upload /**
* POST: /api/upload
* Update library item
* @param {*} req
* @param {*} res
*/
async handleUpload(req, res) { async handleUpload(req, res) {
if (!req.user.canUpload) { if (!req.user.canUpload) {
Logger.warn('User attempted to upload without permission', req.user) Logger.warn('User attempted to upload without permission', req.user)
@ -88,7 +95,12 @@ class MiscController {
res.sendStatus(200) res.sendStatus(200)
} }
// GET: api/tasks /**
* GET: /api/tasks
* Get tasks for task manager
* @param {*} req
* @param {*} res
*/
getTasks(req, res) { getTasks(req, res) {
const includeArray = (req.query.include || '').split(',') const includeArray = (req.query.include || '').split(',')
@ -105,7 +117,12 @@ class MiscController {
res.json(data) res.json(data)
} }
// PATCH: api/settings (admin) /**
* PATCH: /api/settings
* Update server settings
* @param {*} req
* @param {*} res
*/
async updateServerSettings(req, res) { async updateServerSettings(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to update server settings', req.user) Logger.error('User other than admin attempting to update server settings', req.user)
@ -147,26 +164,55 @@ class MiscController {
res.json(userResponse) res.json(userResponse)
} }
// GET: api/tags /**
getAllTags(req, res) { * GET: /api/tags
* Get all tags
* @param {*} req
* @param {*} res
*/
async getAllTags(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`) Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const tags = [] const tags = []
Database.libraryItems.forEach((li) => { const books = await Database.models.book.findAll({
if (li.media.tags && li.media.tags.length) { attributes: ['tags'],
li.media.tags.forEach((tag) => { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0
})
})
for (const book of books) {
for (const tag of book.tags) {
if (!tags.includes(tag)) tags.push(tag) if (!tags.includes(tag)) tags.push(tag)
})
} }
}
const podcasts = await Database.models.podcast.findAll({
attributes: ['tags'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('tags')), {
[Sequelize.Op.gt]: 0
}) })
})
for (const podcast of podcasts) {
for (const tag of podcast.tags) {
if (!tags.includes(tag)) tags.push(tag)
}
}
res.json({ res.json({
tags: tags tags: tags
}) })
} }
// POST: api/tags/rename /**
* POST: /api/tags/rename
* Rename tag
* Req.body { tag, newTag }
* @param {*} req
* @param {*} res
*/
async renameTag(req, res) { async renameTag(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to renameTag`) Logger.error(`[MiscController] Non-admin user attempted to renameTag`)
@ -183,19 +229,24 @@ class MiscController {
let tagMerged = false let tagMerged = false
let numItemsUpdated = 0 let numItemsUpdated = 0
for (const li of Database.libraryItems) { const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag, newTag])
if (!li.media.tags || !li.media.tags.length) continue for (const libraryItem of libraryItemsWithTag) {
let existingTags = libraryItem.media.tags
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge if (existingTags.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 Database.updateLibraryItem(li) if (existingTags.includes(tag)) {
SocketAuthority.emitter('item_updated', li.toJSONExpanded()) existingTags = existingTags.filter(t => t !== tag) // Remove old tag
if (!existingTags.includes(newTag)) {
existingTags.push(newTag)
}
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${libraryItem.media.title}"`)
await libraryItem.media.update({
tags: existingTags
})
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
} }
@ -206,7 +257,13 @@ class MiscController {
}) })
} }
// DELETE: api/tags/:tag /**
* DELETE: /api/tags/:tag
* Remove a tag
* :tag param is base64 encoded
* @param {*} req
* @param {*} res
*/
async deleteTag(req, res) { async deleteTag(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to deleteTag`) Logger.error(`[MiscController] Non-admin user attempted to deleteTag`)
@ -215,44 +272,74 @@ class MiscController {
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
let numItemsUpdated = 0 // Get all items with tag
for (const li of Database.libraryItems) { const libraryItemsWithTag = await libraryItemFilters.getAllLibraryItemsWithTags([tag])
if (!li.media.tags || !li.media.tags.length) continue
if (li.media.tags.includes(tag)) { let numItemsUpdated = 0
li.media.tags = li.media.tags.filter(t => t !== tag) // Remove tag from items
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`) for (const libraryItem of libraryItemsWithTag) {
await Database.updateLibraryItem(li) Logger.debug(`[MiscController] Remove tag "${tag}" from item "${libraryItem.media.title}"`)
SocketAuthority.emitter('item_updated', li.toJSONExpanded()) await libraryItem.media.update({
tags: libraryItem.media.tags.filter(t => t !== tag)
})
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
}
res.json({ res.json({
numItemsUpdated numItemsUpdated
}) })
} }
// GET: api/genres /**
getAllGenres(req, res) { * GET: /api/genres
* Get all genres
* @param {*} req
* @param {*} res
*/
async getAllGenres(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`) Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const genres = [] const genres = []
Database.libraryItems.forEach((li) => { const books = await Database.models.book.findAll({
if (li.media.metadata.genres && li.media.metadata.genres.length) { attributes: ['genres'],
li.media.metadata.genres.forEach((genre) => { where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
if (!genres.includes(genre)) genres.push(genre) [Sequelize.Op.gt]: 0
}) })
})
for (const book of books) {
for (const tag of book.genres) {
if (!genres.includes(tag)) genres.push(tag)
} }
}
const podcasts = await Database.models.podcast.findAll({
attributes: ['genres'],
where: Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('genres')), {
[Sequelize.Op.gt]: 0
}) })
})
for (const podcast of podcasts) {
for (const tag of podcast.genres) {
if (!genres.includes(tag)) genres.push(tag)
}
}
res.json({ res.json({
genres genres
}) })
} }
// POST: api/genres/rename /**
* POST: /api/genres/rename
* Rename genres
* Req.body { genre, newGenre }
* @param {*} req
* @param {*} res
*/
async renameGenre(req, res) { async renameGenre(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to renameGenre`) Logger.error(`[MiscController] Non-admin user attempted to renameGenre`)
@ -269,19 +356,24 @@ class MiscController {
let genreMerged = false let genreMerged = false
let numItemsUpdated = 0 let numItemsUpdated = 0
for (const li of Database.libraryItems) { const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre, newGenre])
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue for (const libraryItem of libraryItemsWithGenre) {
let existingGenres = libraryItem.media.genres
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge if (existingGenres.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 Database.updateLibraryItem(li) if (existingGenres.includes(genre)) {
SocketAuthority.emitter('item_updated', li.toJSONExpanded()) existingGenres = existingGenres.filter(t => t !== genre) // Remove old genre
if (!existingGenres.includes(newGenre)) {
existingGenres.push(newGenre)
}
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${libraryItem.media.title}"`)
await libraryItem.media.update({
genres: existingGenres
})
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
} }
@ -292,7 +384,13 @@ class MiscController {
}) })
} }
// DELETE: api/genres/:genre /**
* DELETE: /api/genres/:genre
* Remove a genre
* :genre param is base64 encoded
* @param {*} req
* @param {*} res
*/
async deleteGenre(req, res) { async deleteGenre(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`) Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`)
@ -301,18 +399,20 @@ class MiscController {
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
let numItemsUpdated = 0 // Get all items with genre
for (const li of Database.libraryItems) { const libraryItemsWithGenre = await libraryItemFilters.getAllLibraryItemsWithGenres([genre])
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
if (li.media.metadata.genres.includes(genre)) { let numItemsUpdated = 0
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre) // Remove genre from items
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`) for (const libraryItem of libraryItemsWithGenre) {
await Database.updateLibraryItem(li) Logger.debug(`[MiscController] Remove genre "${genre}" from item "${libraryItem.media.title}"`)
SocketAuthority.emitter('item_updated', li.toJSONExpanded()) await libraryItem.media.update({
genres: libraryItem.media.genres.filter(g => g !== genre)
})
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++ numItemsUpdated++
} }
}
res.json({ res.json({
numItemsUpdated numItemsUpdated

View File

@ -1,6 +1,5 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const Logger = require('../../Logger')
const libraryItemsBookFilters = require('./libraryItemsBookFilters') const libraryItemsBookFilters = require('./libraryItemsBookFilters')
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters') const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')

View File

@ -0,0 +1,126 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
module.exports = {
/**
* Get all library items that have tags
* @param {string[]} tags
* @returns {Promise<LibraryItem[]>}
*/
async getAllLibraryItemsWithTags(tags) {
const libraryItems = []
const booksWithTag = await Database.models.book.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), {
[Sequelize.Op.gte]: 1
}),
replacements: {
tags
},
include: [
{
model: Database.models.libraryItem
},
{
model: Database.models.author,
through: {
attributes: []
}
},
{
model: Database.models.series,
through: {
attributes: ['sequence']
}
}
]
})
for (const book of booksWithTag) {
const libraryItem = book.libraryItem
libraryItem.media = book
libraryItems.push(libraryItem)
}
const podcastsWithTag = await Database.models.podcast.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:tags))`), {
[Sequelize.Op.gte]: 1
}),
replacements: {
tags
},
include: [
{
model: Database.models.libraryItem
},
{
model: Database.models.podcastEpisode
}
]
})
for (const podcast of podcastsWithTag) {
const libraryItem = podcast.libraryItem
libraryItem.media = podcast
libraryItems.push(libraryItem)
}
return libraryItems
},
/**
* Get all library items that have genres
* @param {string[]} genres
* @returns {Promise<LibraryItem[]>}
*/
async getAllLibraryItemsWithGenres(genres) {
const libraryItems = []
const booksWithGenre = await Database.models.book.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), {
[Sequelize.Op.gte]: 1
}),
replacements: {
genres
},
include: [
{
model: Database.models.libraryItem
},
{
model: Database.models.author,
through: {
attributes: []
}
},
{
model: Database.models.series,
through: {
attributes: ['sequence']
}
}
]
})
for (const book of booksWithGenre) {
const libraryItem = book.libraryItem
libraryItem.media = book
libraryItems.push(libraryItem)
}
const podcastsWithGenre = await Database.models.podcast.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(genres) WHERE json_valid(genres) AND json_each.value IN (:genres))`), {
[Sequelize.Op.gte]: 1
}),
replacements: {
genres
},
include: [
{
model: Database.models.libraryItem
},
{
model: Database.models.podcastEpisode
}
]
})
for (const podcast of podcastsWithGenre) {
const libraryItem = podcast.libraryItem
libraryItem.media = podcast
libraryItems.push(libraryItem)
}
return libraryItems
}
}