mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-13 22:14:24 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 777a055fcd | |||
| b45085d2d6 | |||
| 22f6e86a12 | |||
| dc6783ea76 | |||
| a6f10ca48e | |||
| aac01d6d9a | |||
| 7a33a412fc | |||
| 0135b3560c | |||
| 6968a5c02a | |||
| 5e2bb0b12c | |||
| 7122756e58 | |||
| 8ecc912c2d | |||
| c8cea4e6af | |||
| 0c5d05d319 | |||
| 4a3eb7727b | |||
| f0525d4f0d | |||
| 346df3680c | |||
| 6aa7c8a3d8 |
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user