Update more API endpoints to use new user model

This commit is contained in:
advplyr 2024-08-11 15:15:34 -05:00
parent 9facf77ff1
commit afc16358ca
23 changed files with 856 additions and 404 deletions

View File

@ -365,7 +365,7 @@ class AuthorController {
if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -337,7 +337,7 @@ class CollectionController {
if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -1,3 +1,4 @@
const { Request, Response, NextFunction } = require('express')
const Path = require('path')
const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
@ -15,6 +16,14 @@ const CacheManager = require('../managers/CacheManager')
const CoverManager = require('../managers/CoverManager')
const ShareManager = require('../managers/ShareManager')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class LibraryItemController {
constructor() {}
@ -328,7 +337,14 @@ class LibraryItemController {
return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options)
}
// POST: api/items/:id/play
/**
* POST: /api/items/:id/play
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
startPlaybackSession(req, res) {
if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') {
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
@ -338,7 +354,14 @@ class LibraryItemController {
this.playbackSessionManager.startSessionRequest(req, res, null)
}
// POST: api/items/:id/play/:episodeId
/**
* POST: /api/items/:id/play/:episodeId
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
startEpisodePlaybackSession(req, res) {
var libraryItem = req.libraryItem
if (!libraryItem.media.numTracks) {
@ -830,7 +853,7 @@ class LibraryItemController {
} else if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -12,7 +12,6 @@ const userStats = require('../utils/queries/userStats')
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*
*/
class MeController {

View File

@ -1,5 +1,6 @@
const Sequelize = require('sequelize')
const Path = require('path')
const { Request, Response } = require('express')
const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
@ -13,21 +14,27 @@ const { sanitizeFilename } = require('../utils/fileUtils')
const TaskManager = require('../managers/TaskManager')
const adminStats = require('../utils/queries/adminStats')
//
// This is a controller for routes that don't have a home yet :(
//
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class MiscController {
constructor() {}
/**
* POST: /api/upload
* Update library item
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async handleUpload(req, res) {
if (!req.user.canUpload) {
Logger.warn('User attempted to upload without permission', req.user)
if (!req.userNew.canUpload) {
Logger.warn(`User "${req.userNew.username}" attempted to upload without permission`)
return res.sendStatus(403)
}
if (!req.files) {
@ -83,8 +90,9 @@ class MiscController {
/**
* GET: /api/tasks
* Get tasks for task manager
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
getTasks(req, res) {
const includeArray = (req.query.include || '').split(',')
@ -106,12 +114,12 @@ class MiscController {
* PATCH: /api/settings
* Update server settings
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateServerSettings(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to update server settings', req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`User "${req.userNew.username}" other than admin attempting to update server settings`)
return res.sendStatus(403)
}
const settingsUpdate = req.body
@ -137,12 +145,12 @@ class MiscController {
/**
* PATCH: /api/sorting-prefixes
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateSortingPrefixes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to update server sorting prefixes', req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`User "${req.userNew.username}" other than admin attempting to update server sorting prefixes`)
return res.sendStatus(403)
}
let sortingPrefixes = req.body.sortingPrefixes
@ -237,14 +245,10 @@ class MiscController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async authorize(req, res) {
if (!req.user) {
Logger.error('Invalid user in authorize')
return res.sendStatus(401)
}
const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew)
res.json(userResponse)
}
@ -252,13 +256,14 @@ class MiscController {
/**
* GET: /api/tags
* Get all tags
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getAllTags(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllTags`)
return res.sendStatus(403)
}
const tags = []
@ -295,13 +300,14 @@ class MiscController {
* POST: /api/tags/rename
* Rename tag
* Req.body { tag, newTag }
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async renameTag(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to renameTag`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameTag`)
return res.sendStatus(403)
}
const tag = req.body.tag
@ -349,13 +355,14 @@ class MiscController {
* DELETE: /api/tags/:tag
* Remove a tag
* :tag param is base64 encoded
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async deleteTag(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to deleteTag`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteTag`)
return res.sendStatus(403)
}
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
@ -388,13 +395,14 @@ class MiscController {
/**
* GET: /api/genres
* Get all genres
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getAllGenres(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllGenres`)
return res.sendStatus(403)
}
const genres = []
const books = await Database.bookModel.findAll({
@ -430,13 +438,14 @@ class MiscController {
* POST: /api/genres/rename
* Rename genres
* Req.body { genre, newGenre }
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async renameGenre(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to renameGenre`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameGenre`)
return res.sendStatus(403)
}
const genre = req.body.genre
@ -484,13 +493,14 @@ class MiscController {
* DELETE: /api/genres/:genre
* Remove a genre
* :genre param is base64 encoded
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async deleteGenre(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteGenre`)
return res.sendStatus(403)
}
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
@ -526,15 +536,16 @@ class MiscController {
* Req.body { libraryId, path, type, [oldPath] }
* type = add, unlink, rename
* oldPath = required only for rename
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
updateWatchedPath(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`)
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to updateWatchedPath`)
return res.sendStatus(403)
}
const libraryId = req.body.libraryId
@ -586,12 +597,12 @@ class MiscController {
/**
* GET: api/auth-settings (admin only)
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
getAuthSettings(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get auth settings`)
return res.sendStatus(403)
}
return res.json(Database.serverSettings.authenticationSettings)
@ -601,12 +612,12 @@ class MiscController {
* PATCH: api/auth-settings
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateAuthSettings(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to update auth settings`)
return res.sendStatus(403)
}
@ -706,12 +717,12 @@ class MiscController {
/**
* GET: /api/stats/year/:year
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async getAdminStatsForYear(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin stats for year`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get admin stats for year`)
return res.sendStatus(403)
}
const year = Number(req.params.year)
@ -727,12 +738,12 @@ class MiscController {
* GET: /api/logger-data
* admin or up
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async getLoggerData(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get logger data`)
return res.sendStatus(403)
}

View File

@ -1,10 +1,27 @@
const Logger = require('../Logger')
const { Request, Response, NextFunction } = require('express')
const Database = require('../Database')
const { version } = require('../../package.json')
class NotificationController {
constructor() { }
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class NotificationController {
constructor() {}
/**
* GET: /api/notifications
* Get notifications, settings and data
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
get(req, res) {
res.json({
data: this.notificationManager.getData(),
@ -12,6 +29,12 @@ class NotificationController {
})
}
/**
* PATCH: /api/notifications
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async update(req, res) {
const updated = Database.notificationSettings.update(req.body)
if (updated) {
@ -20,15 +43,38 @@ class NotificationController {
res.sendStatus(200)
}
/**
* GET: /api/notificationdata
* @deprecated Use /api/notifications
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
getData(req, res) {
res.json(this.notificationManager.getData())
}
/**
* GET: /api/notifications/test
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async fireTestEvent(req, res) {
await this.notificationManager.triggerNotification('onTest', { version: `v${version}` }, req.query.fail === '1')
res.sendStatus(200)
}
/**
* POST: /api/notifications
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async createNotification(req, res) {
const success = Database.notificationSettings.createNotification(req.body)
@ -38,6 +84,12 @@ class NotificationController {
res.json(Database.notificationSettings)
}
/**
* DELETE: /api/notifications/:id
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async deleteNotification(req, res) {
if (Database.notificationSettings.removeNotification(req.notification.id)) {
await Database.updateSetting(Database.notificationSettings)
@ -45,6 +97,12 @@ class NotificationController {
res.json(Database.notificationSettings)
}
/**
* PATCH: /api/notifications/:id
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateNotification(req, res) {
const success = Database.notificationSettings.updateNotification(req.body)
if (success) {
@ -53,17 +111,32 @@ class NotificationController {
res.json(Database.notificationSettings)
}
/**
* GET: /api/notifications/:id/test
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async sendNotificationTest(req, res) {
if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured')
if (!Database.notificationSettings.isUseable) return res.status(400).send('Apprise is not configured')
const success = await this.notificationManager.sendTestNotification(req.notification)
if (success) res.sendStatus(200)
else res.sendStatus(500)
}
/**
* Requires admin or up
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
middleware(req, res, next) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(404)
if (!req.userNew.isAdminOrUp) {
return res.sendStatus(403)
}
if (req.params.id) {
@ -77,4 +150,4 @@ class NotificationController {
next()
}
}
module.exports = new NotificationController()
module.exports = new NotificationController()

View File

@ -1,21 +1,31 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
const Playlist = require('../objects/Playlist')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class PlaylistController {
constructor() {}
/**
* POST: /api/playlists
* Create playlist
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async create(req, res) {
const oldPlaylist = new Playlist()
req.body.userId = req.user.id
req.body.userId = req.userNew.id
const success = oldPlaylist.setData(req.body)
if (!success) {
return res.status(400).send('Invalid playlist request data')
@ -58,13 +68,14 @@ class PlaylistController {
/**
* GET: /api/playlists
* Get all playlists for user
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findAllForUser(req, res) {
const playlistsForUser = await Database.playlistModel.findAll({
where: {
userId: req.user.id
userId: req.userNew.id
}
})
const playlists = []
@ -79,8 +90,9 @@ class PlaylistController {
/**
* GET: /api/playlists/:id
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findOne(req, res) {
const jsonExpanded = await req.playlist.getOldJsonExpanded()
@ -90,8 +102,9 @@ class PlaylistController {
/**
* PATCH: /api/playlists/:id
* Update playlist
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async update(req, res) {
const updatedPlaylist = req.playlist.set(req.body)
@ -156,8 +169,9 @@ class PlaylistController {
/**
* DELETE: /api/playlists/:id
* Remove playlist
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async delete(req, res) {
const jsonExpanded = await req.playlist.getOldJsonExpanded()
@ -169,8 +183,9 @@ class PlaylistController {
/**
* POST: /api/playlists/:id/item
* Add item to playlist
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async addItem(req, res) {
const oldPlaylist = await Database.playlistModel.getById(req.playlist.id)
@ -213,8 +228,9 @@ class PlaylistController {
/**
* DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId?
* Remove item from playlist
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async removeItem(req, res) {
const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId)
@ -266,8 +282,9 @@ class PlaylistController {
/**
* POST: /api/playlists/:id/batch/add
* Batch add playlist items
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async addBatch(req, res) {
if (!req.body.items?.length) {
@ -330,8 +347,9 @@ class PlaylistController {
/**
* POST: /api/playlists/:id/batch/remove
* Batch remove playlist items
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async removeBatch(req, res) {
if (!req.body.items?.length) {
@ -387,8 +405,9 @@ class PlaylistController {
/**
* POST: /api/playlists/collection/:collectionId
* Create a playlist from a collection
* @param {*} req
* @param {*} res
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async createFromCollection(req, res) {
const collection = await Database.collectionModel.findByPk(req.params.collectionId)
@ -409,7 +428,7 @@ class PlaylistController {
const oldPlaylist = new Playlist()
oldPlaylist.setData({
userId: req.user.id,
userId: req.userNew.id,
libraryId: collection.libraryId,
name: collection.name,
description: collection.description || null
@ -436,14 +455,20 @@ class PlaylistController {
res.json(jsonExpanded)
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
if (req.params.id) {
const playlist = await Database.playlistModel.findByPk(req.params.id)
if (!playlist) {
return res.status(404).send('Playlist not found')
}
if (playlist.userId !== req.user.id) {
Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`)
if (playlist.userId !== req.userNew.id) {
Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.userNew.id} that is not the owner`)
return res.sendStatus(403)
}
req.playlist = playlist

View File

@ -1,3 +1,4 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
@ -13,6 +14,14 @@ const CoverManager = require('../managers/CoverManager')
const LibraryItem = require('../objects/LibraryItem')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class PodcastController {
/**
* POST /api/podcasts
@ -20,12 +29,12 @@ class PodcastController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async create(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to create podcast`)
return res.sendStatus(403)
}
const payload = req.body
@ -121,12 +130,12 @@ class PodcastController {
* @typedef getPodcastFeedReqBody
* @property {string} rssFeed
*
* @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req
* @param {import('express').Response} res
* @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObjects} req
* @param {Response} res
*/
async getPodcastFeed(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get podcast feed`)
return res.sendStatus(403)
}
@ -147,12 +156,12 @@ class PodcastController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async getFeedsFromOPMLText(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get feeds from opml`)
return res.sendStatus(403)
}
@ -170,12 +179,12 @@ class PodcastController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async bulkCreatePodcastsFromOpmlFeedUrls(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to bulk create podcasts`)
return res.sendStatus(403)
}
@ -200,9 +209,17 @@ class PodcastController {
res.sendStatus(200)
}
/**
* GET: /api/podcasts/:id/checknew
*
* @this import('../routers/ApiRouter')
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async checkNewEpisodes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to check/download episodes`)
return res.sendStatus(403)
}
@ -220,15 +237,31 @@ class PodcastController {
})
}
/**
* GET: /api/podcasts/:id/clear-queue
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
clearEpisodeDownloadQueue(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempting to clear download queue`)
return res.sendStatus(403)
}
this.podcastManager.clearDownloadQueue(req.params.id)
res.sendStatus(200)
}
/**
* GET: /api/podcasts/:id/downloads
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
getEpisodeDownloads(req, res) {
var libraryItem = req.libraryItem
@ -255,9 +288,17 @@ class PodcastController {
})
}
/**
* POST: /api/podcasts/:id/download-episodes
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async downloadEpisodes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`)
return res.sendStatus(403)
}
const libraryItem = req.libraryItem
@ -270,10 +311,17 @@ class PodcastController {
res.sendStatus(200)
}
// POST: api/podcasts/:id/match-episodes
/**
* POST: /api/podcasts/:id/match-episodes
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async quickMatchEpisodes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`)
return res.sendStatus(403)
}
@ -289,6 +337,12 @@ class PodcastController {
})
}
/**
* PATCH: /api/podcasts/:id/episode/:episodeId
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateEpisode(req, res) {
const libraryItem = req.libraryItem
@ -305,7 +359,12 @@ class PodcastController {
res.json(libraryItem.toJSONExpanded())
}
// GET: api/podcasts/:id/episode/:episodeId
/**
* GET: /api/podcasts/:id/episode/:episodeId
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getEpisode(req, res) {
const episodeId = req.params.episodeId
const libraryItem = req.libraryItem
@ -319,7 +378,12 @@ class PodcastController {
res.json(episode)
}
// DELETE: api/podcasts/:id/episode/:episodeId
/**
* DELETE: /api/podcasts/:id/episode/:episodeId
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async removeEpisode(req, res) {
const episodeId = req.params.episodeId
const libraryItem = req.libraryItem
@ -390,6 +454,12 @@ class PodcastController {
res.json(libraryItem.toJSON())
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
const item = await Database.libraryItemModel.getOldById(req.params.id)
if (!item?.media) return res.sendStatus(404)
@ -399,15 +469,15 @@ class PodcastController {
}
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItem(item)) {
if (!req.userNew.checkCanAccessLibraryItem(item)) {
return res.sendStatus(403)
}
if (req.method == 'DELETE' && !req.user.canDelete) {
Logger.warn(`[PodcastController] User attempted to delete without permission`, req.user.username)
if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
Logger.warn('[PodcastController] User attempted to update without permission', req.user.username)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -1,19 +1,43 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const Database = require('../Database')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
class RSSFeedController {
constructor() { }
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class RSSFeedController {
constructor() {}
/**
* GET: /api/feeds
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getAll(req, res) {
const feeds = await this.rssFeedManager.getFeeds()
res.json({
feeds: feeds.map(f => f.toJSON()),
minified: feeds.map(f => f.toJSONMinified())
feeds: feeds.map((f) => f.toJSON()),
minified: feeds.map((f) => f.toJSONMinified())
})
}
// POST: api/feeds/item/:itemId/open
/**
* POST: /api/feeds/item/:itemId/open
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async openRSSFeedForItem(req, res) {
const options = req.body || {}
@ -21,8 +45,8 @@ class RSSFeedController {
if (!item) return res.sendStatus(404)
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItem(item)) {
Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`)
if (!req.userNew.checkCanAccessLibraryItem(item)) {
Logger.error(`[RSSFeedController] User "${req.userNew.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`)
return res.sendStatus(403)
}
@ -44,13 +68,20 @@ class RSSFeedController {
return res.status(400).send('Slug already in use')
}
const feed = await this.rssFeedManager.openFeedForItem(req.user, item, req.body)
const feed = await this.rssFeedManager.openFeedForItem(req.userNew.id, item, req.body)
res.json({
feed: feed.toJSONMinified()
})
}
// POST: api/feeds/collection/:collectionId/open
/**
* POST: /api/feeds/collection/:collectionId/open
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async openRSSFeedForCollection(req, res) {
const options = req.body || {}
@ -70,7 +101,7 @@ class RSSFeedController {
}
const collectionExpanded = await collection.getOldJsonExpanded()
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
const collectionItemsWithTracks = collectionExpanded.books.filter((li) => li.media.tracks.length)
// Check collection has audio tracks
if (!collectionItemsWithTracks.length) {
@ -78,13 +109,20 @@ class RSSFeedController {
return res.status(400).send('Collection has no audio tracks')
}
const feed = await this.rssFeedManager.openFeedForCollection(req.user, collectionExpanded, req.body)
const feed = await this.rssFeedManager.openFeedForCollection(req.userNew.id, collectionExpanded, req.body)
res.json({
feed: feed.toJSONMinified()
})
}
// POST: api/feeds/series/:seriesId/open
/**
* POST: /api/feeds/series/:seriesId/open
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async openRSSFeedForSeries(req, res) {
const options = req.body || {}
@ -106,7 +144,7 @@ class RSSFeedController {
const seriesJson = series.toJSON()
// Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
// Check series has audio tracks
if (!seriesJson.books.length) {
@ -114,20 +152,34 @@ class RSSFeedController {
return res.status(400).send('Series has no audio tracks')
}
const feed = await this.rssFeedManager.openFeedForSeries(req.user, seriesJson, req.body)
const feed = await this.rssFeedManager.openFeedForSeries(req.userNew.id, seriesJson, req.body)
res.json({
feed: feed.toJSONMinified()
})
}
// POST: api/feeds/:id/close
/**
* POST: /api/feeds/:id/close
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
closeRSSFeed(req, res) {
this.rssFeedManager.closeRssFeed(req, res)
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
middleware(req, res, next) {
if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds
Logger.error(`[RSSFeedController] Non-admin user attempted to make a request to an RSS feed route`, req.user.username)
if (!req.userNew.isAdminOrUp) {
// Only admins can manage rss feeds
Logger.error(`[RSSFeedController] Non-admin user "${req.userNew.username}" attempted to make a request to an RSS feed route`)
return res.sendStatus(403)
}

View File

@ -1,3 +1,4 @@
const { Request, Response } = require('express')
const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
@ -6,25 +7,51 @@ const MusicFinder = require('../finders/MusicFinder')
const Database = require('../Database')
const { isValidASIN } = require('../utils')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class SearchController {
constructor() {}
/**
* GET: /api/search/books
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findBooks(req, res) {
const id = req.query.id
const libraryItem = await Database.libraryItemModel.getOldById(id)
const provider = req.query.provider || 'google'
const title = req.query.title || ''
const author = req.query.author || ''
if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') {
Logger.error(`[SearchController] findBooks: Invalid request query params`)
return res.status(400).send('Invalid request query params')
}
const results = await BookFinder.search(libraryItem, provider, title, author)
res.json(results)
}
/**
* GET: /api/search/covers
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findCovers(req, res) {
const query = req.query
const podcast = query.podcast == 1
if (!query.title) {
Logger.error(`[SearchController] findCovers: No title sent in query`)
if (!query.title || typeof query.title !== 'string') {
Logger.error(`[SearchController] findCovers: Invalid title sent in query`)
return res.sendStatus(400)
}
@ -37,10 +64,11 @@ class SearchController {
}
/**
* GET: /api/search/podcasts
* Find podcast RSS feeds given a term
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async findPodcasts(req, res) {
const term = req.query.term
@ -56,12 +84,29 @@ class SearchController {
res.json(results)
}
/**
* GET: /api/search/authors
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findAuthor(req, res) {
const query = req.query.q
if (!query || typeof query !== 'string') {
Logger.error(`[SearchController] findAuthor: Invalid query param`)
return res.status(400).send('Invalid query param')
}
const author = await AuthorFinder.findAuthorByName(query)
res.json(author)
}
/**
* GET: /api/search/chapters
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findChapters(req, res) {
const asin = req.query.asin
if (!isValidASIN(asin.toUpperCase())) {
@ -74,12 +119,5 @@ class SearchController {
}
res.json(chapterData)
}
async findMusicTrack(req, res) {
const tracks = await MusicFinder.searchTrack(req.query || {})
res.json({
tracks
})
}
}
module.exports = new SearchController()

View File

@ -1,8 +1,17 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class SeriesController {
constructor() {}
@ -13,8 +22,8 @@ class SeriesController {
* 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 {*} req
* @param {*} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async findOne(req, res) {
const include = (req.query.include || '')
@ -28,8 +37,7 @@ class SeriesController {
if (include.includes('progress')) {
const libraryItemsInSeries = req.libraryItemsInSeries
const libraryItemsFinished = libraryItemsInSeries.filter((li) => {
const mediaProgress = req.user.getMediaProgress(li.id)
return mediaProgress?.isFinished
return req.userNew.getMediaProgress(li.media.id)?.isFinished
})
seriesJson.progress = {
libraryItemIds: libraryItemsInSeries.map((li) => li.id),
@ -46,6 +54,11 @@ class SeriesController {
res.json(seriesJson)
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async update(req, res) {
const hasUpdated = req.series.update(req.body)
if (hasUpdated) {
@ -55,6 +68,12 @@ class SeriesController {
res.json(req.series.toJSON())
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
const series = await Database.seriesModel.getOldById(req.params.id)
if (!series) return res.sendStatus(404)
@ -64,15 +83,15 @@ class SeriesController {
*/
const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.userNew)
if (!libraryItems.length) {
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user)
Logger.warn(`[SeriesController] User "${req.userNew.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 attempted to delete without permission`, req.user)
if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[SeriesController] User "${req.userNew.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 attempted to update without permission', req.user)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -1,26 +1,32 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const Database = require('../Database')
const { toNumber, isUUID } = require('../utils/index')
const ShareManager = require('../managers/ShareManager')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class SessionController {
constructor() {}
async findOne(req, res) {
return res.json(req.playbackSession)
}
/**
* GET: /api/sessions
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async getAllWithUserData(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[SessionController] getAllWithUserData: Non-admin user "${req.userNew.username}" requested all session data`)
return res.sendStatus(404)
}
// Validate "user" query
@ -105,9 +111,17 @@ class SessionController {
res.json(payload)
}
/**
* GET: /api/sessions/open
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getOpenSessions(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[SessionController] getOpenSessions: Non-admin user "${req.userNew.username}" requested open session data`)
return res.sendStatus(404)
}
@ -127,25 +141,54 @@ class SessionController {
})
}
/**
* GET: /api/session/:id
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getOpenSession(req, res) {
const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId)
const sessionForClient = req.playbackSession.toJSONForClient(libraryItem)
res.json(sessionForClient)
}
// POST: api/session/:id/sync
/**
* POST: /api/session/:id/sync
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
sync(req, res) {
this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res)
this.playbackSessionManager.syncSessionRequest(req.userNew, req.playbackSession, req.body, res)
}
// POST: api/session/:id/close
/**
* POST: /api/session/:id/close
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
close(req, res) {
let syncData = req.body
if (syncData && !Object.keys(syncData).length) syncData = null
this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res)
this.playbackSessionManager.closeSessionRequest(req.userNew, req.playbackSession, syncData, res)
}
// DELETE: api/session/:id
/**
* DELETE: /api/session/:id
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async delete(req, res) {
// if session is open then remove it
const openSession = this.playbackSessionManager.getSession(req.playbackSession.id)
@ -164,12 +207,12 @@ class SessionController {
* @typedef batchDeleteReqBody
* @property {string[]} sessions
*
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req
* @param {import('express').Response} res
* @param {Request<{}, {}, batchDeleteReqBody, {}> & RequestUserObjects} req
* @param {Response} res
*/
async batchDelete(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[SessionController] Non-admin user "${req.userNew.username}" attempted to batch delete sessions`)
return res.sendStatus(403)
}
// Validate session ids
@ -192,7 +235,7 @@ class SessionController {
id: req.body.sessions
}
})
Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`)
Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.userNew.username}"`)
res.sendStatus(200)
} catch (error) {
Logger.error(`[SessionController] Failed to remove playback sessions`, error)
@ -200,22 +243,42 @@ class SessionController {
}
}
// POST: api/session/local
/**
* POST: /api/session/local
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
syncLocal(req, res) {
this.playbackSessionManager.syncLocalSessionRequest(req, res)
}
// POST: api/session/local-all
/**
* POST: /api/session/local-all
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
syncLocalSessions(req, res) {
this.playbackSessionManager.syncLocalSessionsRequest(req, res)
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
openSessionMiddleware(req, res, next) {
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) return res.sendStatus(404)
if (playbackSession.userId !== req.user.id) {
Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`)
if (playbackSession.userId !== req.userNew.id) {
Logger.error(`[SessionController] User "${req.userNew.username}" attempting to access session belonging to another user "${req.params.id}"`)
return res.sendStatus(404)
}
@ -223,6 +286,12 @@ class SessionController {
next()
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
const playbackSession = await Database.getPlaybackSession(req.params.id)
if (!playbackSession) {
@ -230,11 +299,11 @@ class SessionController {
return res.sendStatus(404)
}
if (req.method == 'DELETE' && !req.user.canDelete) {
Logger.warn(`[SessionController] User attempted to delete without permission`, req.user)
if (req.method == 'DELETE' && !req.userNew.canDelete) {
Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
Logger.warn('[SessionController] User attempted to update without permission', req.user.username)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) {
Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to update without permission`)
return res.sendStatus(403)
}

View File

@ -1,3 +1,4 @@
const { Request, Response } = require('express')
const uuid = require('uuid')
const Path = require('path')
const { Op } = require('sequelize')
@ -10,6 +11,14 @@ const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUti
const PlaybackSession = require('../objects/PlaybackSession')
const ShareManager = require('../managers/ShareManager')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class ShareController {
constructor() {}
@ -20,8 +29,8 @@ class ShareController {
*
* @this {import('../routers/PublicRouter')}
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {Request} req
* @param {Response} res
*/
async getMediaItemShareBySlug(req, res) {
const { slug } = req.params
@ -122,8 +131,8 @@ class ShareController {
* GET: /api/share/:slug/cover
* Get media item share cover image
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {Request} req
* @param {Response} res
*/
async getMediaItemShareCoverImage(req, res) {
if (!req.cookies.share_session_id) {
@ -162,8 +171,8 @@ class ShareController {
* GET: /api/share/:slug/track/:index
* Get media item share audio track
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {Request} req
* @param {Response} res
*/
async getMediaItemShareAudioTrack(req, res) {
if (!req.cookies.share_session_id) {
@ -208,8 +217,8 @@ class ShareController {
* PATCH: /api/share/:slug/progress
* Update media item share progress
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {Request} req
* @param {Response} res
*/
async updateMediaItemShareProgress(req, res) {
if (!req.cookies.share_session_id) {
@ -242,12 +251,12 @@ class ShareController {
* POST: /api/share/mediaitem
* Create a new media item share
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async createMediaItemShare(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to create item share`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to create item share`)
return res.sendStatus(403)
}
@ -290,7 +299,7 @@ class ShareController {
expiresAt: expiresAt || null,
mediaItemId,
mediaItemType,
userId: req.user.id
userId: req.userNew.id
})
ShareManager.openMediaItemShare(mediaItemShare)
@ -306,12 +315,12 @@ class ShareController {
* DELETE: /api/share/mediaitem/:id
* Delete media item share
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async deleteMediaItemShare(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to delete item share`)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to delete item share`)
return res.sendStatus(403)
}

View File

@ -1,6 +1,15 @@
const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger')
const Database = require('../Database')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*/
class ToolsController {
constructor() {}
@ -10,8 +19,8 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async encodeM4b(req, res) {
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
@ -30,7 +39,7 @@ class ToolsController {
}
const options = req.query || {}
this.abMergeManager.startAudiobookMerge(req.user, req.libraryItem, options)
this.abMergeManager.startAudiobookMerge(req.userNew.id, req.libraryItem, options)
res.sendStatus(200)
}
@ -41,8 +50,8 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async cancelM4bEncode(req, res) {
const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id)
@ -59,8 +68,8 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async embedAudioFileMetadata(req, res) {
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
@ -77,7 +86,7 @@ class ToolsController {
forceEmbedChapters: req.query.forceEmbedChapters === '1',
backup: req.query.backup === '1'
}
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, options)
this.audioMetadataManager.updateMetadataForItem(req.userNew.id, req.libraryItem, options)
res.sendStatus(200)
}
@ -87,8 +96,8 @@ class ToolsController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async batchEmbedMetadata(req, res) {
const libraryItemIds = req.body.libraryItemIds || []
@ -105,8 +114,8 @@ class ToolsController {
}
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user`, req.user)
if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) {
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user "${req.userNew.username}"`)
return res.sendStatus(403)
}
@ -127,19 +136,19 @@ class ToolsController {
forceEmbedChapters: req.query.forceEmbedChapters === '1',
backup: req.query.backup === '1'
}
this.audioMetadataManager.handleBatchEmbed(req.user, libraryItems, options)
this.audioMetadataManager.handleBatchEmbed(req.userNew.id, libraryItems, options)
res.sendStatus(200)
}
/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user "${req.userNew.username}" attempted to access tools route`)
return res.sendStatus(403)
}
@ -148,7 +157,7 @@ class ToolsController {
if (!item?.media) return res.sendStatus(404)
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItem(item)) {
if (!req.userNew.checkCanAccessLibraryItem(item)) {
return res.sendStatus(403)
}

View File

@ -1,3 +1,4 @@
const { Request, Response, NextFunction } = require('express')
const uuidv4 = require('uuid').v4
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
@ -8,12 +9,18 @@ const User = require('../objects/user/User')
const { toNumber } = require('../utils/index')
/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*
* @typedef UserControllerRequestProps
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user - User that made the request
* @property {import('../objects/user/User')} [reqUser] - User for req param id
*
* @typedef {import('express').Request & UserControllerRequestProps} UserControllerRequest
* @typedef {import('express').Response} UserControllerResponse
* @typedef {Request & UserControllerRequestProps} UserControllerRequest
*/
class UserController {
@ -22,11 +29,11 @@ class UserController {
/**
*
* @param {UserControllerRequest} req
* @param {UserControllerResponse} res
* @param {Response} res
*/
async findAll(req, res) {
if (!req.user.isAdminOrUp) return res.sendStatus(403)
const hideRootToken = !req.user.isRoot
if (!req.userNew.isAdminOrUp) return res.sendStatus(403)
const hideRootToken = !req.userNew.isRoot
const includes = (req.query.include || '').split(',').map((i) => i.trim())
@ -52,11 +59,11 @@ class UserController {
* Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt`
*
* @param {UserControllerRequest} req
* @param {UserControllerResponse} res
* @param {Response} res
*/
async findOne(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user)
if (!req.userNew.isAdminOrUp) {
Logger.error(`Non-admin user "${req.userNew.username}" attempted to get user`)
return res.sendStatus(403)
}
@ -95,13 +102,22 @@ class UserController {
return oldMediaProgress
})
const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot)
const userJson = req.reqUser.toJSONForBrowser(!req.userNew.isRoot)
userJson.mediaProgress = oldMediaProgresses
res.json(userJson)
}
/**
* POST: /api/users
* Create a new user
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async create(req, res) {
const account = req.body
const username = account.username
@ -134,13 +150,13 @@ class UserController {
* Update user
*
* @param {UserControllerRequest} req
* @param {UserControllerResponse} res
* @param {Response} res
*/
async update(req, res) {
const user = req.reqUser
if (user.type === 'root' && !req.user.isRoot) {
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
if (user.type === 'root' && !req.userNew.isRoot) {
Logger.error(`[UserController] Admin user "${req.userNew.username}" attempted to update root user`)
return res.sendStatus(403)
}
@ -168,7 +184,7 @@ class UserController {
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
}
await Database.updateUser(user)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', user.toJSONForBrowser())
}
res.json({
@ -177,14 +193,21 @@ class UserController {
})
}
/**
* DELETE: /api/users/:id
* Delete a user
*
* @param {UserControllerRequest} req
* @param {Response} res
*/
async delete(req, res) {
if (req.params.id === 'root') {
Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted')
return res.sendStatus(500)
return res.sendStatus(400)
}
if (req.user.id === req.params.id) {
Logger.error(`[UserController] ${req.user.username} is attempting to delete themselves... why? WHY?`)
return res.sendStatus(500)
if (req.userNew.id === req.params.id) {
Logger.error(`[UserController] User ${req.userNew.username} is attempting to delete self`)
return res.sendStatus(400)
}
const user = req.reqUser
@ -212,20 +235,25 @@ class UserController {
* PATCH: /api/users/:id/openid-unlink
*
* @param {UserControllerRequest} req
* @param {UserControllerResponse} res
* @param {Response} res
*/
async unlinkFromOpenID(req, res) {
Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`)
req.reqUser.authOpenIDSub = null
if (await Database.userModel.updateFromOld(req.reqUser)) {
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser())
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.reqUser.toJSONForBrowser())
res.sendStatus(200)
} else {
res.sendStatus(500)
}
}
// GET: api/users/:id/listening-sessions
/**
* GET: /api/users/:id/listening-sessions
*
* @param {UserControllerRequest} req
* @param {Response} res
*/
async getListeningSessions(req, res) {
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
@ -246,15 +274,29 @@ class UserController {
res.json(payload)
}
// GET: api/users/:id/listening-stats
/**
* GET: /api/users/:id/listening-stats
*
* @this {import('../routers/ApiRouter')}
*
* @param {UserControllerRequest} req
* @param {Response} res
*/
async getListeningStats(req, res) {
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
res.json(listeningStats)
}
// POST: api/users/online (admin)
/**
* GET: /api/users/online
*
* @this {import('../routers/ApiRouter')}
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getOnlineUsers(req, res) {
if (!req.user.isAdminOrUp) {
if (!req.userNew.isAdminOrUp) {
return res.sendStatus(403)
}
@ -264,10 +306,16 @@ class UserController {
})
}
/**
*
* @param {RequestWithUser} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
if (!req.userNew.isAdminOrUp && req.userNew.id !== req.params.id) {
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.userNew.isAdminOrUp) {
return res.sendStatus(403)
}

View File

@ -46,11 +46,11 @@ class AbMergeManager {
/**
*
* @param {import('../objects/user/User')} user
* @param {string} userId
* @param {import('../objects/LibraryItem')} libraryItem
* @param {AbMergeEncodeOptions} [options={}]
*/
async startAudiobookMerge(user, libraryItem, options = {}) {
async startAudiobookMerge(userId, libraryItem, options = {}) {
const task = new Task()
const audiobookDirname = Path.basename(libraryItem.path)
@ -61,7 +61,7 @@ class AbMergeManager {
const taskData = {
libraryItemId: libraryItem.id,
libraryItemPath: libraryItem.path,
userId: user.id,
userId,
originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path),
inos: libraryItem.media.includedAudioFiles.map((f) => f.ino),
tempFilepath,

View File

@ -42,7 +42,7 @@ class ApiCacheManager {
Logger.debug(`[ApiCacheManager] Skipping cache for random sort`)
return next()
}
const key = { user: req.user.username, url: req.url }
const key = { user: req.userNew.username, url: req.url }
const stringifiedKey = JSON.stringify(key)
Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`)
const cached = this.cache.get(stringifiedKey)

View File

@ -32,13 +32,25 @@ class AudioMetadataMangaer {
return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length)
}
handleBatchEmbed(user, libraryItems, options = {}) {
/**
*
* @param {string} userId
* @param {*} libraryItems
* @param {*} options
*/
handleBatchEmbed(userId, libraryItems, options = {}) {
libraryItems.forEach((li) => {
this.updateMetadataForItem(user, li, options)
this.updateMetadataForItem(userId, li, options)
})
}
async updateMetadataForItem(user, libraryItem, options = {}) {
/**
*
* @param {string} userId
* @param {*} libraryItem
* @param {*} options
*/
async updateMetadataForItem(userId, libraryItem, options = {}) {
const forceEmbedChapters = !!options.forceEmbedChapters
const backupFiles = !!options.backup
@ -58,7 +70,7 @@ class AudioMetadataMangaer {
const taskData = {
libraryItemId: libraryItem.id,
libraryItemPath: libraryItem.path,
userId: user.id,
userId,
audioFiles: audioFiles.map((af) => ({
index: af.index,
ino: af.ino,

View File

@ -39,7 +39,7 @@ class PlaybackSessionManager {
/**
*
* @param {import('express').Request} req
* @param {import('../controllers/SessionController').RequestWithUser} req
* @param {Object} [clientDeviceInfo]
* @returns {Promise<DeviceInfo>}
*/
@ -48,7 +48,7 @@ class PlaybackSessionManager {
const ip = requestIp.getClientIp(req)
const deviceInfo = new DeviceInfo()
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id)
deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.userNew?.id)
if (clientDeviceInfo?.deviceId) {
const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId)
@ -67,18 +67,25 @@ class PlaybackSessionManager {
/**
*
* @param {import('express').Request} req
* @param {import('../controllers/SessionController').RequestWithUser} req
* @param {import('express').Response} res
* @param {string} [episodeId]
*/
async startSessionRequest(req, res, episodeId) {
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`)
const { user, libraryItem, body: options } = req
const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options)
const { libraryItem, body: options } = req
const session = await this.startSession(req.userNew, deviceInfo, libraryItem, episodeId, options)
res.json(session.toJSONForClient(libraryItem))
}
/**
*
* @param {import('../models/User')} user
* @param {*} session
* @param {*} payload
* @param {import('express').Response} res
*/
async syncSessionRequest(user, session, payload, res) {
if (await this.syncSession(user, session, payload)) {
res.sendStatus(200)
@ -89,7 +96,7 @@ class PlaybackSessionManager {
async syncLocalSessionsRequest(req, res) {
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
const user = req.user
const user = req.userNew
const sessions = req.body.sessions || []
const syncResults = []
@ -104,6 +111,13 @@ class PlaybackSessionManager {
})
}
/**
*
* @param {import('../models/User')} user
* @param {*} sessionJson
* @param {*} deviceInfo
* @returns
*/
async syncLocalSession(user, sessionJson, deviceInfo) {
const libraryItem = await Database.libraryItemModel.getOldById(sessionJson.libraryItemId)
const episode = sessionJson.episodeId && libraryItem && libraryItem.isPodcast ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
@ -174,41 +188,58 @@ class PlaybackSessionManager {
progressSynced: false
}
const userProgressForItem = user.getMediaProgress(session.libraryItemId, session.episodeId)
const mediaItemId = session.episodeId || libraryItem.media.id
let userProgressForItem = user.getMediaProgress(mediaItemId)
if (userProgressForItem) {
if (userProgressForItem.lastUpdate > session.updatedAt) {
if (userProgressForItem.updatedAt.valueOf() > session.updatedAt) {
Logger.debug(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently`)
} else {
Logger.debug(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`)
result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId)
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
...session.mediaProgressObject
})
result.progressSynced = !!updateResponse.mediaProgress
if (result.progressSynced) {
userProgressForItem = updateResponse.mediaProgress
}
}
} else {
Logger.debug(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`)
result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId)
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
...session.mediaProgressObject
})
result.progressSynced = !!updateResponse.mediaProgress
if (result.progressSynced) {
userProgressForItem = updateResponse.mediaProgress
}
}
// Update user and emit socket event
if (result.progressSynced) {
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
if (itemProgress) {
await Database.upsertMediaProgress(itemProgress)
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
sessionId: session.id,
deviceDescription: session.deviceDescription,
data: itemProgress.toJSON()
})
}
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
id: userProgressForItem.id,
sessionId: session.id,
deviceDescription: session.deviceDescription,
data: userProgressForItem.getOldMediaProgress()
})
}
return result
}
/**
*
* @param {import('../controllers/SessionController').RequestWithUser} req
* @param {*} res
*/
async syncLocalSessionRequest(req, res) {
const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo)
const user = req.user
const sessionJson = req.body
const result = await this.syncLocalSession(user, sessionJson, deviceInfo)
const result = await this.syncLocalSession(req.userNew, sessionJson, deviceInfo)
if (result.error) {
res.status(500).send(result.error)
} else {
@ -216,6 +247,13 @@ class PlaybackSessionManager {
}
}
/**
*
* @param {import('../models/User')} user
* @param {*} session
* @param {*} syncData
* @param {import('express').Response} res
*/
async closeSessionRequest(user, session, syncData, res) {
await this.closeSession(user, session, syncData)
res.sendStatus(200)
@ -223,7 +261,7 @@ class PlaybackSessionManager {
/**
*
* @param {import('../objects/user/User')} user
* @param {import('../models/User')} user
* @param {DeviceInfo} deviceInfo
* @param {import('../objects/LibraryItem')} libraryItem
* @param {string|null} episodeId
@ -241,7 +279,8 @@ class PlaybackSessionManager {
const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
const mediaPlayer = options.mediaPlayer || 'unknown'
const userProgress = libraryItem.isMusic ? null : user.getMediaProgress(libraryItem.id, episodeId)
const mediaItemId = episodeId || libraryItem.media.id
const userProgress = user.getMediaProgress(mediaItemId)
let userStartTime = 0
if (userProgress) {
if (userProgress.isFinished) {
@ -292,6 +331,13 @@ class PlaybackSessionManager {
return newPlaybackSession
}
/**
*
* @param {import('../models/User')} user
* @param {*} session
* @param {*} syncData
* @returns
*/
async syncSession(user, session, syncData) {
const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId)
if (!libraryItem) {
@ -303,20 +349,19 @@ class PlaybackSessionManager {
session.addListeningTime(syncData.timeListened)
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" (Device: ${session.deviceDescription}) | Total Time Listened: ${session.timeListening}`)
const itemProgressUpdate = {
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
duration: syncData.duration,
currentTime: syncData.currentTime,
progress: session.progress
}
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
if (wasUpdated) {
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
if (itemProgress) await Database.upsertMediaProgress(itemProgress)
})
if (updateResponse.mediaProgress) {
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
id: updateResponse.mediaProgress.id,
sessionId: session.id,
deviceDescription: session.deviceDescription,
data: itemProgress.toJSON()
data: updateResponse.mediaProgress.getOldMediaProgress()
})
}
this.saveSession(session)
@ -325,6 +370,13 @@ class PlaybackSessionManager {
}
}
/**
*
* @param {import('../models/User')} user
* @param {*} session
* @param {*} syncData
* @returns
*/
async closeSession(user, session, syncData = null) {
if (syncData) {
await this.syncSession(user, session, syncData)

View File

@ -9,7 +9,7 @@ const Feed = require('../objects/Feed')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
class RssFeedManager {
constructor() { }
constructor() {}
async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
@ -44,7 +44,7 @@ class RssFeedManager {
const feeds = await Database.feedModel.getOldFeeds()
for (const feed of feeds) {
// Remove invalid feeds
if (!await this.validateFeedEntity(feed)) {
if (!(await this.validateFeedEntity(feed))) {
await Database.removeFeed(feed.id)
}
}
@ -138,7 +138,7 @@ class RssFeedManager {
const seriesJson = series.toJSON()
// Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
// Find most recently updated item in series
let mostRecentlyUpdatedAt = seriesJson.updatedAt
@ -202,7 +202,14 @@ class RssFeedManager {
readStream.pipe(res)
}
async openFeedForItem(user, libraryItem, options) {
/**
*
* @param {string} userId
* @param {*} libraryItem
* @param {*} options
* @returns
*/
async openFeedForItem(userId, libraryItem, options) {
const serverAddress = options.serverAddress
const slug = options.slug
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
@ -210,7 +217,7 @@ class RssFeedManager {
const ownerEmail = options.metadataDetails?.ownerEmail
const feed = new Feed()
feed.setFromItem(user.id, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail)
feed.setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail)
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
await Database.createFeed(feed)
@ -218,7 +225,14 @@ class RssFeedManager {
return feed
}
async openFeedForCollection(user, collectionExpanded, options) {
/**
*
* @param {string} userId
* @param {*} collectionExpanded
* @param {*} options
* @returns
*/
async openFeedForCollection(userId, collectionExpanded, options) {
const serverAddress = options.serverAddress
const slug = options.slug
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
@ -226,7 +240,7 @@ class RssFeedManager {
const ownerEmail = options.metadataDetails?.ownerEmail
const feed = new Feed()
feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
await Database.createFeed(feed)
@ -234,7 +248,14 @@ class RssFeedManager {
return feed
}
async openFeedForSeries(user, seriesExpanded, options) {
/**
*
* @param {string} userId
* @param {*} seriesExpanded
* @param {*} options
* @returns
*/
async openFeedForSeries(userId, seriesExpanded, options) {
const serverAddress = options.serverAddress
const slug = options.slug
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
@ -242,7 +263,7 @@ class RssFeedManager {
const ownerEmail = options.metadataDetails?.ownerEmail
const feed = new Feed()
feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
feed.setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
await Database.createFeed(feed)

View File

@ -419,8 +419,11 @@ class User extends Model {
)
}
get isRoot() {
return this.type === 'root'
}
get isAdminOrUp() {
return this.type === 'root' || this.type === 'admin'
return this.isRoot || this.type === 'admin'
}
get isUser() {
return this.type === 'user'

View File

@ -372,88 +372,5 @@ class User {
return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON
}
/**
* Get first available library id for user
*
* @param {string[]} libraryIds
* @returns {string|null}
*/
getDefaultLibraryId(libraryIds) {
// Libraries should already be in ascending display order, find first accessible
return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null
}
getMediaProgress(libraryItemId, episodeId = null) {
if (!this.mediaProgress) return null
return this.mediaProgress.find((lip) => {
if (episodeId && lip.episodeId !== episodeId) return false
return lip.libraryItemId === libraryItemId
})
}
getAllMediaProgressForLibraryItem(libraryItemId) {
if (!this.mediaProgress) return []
return this.mediaProgress.filter((li) => li.libraryItemId === libraryItemId)
}
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
const itemProgress = this.mediaProgress.find((li) => {
if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId === libraryItem.id
})
if (!itemProgress) {
const newItemProgress = new MediaProgress()
newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id)
this.mediaProgress.push(newItemProgress)
return true
}
const wasUpdated = itemProgress.update(updatePayload)
if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync
return wasUpdated
}
checkCanAccessLibrary(libraryId) {
if (this.permissions.accessAllLibraries) return true
if (!this.librariesAccessible) return false
return this.librariesAccessible.includes(libraryId)
}
checkCanAccessLibraryItemWithTags(tags) {
if (this.permissions.accessAllTags) return true
if (this.permissions.selectedTagsNotAccessible) {
if (!tags?.length) return true
return tags.every((tag) => !this.itemTagsSelected.includes(tag))
}
if (!tags?.length) return false
return this.itemTagsSelected.some((tag) => tags.includes(tag))
}
checkCanAccessLibraryItem(libraryItem) {
if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false
if (libraryItem.media.metadata.explicit && !this.canAccessExplicitContent) return false
return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags)
}
/**
* Number of podcast episodes not finished for library item
* Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance
* @param {LibraryItem|object} libraryItem
* @returns {number}
*/
getNumEpisodesIncompleteForPodcast(libraryItem) {
if (!libraryItem?.media.episodes) return 0
let numEpisodesIncomplete = 0
for (const episode of libraryItem.media.episodes) {
const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id)
if (!mediaProgress?.isFinished) {
numEpisodesIncomplete++
}
}
return numEpisodesIncomplete
}
}
module.exports = User

View File

@ -39,6 +39,7 @@ class ApiRouter {
constructor(Server) {
/** @type {import('../Auth')} */
this.auth = Server.auth
/** @type {import('../managers/PlaybackSessionManager')} */
this.playbackSessionManager = Server.playbackSessionManager
/** @type {import('../managers/AbMergeManager')} */
this.abMergeManager = Server.abMergeManager
@ -50,8 +51,10 @@ class ApiRouter {
this.podcastManager = Server.podcastManager
/** @type {import('../managers/AudioMetadataManager')} */
this.audioMetadataManager = Server.audioMetadataManager
/** @type {import('../managers/RssFeedManager')} */
this.rssFeedManager = Server.rssFeedManager
this.cronManager = Server.cronManager
/** @type {import('../managers/NotificationManager')} */
this.notificationManager = Server.notificationManager
this.emailManager = Server.emailManager
this.apiCacheManager = Server.apiCacheManager
@ -281,7 +284,6 @@ class ApiRouter {
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
this.router.get('/search/chapters', SearchController.findChapters.bind(this))
this.router.get('/search/tracks', SearchController.findMusicTrack.bind(this))
//
// Cache Routes (Admin and up)