Merge branch 'master' into mf/rssInboundManager

This commit is contained in:
mfcar 2023-12-23 21:42:46 -03:00
commit 47854f2e28
No known key found for this signature in database
49 changed files with 1930 additions and 120 deletions

View file

@ -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())

View file

@ -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()

View file

@ -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()

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)