Merge branch 'advplyr:master' into inode-bug-fix

This commit is contained in:
Jason Axley 2025-08-27 13:17:12 -07:00 committed by GitHub
commit 925c8aff91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1143 additions and 286 deletions

View file

@ -243,6 +243,7 @@ class Auth {
}
// Store the authentication method for long
Logger.debug(`[Auth] paramsToCookies: setting auth_method cookie to ${authMethod}`)
res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
return null
}
@ -258,6 +259,7 @@ class Auth {
// Handle token generation and get userResponse object
// For API based auth (e.g. mobile), we will return the refresh token in the response
const isApiBased = this.isAuthMethodAPIBased(req.cookies.auth_method)
Logger.debug(`[Auth] handleLoginSuccessBasedOnCookie: isApiBased: ${isApiBased}, auth_method: ${req.cookies.auth_method}`)
const userResponse = await this.handleLoginSuccess(req, res, isApiBased)
if (isApiBased) {
@ -298,6 +300,8 @@ class Auth {
userResponse.user.refreshToken = returnTokens ? refreshToken : null
userResponse.user.accessToken = accessToken
Logger.debug(`[Auth] handleLoginSuccess: returnTokens: ${returnTokens}, isRefreshTokenInResponse: ${!!userResponse.user.refreshToken}`)
if (!returnTokens) {
this.tokenManager.setRefreshTokenCookie(req, res, refreshToken)
}

View file

@ -11,6 +11,7 @@ const axios = require('axios')
const { version } = require('../package.json')
// Utils
const is = require('./libs/requestIp/isJs')
const fileUtils = require('./utils/fileUtils')
const { toNumber } = require('./utils/index')
const Logger = require('./Logger')
@ -419,7 +420,7 @@ class Server {
})
} else {
this.server.listen(this.Port, this.Host, () => {
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
if (this.Host) Logger.info(`Listening on http://${is.ipv6(this.Host) ? `[${this.Host}]` : this.Host}:${this.Port}`)
else Logger.info(`Listening on port :${this.Port}`)
})
}

View file

@ -121,28 +121,17 @@ class PodcastManager {
await fs.mkdir(this.currentDownload.libraryItem.path)
}
let success = false
if (this.currentDownload.isMp3) {
// Download episode and tag it
const ffmpegDownloadResponse = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
})
success = !!ffmpegDownloadResponse?.success
// Download episode and tag it
const ffmpegDownloadResponse = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
})
let success = !!ffmpegDownloadResponse?.success
// If failed due to ffmpeg error, retry without tagging
// e.g. RSS feed may have incorrect file extension and file type
// See https://github.com/advplyr/audiobookshelf/issues/3837
if (!success && ffmpegDownloadResponse?.isFfmpegError) {
Logger.info(`[PodcastManager] Retrying episode download without tagging`)
// Download episode only
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
.then(() => true)
.catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
return false
})
}
} else {
// If failed due to ffmpeg error, retry without tagging
// e.g. RSS feed may have incorrect file extension and file type
// See https://github.com/advplyr/audiobookshelf/issues/3837
if (!success && ffmpegDownloadResponse?.isFfmpegError) {
Logger.info(`[PodcastManager] Retrying episode download without tagging`)
// Download episode only
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
.then(() => true)

View file

@ -82,6 +82,13 @@ class Podcast extends Model {
const genres = Array.isArray(payload.metadata.genres) && payload.metadata.genres.every((g) => typeof g === 'string' && g.length) ? payload.metadata.genres : []
const tags = Array.isArray(payload.tags) && payload.tags.every((t) => typeof t === 'string' && t.length) ? payload.tags : []
const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type']
stringKeys.forEach((key) => {
if (typeof payload.metadata[key] === 'number') {
payload.metadata[key] = String(payload.metadata[key])
}
})
return this.create(
{
title,
@ -205,6 +212,11 @@ class Podcast extends Model {
if (payload.metadata) {
const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type']
stringKeys.forEach((key) => {
// Convert numbers to strings
if (typeof payload.metadata[key] === 'number') {
payload.metadata[key] = String(payload.metadata[key])
}
let newKey = key
if (key === 'type') {
newKey = 'podcastType'

View file

@ -63,16 +63,6 @@ class PodcastEpisodeDownload {
const enclosureType = this.rssPodcastEpisode.enclosure.type
return typeof enclosureType === 'string' ? enclosureType : null
}
/**
* RSS feed may have an episode with file extension of mp3 but the specified enclosure type is not mpeg.
* @see https://github.com/advplyr/audiobookshelf/issues/3711
*
* @returns {boolean}
*/
get isMp3() {
if (this.enclosureType && !this.enclosureType.includes('mpeg')) return false
return this.fileExtension === 'mp3'
}
get episodeTitle() {
return this.rssPodcastEpisode.title
}

View file

@ -146,7 +146,7 @@ class Stream extends EventEmitter {
async generatePlaylist() {
await fs.ensureDir(this.streamPath)
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType)
return this.clientPlaylistUri
}

View file

@ -118,6 +118,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
method: 'GET',
responseType: 'stream',
headers: {
'Accept': '*/*',
'User-Agent': userAgent
},
timeout: global.PodcastDownloadTimeout

View file

@ -502,7 +502,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
@ -511,10 +511,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 {

View file

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

View file

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