From 8d03b23f46bf2e6daa37f43f03db02df9e65acef Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 13 Aug 2023 13:10:34 -0500 Subject: [PATCH] Update MiscController api routes to load library items from db --- server/controllers/MiscController.js | 230 +++++++++++++++------ server/utils/queries/libraryFilters.js | 1 - server/utils/queries/libraryItemFilters.js | 126 +++++++++++ 3 files changed, 291 insertions(+), 66 deletions(-) create mode 100644 server/utils/queries/libraryItemFilters.js diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 6bf889fb..4442a5ce 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -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({ diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 3229de3e..c0a93d15 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -1,6 +1,5 @@ const Sequelize = require('sequelize') const Database = require('../../Database') -const Logger = require('../../Logger') const libraryItemsBookFilters = require('./libraryItemsBookFilters') const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters') diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js new file mode 100644 index 00000000..7fe8fbeb --- /dev/null +++ b/server/utils/queries/libraryItemFilters.js @@ -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} + */ + 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} + */ + 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 + } +} \ No newline at end of file