mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-02-28 21:19:42 +00:00
- Remove disabled state from series name input in edit modal - Add duplicate name validation in SeriesController (resolves TODO) - Update nameIgnorePrefix when series name changes - Add frontend validation for duplicate series names - Add updateSeriesName method to PATCH series immediately on rename - Show backend error message in toast on failure - Add i18n string for duplicate name error message This enables users to rename existing series while preventing duplicate series names within the same library.
134 lines
4.4 KiB
JavaScript
134 lines
4.4 KiB
JavaScript
const { Request, Response, NextFunction } = require('express')
|
|
const Logger = require('../Logger')
|
|
const SocketAuthority = require('../SocketAuthority')
|
|
const Database = require('../Database')
|
|
|
|
const RssFeedManager = require('../managers/RssFeedManager')
|
|
|
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
|
const { getTitleIgnorePrefix } = require('../utils/index')
|
|
|
|
/**
|
|
* @typedef RequestUserObject
|
|
* @property {import('../models/User')} user
|
|
*
|
|
* @typedef {Request & RequestUserObject} RequestWithUser
|
|
*
|
|
* @typedef RequestEntityObject
|
|
* @property {import('../models/Series')} series
|
|
*
|
|
* @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest
|
|
*/
|
|
|
|
class SeriesController {
|
|
constructor() {}
|
|
|
|
/**
|
|
* @deprecated
|
|
* /api/series/:id
|
|
*
|
|
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
|
|
* Series are not library specific so we need to know what the library id is
|
|
*
|
|
* @param {SeriesControllerRequest} req
|
|
* @param {Response} res
|
|
*/
|
|
async findOne(req, res) {
|
|
const include = (req.query.include || '')
|
|
.split(',')
|
|
.map((v) => v.trim())
|
|
.filter((v) => !!v)
|
|
|
|
const seriesJson = req.series.toOldJSON()
|
|
|
|
// Add progress map with isFinished flag
|
|
if (include.includes('progress')) {
|
|
const libraryItemsInSeries = req.libraryItemsInSeries
|
|
const libraryItemsFinished = libraryItemsInSeries.filter((li) => {
|
|
return req.user.getMediaProgress(li.media.id)?.isFinished
|
|
})
|
|
seriesJson.progress = {
|
|
libraryItemIds: libraryItemsInSeries.map((li) => li.id),
|
|
libraryItemIdsFinished: libraryItemsFinished.map((li) => li.id),
|
|
isFinished: libraryItemsFinished.length === libraryItemsInSeries.length
|
|
}
|
|
}
|
|
|
|
if (include.includes('rssfeed')) {
|
|
const feedObj = await RssFeedManager.findFeedForEntityId(seriesJson.id)
|
|
seriesJson.rssFeed = feedObj?.toOldJSONMinified() || null
|
|
}
|
|
|
|
res.json(seriesJson)
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/series/:id
|
|
* Update series metadata (name, description)
|
|
*
|
|
* @param {SeriesControllerRequest} req
|
|
* @param {Response} res
|
|
*/
|
|
async update(req, res) {
|
|
const keysToUpdate = ['name', 'description']
|
|
const payload = {}
|
|
for (const key of keysToUpdate) {
|
|
if (req.body[key] !== undefined && typeof req.body[key] === 'string') {
|
|
payload[key] = req.body[key]
|
|
}
|
|
}
|
|
if (!Object.keys(payload).length) {
|
|
return res.status(400).send('No valid fields to update')
|
|
}
|
|
|
|
// Check for duplicate series name in the same library
|
|
if (payload.name && payload.name.toLowerCase() !== req.series.name.toLowerCase()) {
|
|
const existingSeries = await Database.seriesModel.getByNameAndLibrary(payload.name, req.series.libraryId)
|
|
if (existingSeries && existingSeries.id !== req.series.id) {
|
|
return res.status(400).send('A series with that name already exists in this library')
|
|
}
|
|
// Update nameIgnorePrefix when name changes
|
|
payload.nameIgnorePrefix = getTitleIgnorePrefix(payload.name)
|
|
}
|
|
|
|
req.series.set(payload)
|
|
if (req.series.changed()) {
|
|
await req.series.save()
|
|
SocketAuthority.emitter('series_updated', req.series.toOldJSON())
|
|
}
|
|
res.json(req.series.toOldJSON())
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {RequestWithUser} req
|
|
* @param {Response} res
|
|
* @param {NextFunction} next
|
|
*/
|
|
async middleware(req, res, next) {
|
|
const series = await Database.seriesModel.findByPk(req.params.id)
|
|
if (!series) return res.sendStatus(404)
|
|
|
|
/**
|
|
* Filter out any library items not accessible to user
|
|
*/
|
|
const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
|
if (!libraryItems.length) {
|
|
Logger.warn(`[SeriesController] User "${req.user.username}" attempted to access series "${series.id}" with no accessible books`)
|
|
return res.sendStatus(404)
|
|
}
|
|
|
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
|
Logger.warn(`[SeriesController] User "${req.user.username}" attempted to delete without permission`)
|
|
return res.sendStatus(403)
|
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
|
Logger.warn(`[SeriesController] User "${req.user.username}" attempted to update without permission`)
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
req.series = series
|
|
req.libraryItemsInSeries = libraryItems
|
|
next()
|
|
}
|
|
}
|
|
module.exports = new SeriesController()
|