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

View File

@ -1,6 +1,5 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
const Logger = require('../../Logger')
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
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
}
}