mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-19 10:19:37 +00:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
2fdab39e27
35 changed files with 1252 additions and 144 deletions
|
|
@ -235,12 +235,14 @@ class LibraryController {
|
|||
for (const key of keysToCheck) {
|
||||
if (!req.body[key]) continue
|
||||
if (typeof req.body[key] !== 'string') {
|
||||
Logger.error(`[LibraryController] Invalid request. ${key} must be a string`)
|
||||
return res.status(400).send(`Invalid request. ${key} must be a string`)
|
||||
}
|
||||
updatePayload[key] = req.body[key]
|
||||
}
|
||||
if (req.body.displayOrder !== undefined) {
|
||||
if (isNaN(req.body.displayOrder)) {
|
||||
Logger.error(`[LibraryController] Invalid request. displayOrder must be a number`)
|
||||
return res.status(400).send('Invalid request. displayOrder must be a number')
|
||||
}
|
||||
updatePayload.displayOrder = req.body.displayOrder
|
||||
|
|
@ -255,18 +257,29 @@ class LibraryController {
|
|||
}
|
||||
|
||||
// Validate settings
|
||||
const defaultLibrarySettings = Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType)
|
||||
const updatedSettings = {
|
||||
...(req.library.settings || Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType))
|
||||
...(req.library.settings || defaultLibrarySettings)
|
||||
}
|
||||
// In case new settings are added in the future, ensure all settings are present
|
||||
for (const key in defaultLibrarySettings) {
|
||||
if (updatedSettings[key] === undefined) {
|
||||
updatedSettings[key] = defaultLibrarySettings[key]
|
||||
}
|
||||
}
|
||||
|
||||
let hasUpdates = false
|
||||
let hasUpdatedDisableWatcher = false
|
||||
let hasUpdatedScanCron = false
|
||||
if (req.body.settings) {
|
||||
for (const key in req.body.settings) {
|
||||
if (updatedSettings[key] === undefined) continue
|
||||
if (!Object.keys(defaultLibrarySettings).includes(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === 'metadataPrecedence') {
|
||||
if (!Array.isArray(req.body.settings[key])) {
|
||||
Logger.error(`[LibraryController] Invalid request. Settings "metadataPrecedence" must be an array`)
|
||||
return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
|
||||
}
|
||||
if (JSON.stringify(req.body.settings[key]) !== JSON.stringify(updatedSettings[key])) {
|
||||
|
|
@ -276,6 +289,7 @@ class LibraryController {
|
|||
}
|
||||
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
|
||||
if (req.body.settings[key] !== null && typeof req.body.settings[key] !== 'string') {
|
||||
Logger.error(`[LibraryController] Invalid request. Settings "${key}" must be a string`)
|
||||
return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
|
||||
}
|
||||
if (req.body.settings[key] !== updatedSettings[key]) {
|
||||
|
|
@ -285,8 +299,35 @@ class LibraryController {
|
|||
updatedSettings[key] = req.body.settings[key]
|
||||
Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
|
||||
}
|
||||
} else if (key === 'markAsFinishedPercentComplete') {
|
||||
if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) {
|
||||
Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be a number`)
|
||||
return res.status(400).send(`Invalid request. Setting "${key}" must be a number`)
|
||||
} else if (req.body.settings[key] !== null && (Number(req.body.settings[key]) < 0 || Number(req.body.settings[key]) > 100)) {
|
||||
Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be between 0 and 100`)
|
||||
return res.status(400).send(`Invalid request. Setting "${key}" must be between 0 and 100`)
|
||||
}
|
||||
if (req.body.settings[key] !== updatedSettings[key]) {
|
||||
hasUpdates = true
|
||||
updatedSettings[key] = Number(req.body.settings[key])
|
||||
Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
|
||||
}
|
||||
} else if (key === 'markAsFinishedTimeRemaining') {
|
||||
if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) {
|
||||
Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be a number`)
|
||||
return res.status(400).send(`Invalid request. Setting "${key}" must be a number`)
|
||||
} else if (req.body.settings[key] !== null && Number(req.body.settings[key]) < 0) {
|
||||
Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be greater than or equal to 0`)
|
||||
return res.status(400).send(`Invalid request. Setting "${key}" must be greater than or equal to 0`)
|
||||
}
|
||||
if (req.body.settings[key] !== updatedSettings[key]) {
|
||||
hasUpdates = true
|
||||
updatedSettings[key] = Number(req.body.settings[key])
|
||||
Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
|
||||
}
|
||||
} else {
|
||||
if (typeof req.body.settings[key] !== typeof updatedSettings[key]) {
|
||||
Logger.error(`[LibraryController] Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`)
|
||||
return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`)
|
||||
}
|
||||
if (req.body.settings[key] !== updatedSettings[key]) {
|
||||
|
|
@ -328,6 +369,7 @@ class LibraryController {
|
|||
return false
|
||||
})
|
||||
if (!success) {
|
||||
Logger.error(`[LibraryController] Invalid folder directory "${path}"`)
|
||||
return res.status(400).send(`Invalid folder directory "${path}"`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@ class LibraryItemController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
static handleDownloadError(error, res) {
|
||||
if (!res.headersSent) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).send('File not found')
|
||||
} else {
|
||||
return res.status(500).send('Download failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/items/:id/download
|
||||
* Download library item. Zip file if multiple files.
|
||||
|
|
@ -122,7 +132,7 @@ class LibraryItemController {
|
|||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
download(req, res) {
|
||||
async download(req, res) {
|
||||
if (!req.user.canDownload) {
|
||||
Logger.warn(`User "${req.user.username}" attempted to download without permission`)
|
||||
return res.sendStatus(403)
|
||||
|
|
@ -130,21 +140,26 @@ class LibraryItemController {
|
|||
const libraryItemPath = req.libraryItem.path
|
||||
const itemTitle = req.libraryItem.media.metadata.title
|
||||
|
||||
// If library item is a single file in root dir then no need to zip
|
||||
if (req.libraryItem.isFile) {
|
||||
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryItemPath))
|
||||
if (audioMimeType) {
|
||||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
res.download(libraryItemPath, req.libraryItem.relPath)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
const filename = `${itemTitle}.zip`
|
||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
|
||||
try {
|
||||
// If library item is a single file in root dir then no need to zip
|
||||
if (req.libraryItem.isFile) {
|
||||
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryItemPath))
|
||||
if (audioMimeType) {
|
||||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
await new Promise((resolve, reject) => res.download(libraryItemPath, req.libraryItem.relPath, (error) => (error ? reject(error) : resolve())))
|
||||
} else {
|
||||
const filename = `${itemTitle}.zip`
|
||||
await zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
}
|
||||
Logger.info(`[LibraryItemController] Downloaded item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] Download failed for item "${itemTitle}" at "${libraryItemPath}"`, error)
|
||||
LibraryItemController.handleDownloadError(error, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -845,7 +860,13 @@ class LibraryItemController {
|
|||
res.setHeader('Content-Type', audioMimeType)
|
||||
}
|
||||
|
||||
res.download(libraryFile.metadata.path, libraryFile.metadata.filename)
|
||||
try {
|
||||
await new Promise((resolve, reject) => res.download(libraryFile.metadata.path, libraryFile.metadata.filename, (error) => (error ? reject(error) : resolve())))
|
||||
Logger.info(`[LibraryItemController] Downloaded file "${libraryFile.metadata.path}"`)
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] Failed to download file "${libraryFile.metadata.path}"`, error)
|
||||
LibraryItemController.handleDownloadError(error, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -883,7 +904,13 @@ class LibraryItemController {
|
|||
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
|
||||
}
|
||||
|
||||
res.sendFile(ebookFilePath)
|
||||
try {
|
||||
await new Promise((resolve, reject) => res.sendFile(ebookFilePath, (error) => (error ? reject(error) : resolve())))
|
||||
Logger.info(`[LibraryItemController] Downloaded ebook file "${ebookFilePath}"`)
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] Failed to download ebook file "${ebookFilePath}"`, error)
|
||||
LibraryItemController.handleDownloadError(error, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -394,6 +394,58 @@ class MeController {
|
|||
res.json(req.user.toOldJSONForBrowser())
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/me/ereader-devices
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async updateUserEReaderDevices(req, res) {
|
||||
if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) {
|
||||
return res.status(400).send('Invalid payload. ereaderDevices array required')
|
||||
}
|
||||
|
||||
const userEReaderDevices = req.body.ereaderDevices
|
||||
for (const device of userEReaderDevices) {
|
||||
if (!device.name || !device.email) {
|
||||
return res.status(400).send('Invalid payload. ereaderDevices array items must have name and email')
|
||||
} else if (device.availabilityOption !== 'specificUsers' || device.users?.length !== 1 || device.users[0] !== req.user.id) {
|
||||
return res.status(400).send('Invalid payload. ereaderDevices array items must have availabilityOption "specificUsers" and only the current user')
|
||||
}
|
||||
}
|
||||
|
||||
const otherDevices = Database.emailSettings.ereaderDevices.filter((device) => {
|
||||
return !Database.emailSettings.checkUserCanAccessDevice(device, req.user) || device.users?.length !== 1
|
||||
})
|
||||
|
||||
const ereaderDevices = otherDevices.concat(userEReaderDevices)
|
||||
|
||||
// Check for duplicate names
|
||||
const nameSet = new Set()
|
||||
const hasDupes = ereaderDevices.some((device) => {
|
||||
if (nameSet.has(device.name)) {
|
||||
return true // Duplicate found
|
||||
}
|
||||
nameSet.add(device.name)
|
||||
return false
|
||||
})
|
||||
|
||||
if (hasDupes) {
|
||||
return res.status(400).send('Invalid payload. Duplicate "name" field found.')
|
||||
}
|
||||
|
||||
const updated = Database.emailSettings.update({ ereaderDevices })
|
||||
if (updated) {
|
||||
await Database.updateSetting(Database.emailSettings)
|
||||
SocketAuthority.clientEmitter(req.user.id, 'ereader-devices-updated', {
|
||||
ereaderDevices: Database.emailSettings.ereaderDevices
|
||||
})
|
||||
}
|
||||
res.json({
|
||||
ereaderDevices: Database.emailSettings.getEReaderDevices(req.user)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/stats/year/:year
|
||||
*
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ class PlaybackSessionManager {
|
|||
* @returns
|
||||
*/
|
||||
async syncLocalSession(user, sessionJson, deviceInfo) {
|
||||
// TODO: Combine libraryItem query with library query
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(sessionJson.libraryItemId)
|
||||
const episode = sessionJson.episodeId && libraryItem && libraryItem.isPodcast ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
||||
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
||||
|
|
@ -130,6 +131,16 @@ class PlaybackSessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PlaybackSessionManager] syncLocalSession: Library not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
||||
return {
|
||||
id: sessionJson.id,
|
||||
success: false,
|
||||
error: 'Library not found'
|
||||
}
|
||||
}
|
||||
|
||||
sessionJson.userId = user.id
|
||||
sessionJson.serverVersion = serverVersion
|
||||
|
||||
|
|
@ -199,7 +210,9 @@ class PlaybackSessionManager {
|
|||
const updateResponse = await user.createUpdateMediaProgressFromPayload({
|
||||
libraryItemId: libraryItem.id,
|
||||
episodeId: session.episodeId,
|
||||
...session.mediaProgressObject
|
||||
...session.mediaProgressObject,
|
||||
markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete,
|
||||
markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining
|
||||
})
|
||||
result.progressSynced = !!updateResponse.mediaProgress
|
||||
if (result.progressSynced) {
|
||||
|
|
@ -211,7 +224,9 @@ class PlaybackSessionManager {
|
|||
const updateResponse = await user.createUpdateMediaProgressFromPayload({
|
||||
libraryItemId: libraryItem.id,
|
||||
episodeId: session.episodeId,
|
||||
...session.mediaProgressObject
|
||||
...session.mediaProgressObject,
|
||||
markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete,
|
||||
markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining
|
||||
})
|
||||
result.progressSynced = !!updateResponse.mediaProgress
|
||||
if (result.progressSynced) {
|
||||
|
|
@ -330,12 +345,19 @@ class PlaybackSessionManager {
|
|||
* @returns
|
||||
*/
|
||||
async syncSession(user, session, syncData) {
|
||||
// TODO: Combine libraryItem query with library query
|
||||
const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library not found "${libraryItem.libraryId}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
session.currentTime = syncData.currentTime
|
||||
session.addListeningTime(syncData.timeListened)
|
||||
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" (Device: ${session.deviceDescription}) | Total Time Listened: ${session.timeListening}`)
|
||||
|
|
@ -344,12 +366,11 @@ class PlaybackSessionManager {
|
|||
libraryItemId: libraryItem.id,
|
||||
episodeId: session.episodeId,
|
||||
// duration no longer required (v2.15.1) but used if available
|
||||
duration: syncData.duration || libraryItem.media.duration || 0,
|
||||
duration: syncData.duration || session.duration || 0,
|
||||
currentTime: syncData.currentTime,
|
||||
progress: session.progress
|
||||
// TODO: Add support for passing in these values from library settings
|
||||
// markAsFinishedTimeRemaining: 5,
|
||||
// markAsFinishedPercentageComplete: 95
|
||||
progress: session.progress,
|
||||
markAsFinishedTimeRemaining: library.librarySettings.markAsFinishedTimeRemaining,
|
||||
markAsFinishedPercentComplete: library.librarySettings.markAsFinishedPercentComplete
|
||||
})
|
||||
if (updateResponse.mediaProgress) {
|
||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const Logger = require('../Logger')
|
|||
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
||||
* @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read
|
||||
* @property {string[]} metadataPrecedence
|
||||
* @property {number} markAsFinishedTimeRemaining Time remaining in seconds to mark as finished. (defaults to 10s)
|
||||
* @property {number} markAsFinishedPercentComplete Percent complete to mark as finished (0-100). If this is set it will be used over markAsFinishedTimeRemaining.
|
||||
*/
|
||||
|
||||
class Library extends Model {
|
||||
|
|
@ -57,7 +59,9 @@ class Library extends Model {
|
|||
coverAspectRatio: 1, // Square
|
||||
disableWatcher: false,
|
||||
autoScanCronExpression: null,
|
||||
podcastSearchRegion: 'us'
|
||||
podcastSearchRegion: 'us',
|
||||
markAsFinishedPercentComplete: null,
|
||||
markAsFinishedTimeRemaining: 10
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
|
|
@ -70,7 +74,9 @@ class Library extends Model {
|
|||
epubsAllowScriptedContent: false,
|
||||
hideSingleBookSeries: false,
|
||||
onlyShowLaterBooksInContinueSeries: false,
|
||||
metadataPrecedence: this.defaultMetadataPrecedence
|
||||
metadataPrecedence: this.defaultMetadataPrecedence,
|
||||
markAsFinishedPercentComplete: null,
|
||||
markAsFinishedTimeRemaining: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -196,6 +202,13 @@ class Library extends Model {
|
|||
return this.extraData?.lastScanMetadataPrecedence || []
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {LibrarySettingsObject}
|
||||
*/
|
||||
get librarySettings() {
|
||||
return this.settings || Library.getDefaultLibrarySettingsForMediaType(this.mediaType)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update to use new model
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -229,30 +229,30 @@ class MediaProgress extends Model {
|
|||
const timeRemaining = this.duration - this.currentTime
|
||||
|
||||
// Check if progress is far enough to mark as finished
|
||||
// - If markAsFinishedPercentageComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 5 seconds)
|
||||
// - If markAsFinishedPercentComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 10 seconds)
|
||||
let shouldMarkAsFinished = false
|
||||
if (!this.isFinished && this.duration) {
|
||||
if (!isNullOrNaN(progressPayload.markAsFinishedPercentageComplete)) {
|
||||
const markAsFinishedPercentageComplete = Number(progressPayload.markAsFinishedPercentageComplete) / 100
|
||||
shouldMarkAsFinished = markAsFinishedPercentageComplete <= this.progress
|
||||
if (this.duration) {
|
||||
if (!isNullOrNaN(progressPayload.markAsFinishedPercentComplete) && progressPayload.markAsFinishedPercentComplete > 0) {
|
||||
const markAsFinishedPercentComplete = Number(progressPayload.markAsFinishedPercentComplete) / 100
|
||||
shouldMarkAsFinished = markAsFinishedPercentComplete < this.progress
|
||||
if (shouldMarkAsFinished) {
|
||||
Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentageComplete}`)
|
||||
Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentComplete}`)
|
||||
}
|
||||
} else {
|
||||
const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 5 : Number(progressPayload.markAsFinishedTimeRemaining)
|
||||
shouldMarkAsFinished = timeRemaining <= markAsFinishedTimeRemaining
|
||||
const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 10 : Number(progressPayload.markAsFinishedTimeRemaining)
|
||||
shouldMarkAsFinished = timeRemaining < markAsFinishedTimeRemaining
|
||||
if (shouldMarkAsFinished) {
|
||||
Logger.debug(`[MediaProgress] Marking media progress as finished because time remaining (${timeRemaining}) is less than ${markAsFinishedTimeRemaining} seconds`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldMarkAsFinished) {
|
||||
if (!this.isFinished && shouldMarkAsFinished) {
|
||||
this.isFinished = true
|
||||
this.finishedAt = this.finishedAt || Date.now()
|
||||
this.extraData.progress = 1
|
||||
this.changed('extraData', true)
|
||||
} else if (this.isFinished && this.changed('currentTime') && this.currentTime < this.duration) {
|
||||
} else if (this.isFinished && this.changed('currentTime') && !shouldMarkAsFinished) {
|
||||
this.isFinished = false
|
||||
this.finishedAt = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const { DataTypes, Model } = sequelize
|
|||
* @property {string} [finishedAt]
|
||||
* @property {number} [lastUpdate]
|
||||
* @property {number} [markAsFinishedTimeRemaining]
|
||||
* @property {number} [markAsFinishedPercentageComplete]
|
||||
* @property {number} [markAsFinishedPercentComplete]
|
||||
*/
|
||||
|
||||
class User extends Model {
|
||||
|
|
@ -82,6 +82,7 @@ class User extends Model {
|
|||
canAccessExplicitContent: 'accessExplicitContent',
|
||||
canAccessAllLibraries: 'accessAllLibraries',
|
||||
canAccessAllTags: 'accessAllTags',
|
||||
canCreateEReader: 'createEreader',
|
||||
tagsAreDenylist: 'selectedTagsNotAccessible',
|
||||
// Direct mapping for array-based permissions
|
||||
allowedLibraries: 'librariesAccessible',
|
||||
|
|
@ -122,6 +123,7 @@ class User extends Model {
|
|||
update: type === 'root' || type === 'admin',
|
||||
delete: type === 'root',
|
||||
upload: type === 'root' || type === 'admin',
|
||||
createEreader: type === 'root' || type === 'admin',
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
accessExplicitContent: type === 'root' || type === 'admin',
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ class ApiRouter {
|
|||
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
|
||||
this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this))
|
||||
this.router.get('/me/stats/year/:year', MeController.getStatsForYear.bind(this))
|
||||
this.router.post('/me/ereader-devices', MeController.updateUserEReaderDevices.bind(this))
|
||||
|
||||
//
|
||||
// Backup Routes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue