mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 11:21:36 +00:00
Merge branch 'master' into mf/rssInboundManager
This commit is contained in:
commit
47854f2e28
49 changed files with 1930 additions and 120 deletions
|
|
@ -552,8 +552,8 @@ class LibraryController {
|
|||
* @param {import('express').Response} res
|
||||
*/
|
||||
async search(req, res) {
|
||||
if (!req.query.q) {
|
||||
return res.status(400).send('No query string')
|
||||
if (!req.query.q || typeof req.query.q !== 'string') {
|
||||
return res.status(400).send('Invalid request. Query param "q" must be a string')
|
||||
}
|
||||
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
const query = asciiOnlyToLowerCase(req.query.q.trim())
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const SocketAuthority = require('../SocketAuthority')
|
|||
const Database = require('../Database')
|
||||
const { sort } = require('../libs/fastSort')
|
||||
const { toNumber } = require('../utils/index')
|
||||
const userStats = require('../utils/queries/userStats')
|
||||
|
||||
class MeController {
|
||||
constructor() { }
|
||||
|
|
@ -333,5 +334,21 @@ class MeController {
|
|||
}
|
||||
res.json(req.user.toJSONForBrowser())
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/stats/year/:year
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async getStatsForYear(req, res) {
|
||||
const year = Number(req.params.year)
|
||||
if (isNaN(year) || year < 2000 || year > 9999) {
|
||||
Logger.error(`[MeController] Invalid year "${year}"`)
|
||||
return res.status(400).send('Invalid year')
|
||||
}
|
||||
const data = await userStats.getStatsForYear(req.user, year)
|
||||
res.json(data)
|
||||
}
|
||||
}
|
||||
module.exports = new MeController()
|
||||
|
|
@ -11,6 +11,7 @@ const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
|||
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 :(
|
||||
|
|
@ -696,5 +697,25 @@ class MiscController {
|
|||
serverSettings: Database.serverSettings.toJSONForBrowser()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/me/stats/year/:year
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').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`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const year = Number(req.params.year)
|
||||
if (isNaN(year) || year < 2000 || year > 9999) {
|
||||
Logger.error(`[MiscController] Invalid year "${year}"`)
|
||||
return res.status(400).send('Invalid year')
|
||||
}
|
||||
const stats = await adminStats.getStatsForYear(year)
|
||||
res.json(stats)
|
||||
}
|
||||
}
|
||||
module.exports = new MiscController()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const fs = require('../libs/fsExtra')
|
|||
|
||||
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
||||
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils')
|
||||
const { validateUrl } = require('../utils/index')
|
||||
|
||||
const Scanner = require('../scanner/Scanner')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
|
|
@ -16,7 +17,7 @@ class PodcastController {
|
|||
|
||||
async create(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to create podcast`, req.user)
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const payload = req.body
|
||||
|
|
@ -102,10 +103,24 @@ class PodcastController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/podcasts/feed
|
||||
*
|
||||
* @typedef getPodcastFeedReqBody
|
||||
* @property {string} rssFeed
|
||||
*
|
||||
* @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async getPodcastFeed(req, res) {
|
||||
var url = req.body.rssFeed
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const url = validateUrl(req.body.rssFeed)
|
||||
if (!url) {
|
||||
return res.status(400).send('Bad request')
|
||||
return res.status(400).send('Invalid request body. "rssFeed" must be a valid URL')
|
||||
}
|
||||
|
||||
const podcast = await getPodcastFeed(url)
|
||||
|
|
@ -154,6 +169,11 @@ class PodcastController {
|
|||
}
|
||||
|
||||
async getFeedsFromOPMLText(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (!req.body.opmlText) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,19 @@ class SearchController {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Find podcast RSS feeds given a term
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async findPodcasts(req, res) {
|
||||
const term = req.query.term
|
||||
if (!term) {
|
||||
Logger.error('[SearchController] Invalid request query param "term" is required')
|
||||
return res.status(400).send('Invalid request query param "term" is required')
|
||||
}
|
||||
|
||||
const results = await PodcastFinder.search(term)
|
||||
res.json(results)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const { toNumber } = require('../utils/index')
|
||||
const { toNumber, isUUID } = require('../utils/index')
|
||||
|
||||
class SessionController {
|
||||
constructor() { }
|
||||
|
|
@ -9,35 +9,97 @@ class SessionController {
|
|||
return res.json(req.playbackSession)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/sessions
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').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}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
let listeningSessions = []
|
||||
if (req.query.user) {
|
||||
listeningSessions = await this.getUserListeningSessionsHelper(req.query.user)
|
||||
} else {
|
||||
listeningSessions = await this.getAllSessionsWithUserData()
|
||||
// Validate "user" query
|
||||
let userId = req.query.user
|
||||
if (userId && !isUUID(userId)) {
|
||||
Logger.warn(`[SessionController] Invalid "user" query string "${userId}"`)
|
||||
userId = null
|
||||
}
|
||||
// Validate "sort" query
|
||||
const validSortOrders = ['displayTitle', 'duration', 'playMethod', 'startTime', 'currentTime', 'timeListening', 'updatedAt', 'createdAt']
|
||||
let orderKey = req.query.sort || 'updatedAt'
|
||||
if (!validSortOrders.includes(orderKey)) {
|
||||
Logger.warn(`[SessionController] Invalid "sort" query string "${orderKey}" (Must be one of "${validSortOrders.join('|')}")`)
|
||||
orderKey = 'updatedAt'
|
||||
}
|
||||
let orderDesc = req.query.desc === '1' ? 'DESC' : 'ASC'
|
||||
// Validate "itemsPerPage" and "page" query
|
||||
let itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
if (itemsPerPage < 1) {
|
||||
Logger.warn(`[SessionController] Invalid "itemsPerPage" query string "${itemsPerPage}"`)
|
||||
itemsPerPage = 10
|
||||
}
|
||||
let page = toNumber(req.query.page, 0)
|
||||
if (page < 0) {
|
||||
Logger.warn(`[SessionController] Invalid "page" query string "${page}"`)
|
||||
page = 0
|
||||
}
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
let where = null
|
||||
const include = [
|
||||
{
|
||||
model: Database.models.device
|
||||
}
|
||||
]
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
if (userId) {
|
||||
where = {
|
||||
userId
|
||||
}
|
||||
} else {
|
||||
include.push({
|
||||
model: Database.userModel,
|
||||
attributes: ['id', 'username']
|
||||
})
|
||||
}
|
||||
|
||||
const { rows, count } = await Database.playbackSessionModel.findAndCountAll({
|
||||
where,
|
||||
include,
|
||||
order: [
|
||||
[orderKey, orderDesc]
|
||||
],
|
||||
limit: itemsPerPage,
|
||||
offset: itemsPerPage * page
|
||||
})
|
||||
|
||||
// Map playback sessions to old playback sessions
|
||||
const sessions = rows.map(session => {
|
||||
const oldPlaybackSession = Database.playbackSessionModel.getOldPlaybackSession(session)
|
||||
if (session.user) {
|
||||
return {
|
||||
...oldPlaybackSession,
|
||||
user: {
|
||||
id: session.user.id,
|
||||
username: session.user.username
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return oldPlaybackSession.toJSON()
|
||||
}
|
||||
})
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
total: count,
|
||||
numPages: Math.ceil(count / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
if (req.query.user) {
|
||||
payload.userFilter = req.query.user
|
||||
if (userId) {
|
||||
payload.userId = userId
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
|
|
@ -92,6 +154,49 @@ class SessionController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/sessions/batch/delete
|
||||
* @this import('../routers/ApiRouter')
|
||||
*
|
||||
* @typedef batchDeleteReqBody
|
||||
* @property {string[]} sessions
|
||||
*
|
||||
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async batchDelete(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
// Validate session ids
|
||||
if (!req.body.sessions?.length || !Array.isArray(req.body.sessions) || req.body.sessions.some(s => !isUUID(s))) {
|
||||
Logger.error(`[SessionController] Invalid request body. "sessions" array is required`, req.body)
|
||||
return res.status(400).send('Invalid request body. "sessions" array of session id strings is required.')
|
||||
}
|
||||
|
||||
// Check if any of these sessions are open and close it
|
||||
for (const sessionId of req.body.sessions) {
|
||||
const openSession = this.playbackSessionManager.getSession(sessionId)
|
||||
if (openSession) {
|
||||
await this.playbackSessionManager.removeSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionsRemoved = await Database.playbackSessionModel.destroy({
|
||||
where: {
|
||||
id: req.body.sessions
|
||||
}
|
||||
})
|
||||
Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`)
|
||||
res.sendStatus(200)
|
||||
} catch (error) {
|
||||
Logger.error(`[SessionController] Failed to remove playback sessions`, error)
|
||||
res.status(500).send('Failed to remove sessions')
|
||||
}
|
||||
}
|
||||
|
||||
// POST: api/session/local
|
||||
syncLocal(req, res) {
|
||||
this.playbackSessionManager.syncLocalSessionRequest(req, res)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue