Compare commits

..

18 Commits

Author SHA1 Message Date
advplyr 777a055fcd Update podcast episode downloads to have a fallback user agent string 2025-06-12 17:31:12 -05:00
advplyr b45085d2d6 Update podcast episode download user agent to fix #4401 2025-06-12 17:19:24 -05:00
advplyr 22f6e86a12 Fix pathexists filepath back to posix 2025-06-11 16:37:07 -05:00
advplyr dc6783ea76 Merge pull request #4398 from advplyr/pathexists_user_access
Update pathexists endpoint to check user has access to library
2025-06-11 16:31:14 -05:00
advplyr a6f10ca48e Update upload endpoint to check user has access to library 2025-06-11 16:14:51 -05:00
advplyr aac01d6d9a Update pathexists endpoint to check user has access to library 2025-06-11 16:04:18 -05:00
advplyr 7a33a412fc Merge pull request #4393 from advplyr/fix_pathexists_join
Fix filesystem pathexists path join
2025-06-10 17:20:23 -05:00
advplyr 0135b3560c Fix filesystem pathexists path join 2025-06-10 17:02:42 -05:00
advplyr 6968a5c02a Merge pull request #4378 from Vito0912/feat/PodcastNots
Notifications for failed rss feeds and disabled rss feeds
2025-06-09 16:25:19 -05:00
advplyr 5e2bb0b12c Fix notification js docs and update description/defaults 2025-06-09 16:21:05 -05:00
advplyr 7122756e58 Update notification description grammar 2025-06-09 15:51:14 -05:00
advplyr 8ecc912c2d Merge pull request #4388 from advplyr/book_author_secondary_sort
Update book library sort by author to use title as secondary sort #4380
2025-06-08 17:38:45 -05:00
advplyr c8cea4e6af Update book library sort by author to use title as secondary sort #4380 2025-06-08 17:28:19 -05:00
advplyr 0c5d05d319 Fix chapter table on audiobook tools page uneven column widths 2025-06-07 17:10:23 -05:00
advplyr 4a3eb7727b Merge pull request #4385 from advplyr/clean_duplicate_mediaprogress
Update cleanDatabase to remove duplicate mediaProgresses
2025-06-06 17:17:43 -05:00
Vito0912 f0525d4f0d abc is hard 2025-06-05 14:09:35 +02:00
Vito0912 346df3680c local strings 2025-06-05 14:02:29 +02:00
Vito0912 6aa7c8a3d8 added notification 2025-06-05 13:34:18 +02:00
9 changed files with 147 additions and 24 deletions
+8 -8
View File
@@ -28,14 +28,14 @@
<div class="flex justify-center flex-wrap lg:flex-nowrap gap-4">
<div class="w-full max-w-2xl border border-white/10 bg-bg">
<div class="flex py-2 px-4">
<div class="w-1/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelMetaTag }}</div>
<div class="w-2/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
<div class="w-28 min-w-28 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelMetaTag }}</div>
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
</div>
<div class="w-full max-h-72 overflow-auto">
<template v-for="(value, key, index) in metadataObject">
<div :key="key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary/25' : ''">
<div class="w-1/3 font-semibold">{{ key }}</div>
<div class="w-2/3">
<div class="w-28 min-w-28 font-semibold">{{ key }}</div>
<div class="grow">
{{ value }}
</div>
</div>
@@ -45,18 +45,18 @@
<div class="w-full max-w-2xl border border-white/10 bg-bg">
<div class="flex py-2 px-4 bg-primary/25">
<div class="grow text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelChapterTitle }}</div>
<div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelStart }}</div>
<div class="w-24 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelEnd }}</div>
<div class="w-16 min-w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelStart }}</div>
<div class="w-16 min-w-16 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelEnd }}</div>
</div>
<div class="w-full max-h-72 overflow-auto">
<p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">{{ $strings.MessageNoChapters }}</p>
<template v-for="(chapter, index) in metadataChapters">
<div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary/25' : ''">
<div class="grow font-semibold">{{ chapter.title }}</div>
<div class="w-24">
<div class="w-16 min-w-16">
{{ $secondsToTimestamp(chapter.start) }}
</div>
<div class="w-24">
<div class="w-16 min-w-16">
{{ $secondsToTimestamp(chapter.end) }}
</div>
</div>
+2
View File
@@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Triggered when a backup is completed",
"NotificationOnBackupFailedDescription": "Triggered when a backup fails",
"NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded",
"NotificationOnRSSFeedDisabledDescription": "Triggered when automatic episode downloads are disabled due to too many failed attempts",
"NotificationOnRSSFeedFailedDescription": "Triggered when the RSS feed request fails for an automatic episode download",
"NotificationOnTestDescription": "Event for testing the notification system",
"PlaceholderNewCollection": "New collection name",
"PlaceholderNewFolderPath": "New folder path",
+8 -2
View File
@@ -89,7 +89,6 @@ class FileSystemController {
}
const { directory, folderPath } = req.body
if (!directory?.length || typeof directory !== 'string' || !folderPath?.length || typeof folderPath !== 'string') {
Logger.error(`[FileSystemController] Invalid request body: ${JSON.stringify(req.body)}`)
return res.status(400).json({
@@ -109,7 +108,14 @@ class FileSystemController {
return res.sendStatus(404)
}
const filepath = Path.posix.join(libraryFolder.path, directory)
if (!req.user.checkCanAccessLibrary(libraryFolder.libraryId)) {
Logger.error(`[FileSystemController] User "${req.user.username}" attempting to check path exists for library "${libraryFolder.libraryId}" without access`)
return res.sendStatus(403)
}
let filepath = Path.join(libraryFolder.path, directory)
filepath = fileUtils.filePathToPOSIX(filepath)
// Ensure filepath is inside library folder (prevents directory traversal)
if (!filepath.startsWith(libraryFolder.path)) {
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
+6
View File
@@ -59,6 +59,12 @@ class MiscController {
if (!library) {
return res.status(404).send('Library not found')
}
if (!req.user.checkCanAccessLibrary(library.id)) {
Logger.error(`[MiscController] User "${req.user.username}" attempting to upload to library "${library.id}" without access`)
return res.sendStatus(403)
}
const folder = library.libraryFolders.find((fold) => fold.id === folderId)
if (!folder) {
return res.status(404).send('Folder not found')
+48
View File
@@ -71,6 +71,54 @@ class NotificationManager {
this.triggerNotification('onBackupCompleted', eventData)
}
/**
* Handles scheduled episode download RSS feed request failed
*
* @param {string} feedUrl
* @param {number} numFailed
* @param {string} title
*/
async onRSSFeedFailed(feedUrl, numFailed, title) {
if (!Database.notificationSettings.isUseable) return
if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onRSSFeedFailed')) {
Logger.debug(`[NotificationManager] onRSSFeedFailed: No active notifications`)
return
}
Logger.debug(`[NotificationManager] onRSSFeedFailed: RSS feed request failed for ${feedUrl}`)
const eventData = {
feedUrl: feedUrl,
numFailed: numFailed || 0,
title: title || 'Unknown Title'
}
this.triggerNotification('onRSSFeedFailed', eventData)
}
/**
* Handles scheduled episode downloads disabled due to too many failed attempts
*
* @param {string} feedUrl
* @param {number} numFailed
* @param {string} title
*/
async onRSSFeedDisabled(feedUrl, numFailed, title) {
if (!Database.notificationSettings.isUseable) return
if (!Database.notificationSettings.getHasActiveNotificationsForEvent('onRSSFeedDisabled')) {
Logger.debug(`[NotificationManager] onRSSFeedDisabled: No active notifications`)
return
}
Logger.debug(`[NotificationManager] onRSSFeedDisabled: Podcast scheduled episode download disabled due to ${numFailed} failed requests for ${feedUrl}`)
const eventData = {
feedUrl: feedUrl,
numFailed: numFailed || 0,
title: title || 'Unknown Title'
}
this.triggerNotification('onRSSFeedDisabled', eventData)
}
/**
*
* @param {string} errorMsg
+2
View File
@@ -347,10 +347,12 @@ class PodcastManager {
this.failedCheckMap[libraryItem.id]++
if (this.MaxFailedEpisodeChecks !== 0 && this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
Logger.error(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.title}" - disabling auto download`)
void NotificationManager.onRSSFeedDisabled(libraryItem.media.feedURL, this.failedCheckMap[libraryItem.id], libraryItem.media.title)
libraryItem.media.autoDownloadEpisodes = false
delete this.failedCheckMap[libraryItem.id]
} else {
Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.title}"`)
void NotificationManager.onRSSFeedFailed(libraryItem.media.feedURL, this.failedCheckMap[libraryItem.id], libraryItem.media.title)
}
} else if (newEpisodes.length) {
delete this.failedCheckMap[libraryItem.id]
+33 -12
View File
@@ -103,18 +103,39 @@ module.exports.resizeImage = resizeImage
*/
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
return new Promise(async (resolve) => {
const response = await axios({
url: podcastEpisodeDownload.url,
method: 'GET',
responseType: 'stream',
headers: {
'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)'
},
timeout: global.PodcastDownloadTimeout
}).catch((error) => {
Logger.error(`[ffmpegHelpers] Failed to download podcast episode with url "${podcastEpisodeDownload.url}"`, error)
return null
})
// Some podcasts fail due to user agent strings
// See: https://github.com/advplyr/audiobookshelf/issues/3246 (requires iTMS user agent)
// See: https://github.com/advplyr/audiobookshelf/issues/4401 (requires no iTMS user agent)
const userAgents = ['audiobookshelf (+https://audiobookshelf.org; like iTMS)', 'audiobookshelf (+https://audiobookshelf.org)']
let response = null
let lastError = null
for (const userAgent of userAgents) {
try {
response = await axios({
url: podcastEpisodeDownload.url,
method: 'GET',
responseType: 'stream',
headers: {
'User-Agent': userAgent
},
timeout: global.PodcastDownloadTimeout
})
Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`)
break
} catch (error) {
lastError = error
Logger.warn(`[ffmpegHelpers] Failed to download podcast episode with User-Agent "${userAgent}" for url "${podcastEpisodeDownload.url}"`, error.message)
// If this is the last attempt, log the full error
if (userAgent === userAgents[userAgents.length - 1]) {
Logger.error(`[ffmpegHelpers] All User-Agent attempts failed for url "${podcastEpisodeDownload.url}"`, lastError)
}
}
}
if (!response) {
return resolve({
success: false
+32
View File
@@ -60,6 +60,38 @@ module.exports.notificationData = {
errorMsg: 'Example error message'
}
},
{
name: 'onRSSFeedFailed',
requiresLibrary: true,
description: 'Triggered when the RSS feed request fails for an automatic episode download',
descriptionKey: 'NotificationOnRSSFeedFailedDescription',
variables: ['feedUrl', 'numFailed', 'title'],
defaults: {
title: 'RSS Feed Request Failed',
body: 'Failed to request RSS feed for {{title}}.\nFeed URL: {{feedUrl}}\nNumber of failed attempts: {{numFailed}}'
},
testData: {
title: 'Test RSS Feed',
feedUrl: 'https://example.com/rss',
numFailed: 3
}
},
{
name: 'onRSSFeedDisabled',
requiresLibrary: true,
description: 'Triggered when automatic episode downloads are disabled due to too many failed attempts',
descriptionKey: 'NotificationOnRSSFeedDisabledDescription',
variables: ['feedUrl', 'numFailed', 'title'],
defaults: {
title: 'Podcast Episode Download Schedule Disabled',
body: 'Automatic episode downloads for {{title}} have been disabled due to too many failed RSS feed requests.\nFeed URL: {{feedUrl}}\nNumber of failed attempts: {{numFailed}}'
},
testData: {
title: 'Test RSS Feed',
feedUrl: 'https://example.com/rss',
numFailed: 5
}
},
{
name: 'onTest',
requiresLibrary: false,
@@ -264,9 +264,15 @@ module.exports = {
} else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') {
return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir]]
return [
[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir],
[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
]
} else if (sortBy === 'media.metadata.authorName') {
return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir]]
return [
[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir],
[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
]
} else if (sortBy === 'media.metadata.title') {
if (collapseseries) {
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]