mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Update MiscController api routes to load library items from db
This commit is contained in:
parent
fc44c801f2
commit
8d03b23f46
@ -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) => {
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
|
||||
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 "${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,44 +272,74 @@ 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())
|
||||
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({
|
||||
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) {
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||
await Database.updateLibraryItem(li)
|
||||
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||
|
||||
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 "${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,18 +399,20 @@ 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())
|
||||
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({
|
||||
numItemsUpdated
|
||||
|
@ -1,6 +1,5 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const Database = require('../../Database')
|
||||
const Logger = require('../../Logger')
|
||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||
|
||||
|
126
server/utils/queries/libraryItemFilters.js
Normal file
126
server/utils/queries/libraryItemFilters.js
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user