mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-28 14:49:38 +00:00
Merge branch 'advplyr:master' into master
This commit is contained in:
commit
f6a5e31c22
104 changed files with 5267 additions and 953 deletions
|
|
@ -99,7 +99,7 @@ module.exports.resizeImage = resizeImage
|
|||
/**
|
||||
*
|
||||
* @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload
|
||||
* @returns {Promise<{success: boolean, isFfmpegError?: boolean}>}
|
||||
* @returns {Promise<{success: boolean, isRequestError?: boolean}>}
|
||||
*/
|
||||
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
return new Promise(async (resolve) => {
|
||||
|
|
@ -118,6 +118,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
|||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'User-Agent': userAgent
|
||||
},
|
||||
timeout: global.PodcastDownloadTimeout
|
||||
|
|
@ -138,7 +139,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
|||
|
||||
if (!response) {
|
||||
return resolve({
|
||||
success: false
|
||||
success: false,
|
||||
isRequestError: true
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -203,8 +205,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
|||
Logger.error(`Full stderr dump for episode url "${podcastEpisodeDownload.url}": ${stderrLines.join('\n')}`)
|
||||
}
|
||||
resolve({
|
||||
success: false,
|
||||
isFfmpegError: true
|
||||
success: false
|
||||
})
|
||||
})
|
||||
ffmpeg.on('progress', (progress) => {
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ module.exports.getWindowsDrives = async () => {
|
|||
return []
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('wmic logicaldisk get name', async (error, stdout, stderr) => {
|
||||
exec('powershell -Command "(Get-PSDrive -PSProvider FileSystem).Name"', async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
|
|
@ -485,10 +485,9 @@ module.exports.getWindowsDrives = async () => {
|
|||
?.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line)
|
||||
.slice(1)
|
||||
const validDrives = []
|
||||
for (const drive of drives) {
|
||||
let drivepath = drive + '/'
|
||||
let drivepath = drive + ':/'
|
||||
if (await fs.pathExists(drivepath)) {
|
||||
validDrives.push(drivepath)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const fs = require('../../libs/fsExtra')
|
||||
|
||||
function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, token) {
|
||||
function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType) {
|
||||
var ext = hlsSegmentType === 'fmp4' ? 'm4s' : 'ts'
|
||||
|
||||
var lines = [
|
||||
|
|
@ -18,18 +18,18 @@ function getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, to
|
|||
var lastSegment = duration - (numSegments * segmentLength)
|
||||
for (let i = 0; i < numSegments; i++) {
|
||||
lines.push(`#EXTINF:6,`)
|
||||
lines.push(`${segmentName}-${i}.${ext}?token=${token}`)
|
||||
lines.push(`${segmentName}-${i}.${ext}`)
|
||||
}
|
||||
if (lastSegment > 0) {
|
||||
lines.push(`#EXTINF:${lastSegment},`)
|
||||
lines.push(`${segmentName}-${numSegments}.${ext}?token=${token}`)
|
||||
lines.push(`${segmentName}-${numSegments}.${ext}`)
|
||||
}
|
||||
lines.push('#EXT-X-ENDLIST')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function generatePlaylist(outputPath, segmentName, duration, segmentLength, hlsSegmentType, token) {
|
||||
var playlistStr = getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType, token)
|
||||
function generatePlaylist(outputPath, segmentName, duration, segmentLength, hlsSegmentType) {
|
||||
var playlistStr = getPlaylistStr(segmentName, duration, segmentLength, hlsSegmentType)
|
||||
return fs.writeFile(outputPath, playlistStr)
|
||||
}
|
||||
module.exports = generatePlaylist
|
||||
|
|
@ -277,3 +277,57 @@ module.exports.timestampToSeconds = (timestamp) => {
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
class ValidationError extends Error {
|
||||
constructor(paramName, message, status = 400) {
|
||||
super(`Query parameter "${paramName}" ${message}`)
|
||||
this.name = 'ValidationError'
|
||||
this.paramName = paramName
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
module.exports.ValidationError = ValidationError
|
||||
|
||||
class NotFoundError extends Error {
|
||||
constructor(message, status = 404) {
|
||||
super(message)
|
||||
this.name = 'NotFoundError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
module.exports.NotFoundError = NotFoundError
|
||||
|
||||
/**
|
||||
* Safely extracts a query parameter as a string, rejecting arrays to prevent type confusion
|
||||
* Express query parameters can be arrays if the same parameter appears multiple times
|
||||
* @example ?author=Smith => "Smith"
|
||||
* @example ?author=Smith&author=Jones => throws error
|
||||
*
|
||||
* @param {Object} query - Query object
|
||||
* @param {string} paramName - Parameter name
|
||||
* @param {string} defaultValue - Default value if undefined/null
|
||||
* @param {boolean} required - Whether the parameter is required
|
||||
* @param {number} maxLength - Optional maximum length (defaults to 10000 to prevent ReDoS attacks)
|
||||
* @returns {string} String value
|
||||
* @throws {ValidationError} If value is an array
|
||||
* @throws {ValidationError} If value is too long
|
||||
* @throws {ValidationError} If value is required but not provided
|
||||
*/
|
||||
module.exports.getQueryParamAsString = (query, paramName, defaultValue = '', required = false, maxLength = 1000) => {
|
||||
const value = query[paramName]
|
||||
if (value === undefined || value === null) {
|
||||
if (required) {
|
||||
throw new ValidationError(paramName, 'is required')
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
// Explicitly reject arrays to prevent type confusion
|
||||
if (Array.isArray(value)) {
|
||||
throw new ValidationError(paramName, 'is an array')
|
||||
}
|
||||
// Reject excessively long strings to prevent ReDoS attacks
|
||||
if (typeof value === 'string' && value.length > maxLength) {
|
||||
throw new ValidationError(paramName, 'is too long')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function extractEpisodeData(item) {
|
|||
|
||||
// Full description with html
|
||||
if (item['content:encoded']) {
|
||||
const rawDescription = (extractFirstArrayItem(item, 'content:encoded') || '').trim()
|
||||
const rawDescription = (extractFirstArrayItemString(item, 'content:encoded') || '').trim()
|
||||
episode.description = htmlSanitizer.sanitize(rawDescription)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -289,7 +289,11 @@ module.exports = {
|
|||
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
||||
return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS FLOAT) ${nullDir}`)]]
|
||||
} else if (sortBy === 'progress') {
|
||||
return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]]
|
||||
return [[Sequelize.literal(`mediaProgresses.updatedAt ${dir} NULLS LAST`)]]
|
||||
} else if (sortBy === 'progress.createdAt') {
|
||||
return [[Sequelize.literal(`mediaProgresses.createdAt ${dir} NULLS LAST`)]]
|
||||
} else if (sortBy === 'progress.finishedAt') {
|
||||
return [[Sequelize.literal(`mediaProgresses.finishedAt ${dir} NULLS LAST`)]]
|
||||
} else if (sortBy === 'random') {
|
||||
return [Database.sequelize.random()]
|
||||
}
|
||||
|
|
@ -519,7 +523,7 @@ module.exports = {
|
|||
}
|
||||
bookIncludes.push({
|
||||
model: Database.mediaProgressModel,
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt', 'createdAt', 'finishedAt'],
|
||||
where: mediaProgressWhere,
|
||||
required: false
|
||||
})
|
||||
|
|
@ -530,10 +534,10 @@ module.exports = {
|
|||
}
|
||||
|
||||
// When sorting by progress but not filtering by progress, include media progresses
|
||||
if (filterGroup !== 'progress' && sortBy === 'progress') {
|
||||
if (filterGroup !== 'progress' && ['progress.createdAt', 'progress.finishedAt', 'progress'].includes(sortBy)) {
|
||||
bookIncludes.push({
|
||||
model: Database.mediaProgressModel,
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt', 'createdAt', 'finishedAt'],
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue