From 9968743a9349c28bd5e22268e943757d0b60567a Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:32:52 +0200 Subject: [PATCH 01/49] fix wrong display and ignored values --- client/components/widgets/EncoderOptionsCard.vue | 16 ++++++++++++---- client/pages/audiobook/_id/manage.vue | 2 ++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/client/components/widgets/EncoderOptionsCard.vue b/client/components/widgets/EncoderOptionsCard.vue index 005563b44..977d766be 100644 --- a/client/components/widgets/EncoderOptionsCard.vue +++ b/client/components/widgets/EncoderOptionsCard.vue @@ -143,10 +143,18 @@ export default { localStorage.setItem('embedMetadataCodec', val) }, getEncodingOptions() { - return { - codec: this.selectedCodec || 'aac', - bitrate: this.selectedBitrate || '128k', - channels: this.selectedChannels || 2 + if (this.showAdvancedView) { + return { + codec: this.customCodec || this.selectedCodec || 'aac', + bitrate: this.customBitrate || this.selectedBitrate || '128k', + channels: this.customChannels || this.selectedChannels || 2 + } + } else { + return { + codec: this.selectedCodec || 'aac', + bitrate: this.selectedBitrate || '128k', + channels: this.selectedChannels || 2 + } } }, setPreset() { diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 7afe12a9c..f5db9cce2 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -356,6 +356,8 @@ export default { const encodeOptions = this.$refs.encoderOptionsCard.getEncodingOptions() + this.encodingOptions = encodeOptions; + const queryParams = new URLSearchParams(encodeOptions) this.processing = true From b6995ba5d1573c7186b01b63d1443cc20d20503b Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:33:50 +0200 Subject: [PATCH 02/49] prettier --- client/pages/audiobook/_id/manage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index f5db9cce2..377349345 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -356,7 +356,7 @@ export default { const encodeOptions = this.$refs.encoderOptionsCard.getEncodingOptions() - this.encodingOptions = encodeOptions; + this.encodingOptions = encodeOptions const queryParams = new URLSearchParams(encodeOptions) From 424ef1aec31a0b53997363198a5cd15eb598577d Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:34:25 +0200 Subject: [PATCH 03/49] prettier 2 --- client/components/widgets/LoadingSpinner.vue | 2 +- client/components/widgets/SeriesInputWidget.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/widgets/LoadingSpinner.vue b/client/components/widgets/LoadingSpinner.vue index a9c4ef47b..8f3de84af 100644 --- a/client/components/widgets/LoadingSpinner.vue +++ b/client/components/widgets/LoadingSpinner.vue @@ -248,4 +248,4 @@ export default { transform: scale(0); } } - \ No newline at end of file + diff --git a/client/components/widgets/SeriesInputWidget.vue b/client/components/widgets/SeriesInputWidget.vue index d6c8cf9f9..3dab0605a 100644 --- a/client/components/widgets/SeriesInputWidget.vue +++ b/client/components/widgets/SeriesInputWidget.vue @@ -109,4 +109,4 @@ export default { } } } - \ No newline at end of file + From 63ccdb68f0c61a1a28e2bbedb6b49b94490f9cd3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 2 Jun 2025 16:50:03 -0500 Subject: [PATCH 04/49] Fix m4b encoder backup file overwriting the encoded file when they have the same filename --- server/managers/AbMergeManager.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index f6a561607..3611d294f 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -203,7 +203,15 @@ class AbMergeManager { // Move library item tracks to cache for (const [index, trackPath] of task.data.originalTrackPaths.entries()) { const trackFilename = Path.basename(trackPath) - const moveToPath = Path.join(task.data.itemCachePath, trackFilename) + let moveToPath = Path.join(task.data.itemCachePath, trackFilename) + + // If the track is the same as the temp file, we need to rename it to avoid overwriting it + if (task.data.tempFilepath === moveToPath) { + const trackExtname = Path.extname(task.data.tempFilepath) + const newTrackFilename = Path.basename(task.data.tempFilepath, trackExtname) + '.backup' + trackExtname + moveToPath = Path.join(task.data.itemCachePath, newTrackFilename) + } + Logger.debug(`[AbMergeManager] Backing up original track "${trackPath}" to ${moveToPath}`) if (index === 0) { // copy the first track to the cache directory From 4d846e225a40ebea605edf88c2ae973095646f3b Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:02:17 +0200 Subject: [PATCH 05/49] Adds ENV for MaxFailedEpisodeChecks --- server/managers/PodcastManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 052ba8b33..c2fae077d 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -30,7 +30,7 @@ class PodcastManager { this.currentDownload = null this.failedCheckMap = {} - this.MaxFailedEpisodeChecks = 24 + this.MaxFailedEpisodeChecks = process.env.MAX_FAILED_EPISODE_CHECKS || 24 } getEpisodeDownloadsInQueue(libraryItemId) { @@ -345,7 +345,7 @@ class PodcastManager { // Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0 this.failedCheckMap[libraryItem.id]++ - if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) { + 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`) libraryItem.media.autoDownloadEpisodes = false delete this.failedCheckMap[libraryItem.id] From 709c33f27af56a5790bdc5f8312c00b2f8f01eab Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:05:16 +0200 Subject: [PATCH 06/49] ensure proper type --- server/managers/PodcastManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index c2fae077d..54ce4c9fa 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -30,7 +30,7 @@ class PodcastManager { this.currentDownload = null this.failedCheckMap = {} - this.MaxFailedEpisodeChecks = process.env.MAX_FAILED_EPISODE_CHECKS || 24 + this.MaxFailedEpisodeChecks = parseInt(process.env.MAX_FAILED_EPISODE_CHECKS, 10) || 24 } getEpisodeDownloadsInQueue(libraryItemId) { @@ -345,7 +345,7 @@ class PodcastManager { // Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0 this.failedCheckMap[libraryItem.id]++ - if (this.MaxFailedEpisodeChecks != 0 && this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) { + 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`) libraryItem.media.autoDownloadEpisodes = false delete this.failedCheckMap[libraryItem.id] From 9bb4dc3ab0a00c9d9df26390413f3a52b1071a94 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:58:44 +0200 Subject: [PATCH 07/49] potential fix --- server/managers/PodcastManager.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 052ba8b33..bb4a408c2 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -384,7 +384,13 @@ class PodcastManager { Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id})`) return null } - const feed = await getPodcastFeed(podcastLibraryItem.media.feedURL) + const feed = await Promise.race([ + getPodcastFeed(podcastLibraryItem.media.feedURL), + new Promise((_, reject) => + // The added second is to make sure that axios can fail first and only falls back later + setTimeout(() => reject(new Error('Timeout. getPodcastFeed seemed to timeout but not triggering the timeout.')), global.PodcastDownloadTimeout + 1000) + ) + ]) if (!feed?.episodes) { Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id})`, feed) return null From 357176b301551b8c2551b94f41be87c28df3e950 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:15:18 +0200 Subject: [PATCH 08/49] catch timeout --- server/managers/PodcastManager.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index bb4a408c2..625688c97 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -390,7 +390,11 @@ class PodcastManager { // The added second is to make sure that axios can fail first and only falls back later setTimeout(() => reject(new Error('Timeout. getPodcastFeed seemed to timeout but not triggering the timeout.')), global.PodcastDownloadTimeout + 1000) ) - ]) + ]).catch((error) => { + Logger.error(`[PodcastManager] checkPodcastForNewEpisodes failed to fetch feed for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id}):`, error) + return null + }) + if (!feed?.episodes) { Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.title} (ID: ${podcastLibraryItem.id})`, feed) return null From 759c58d3f7561b6d56b61fa78ec6880f10796fa8 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:38:01 +0200 Subject: [PATCH 09/49] remove any attachment --- client/components/ui/VueTrix.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/components/ui/VueTrix.vue b/client/components/ui/VueTrix.vue index 2687d934a..0836df154 100644 --- a/client/components/ui/VueTrix.vue +++ b/client/components/ui/VueTrix.vue @@ -318,10 +318,8 @@ export default { } }, handleAttachmentAdd(event) { - // Prevent pasting in images from the browser - if (!event.attachment.file) { - event.attachment.remove() - } + // Prevent pasting in images/any files from the browser + event.attachment.remove() } }, mounted() { From 6aa7c8a3d8734b55f932e8c270d830bc8644d36f Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:34:18 +0200 Subject: [PATCH 10/49] added notification --- server/managers/NotificationManager.js | 48 ++++++++++++++++++++++++++ server/managers/PodcastManager.js | 2 ++ server/utils/notifications.js | 32 +++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js index 8edcf4280..4f7072be5 100644 --- a/server/managers/NotificationManager.js +++ b/server/managers/NotificationManager.js @@ -71,6 +71,54 @@ class NotificationManager { this.triggerNotification('onBackupCompleted', eventData) } + /** + * Handles RSS feed updates + * @param feedUrl + * @param numFailed + * @param title + * @returns {Promise} + */ + 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 update failed for ${feedUrl}`) + const eventData = { + feedUrl: feedUrl, + numFailed: numFailed || 0, + title: title || 'Unknown Title' + } + this.triggerNotification('onRSSFeedFailed', eventData) + } + + /** + * Handles RSS feed being disabled due to too many failed updates + * @param feedUrl + * @param numFailed + * @param title + * @returns {Promise} + */ + 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: RSS feed disabled due to ${numFailed} failed updates for ${feedUrl}`) + const eventData = { + feedUrl: feedUrl, + numFailed: numFailed || 0, + title: title || 'Unknown Title' + } + this.triggerNotification('onRSSFeedDisabled', eventData) + } + /** * * @param {string} errorMsg diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 625688c97..e5da8fe6b 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -347,10 +347,12 @@ class PodcastManager { this.failedCheckMap[libraryItem.id]++ if (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] diff --git a/server/utils/notifications.js b/server/utils/notifications.js index 7a3e11983..1b9612b95 100644 --- a/server/utils/notifications.js +++ b/server/utils/notifications.js @@ -60,6 +60,38 @@ module.exports.notificationData = { errorMsg: 'Example error message' } }, + { + name: 'onRSSFeedFailed', + requiresLibrary: true, + description: 'Triggered when an RSS feed request/update fails, but gets not disabled', + descriptionKey: 'NotificationOnRSSFeedFailedDescription', + variables: ['feedUrl', 'numFailed', 'title'], + defaults: { + title: 'RSS Feed Update Failed', + body: 'Failed to update 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 an RSS feed is disabled due to too many failed attempts', + descriptionKey: 'NotificationOnRSSFeedDisabledDescription', + variables: ['feedUrl', 'numFailed', 'title'], + defaults: { + title: 'RSS Feed Disabled', + body: 'RSS feed for {{title}} has been disabled due to too many failed updates.\nFeed URL: {{feedUrl}}\nNumber of failed attempts: {{numFailed}}' + }, + testData: { + title: 'Test RSS Feed', + feedUrl: 'https://example.com/rss', + numFailed: 5 + } + }, { name: 'onTest', requiresLibrary: false, From 346df3680ce8dd415ce3d77d5ce789686e8dd5f9 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:02:29 +0200 Subject: [PATCH 11/49] local strings --- client/strings/en-us.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 939eb9f4b..65fd5f0eb 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -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", + "NotificationOnRSSFeedFailedDescription": "Triggered when an RSS feed request/update fails, but gets not disabled", + "NotificationOnRSSFeedDisabledDescription": "Triggered when an RSS feed is disabled due to too many failed attempts", "NotificationOnTestDescription": "Event for testing the notification system", "PlaceholderNewCollection": "New collection name", "PlaceholderNewFolderPath": "New folder path", From 84c9c6cb50d6e4a725d0de6c6cba2449e50eab88 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:07:35 +0200 Subject: [PATCH 12/49] move to global --- server/Server.js | 6 ++++++ server/managers/PodcastManager.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index 17c959c08..317dab388 100644 --- a/server/Server.js +++ b/server/Server.js @@ -91,6 +91,12 @@ class Server { global.PodcastDownloadTimeout = 30000 } + if (process.env.MAX_FAILED_EPISODE_CHECKS) { + global.MaxFailedEpisodeChecks = process.env.MAX_FAILED_EPISODE_CHECKS + } else { + global.MaxFailedEpisodeChecks = 24 + } + if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) } diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 54ce4c9fa..0c7da9257 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -30,7 +30,7 @@ class PodcastManager { this.currentDownload = null this.failedCheckMap = {} - this.MaxFailedEpisodeChecks = parseInt(process.env.MAX_FAILED_EPISODE_CHECKS, 10) || 24 + this.MaxFailedEpisodeChecks = global.MaxFailedEpisodeChecks } getEpisodeDownloadsInQueue(libraryItemId) { From f0525d4f0de8392b87d788fda3a0a6bda57d15c8 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:09:35 +0200 Subject: [PATCH 13/49] abc is hard --- client/strings/en-us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 65fd5f0eb..afecd671c 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -918,8 +918,8 @@ "NotificationOnBackupCompletedDescription": "Triggered when a backup is completed", "NotificationOnBackupFailedDescription": "Triggered when a backup fails", "NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded", - "NotificationOnRSSFeedFailedDescription": "Triggered when an RSS feed request/update fails, but gets not disabled", "NotificationOnRSSFeedDisabledDescription": "Triggered when an RSS feed is disabled due to too many failed attempts", + "NotificationOnRSSFeedFailedDescription": "Triggered when an RSS feed request/update fails, but gets not disabled", "NotificationOnTestDescription": "Event for testing the notification system", "PlaceholderNewCollection": "New collection name", "PlaceholderNewFolderPath": "New folder path", From 8e0185907502926b818fc36a2fea844c5dbac588 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 5 Jun 2025 14:31:12 -0500 Subject: [PATCH 14/49] Cast PODCAST_DOWNLOAD_TIMEOUT and MAX_FAILED_EPISODE_CHECKS env vars to numbers --- server/Server.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/server/Server.js b/server/Server.js index 317dab388..5c6f3c16c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -12,6 +12,7 @@ const { version } = require('../package.json') // Utils const fileUtils = require('./utils/fileUtils') +const { toNumber } = require('./utils/index') const Logger = require('./Logger') const Auth = require('./Auth') @@ -84,18 +85,8 @@ class Server { global.DisableSsrfRequestFilter = (url) => whitelistedUrls.includes(new URL(url).hostname) } } - - if (process.env.PODCAST_DOWNLOAD_TIMEOUT) { - global.PodcastDownloadTimeout = process.env.PODCAST_DOWNLOAD_TIMEOUT - } else { - global.PodcastDownloadTimeout = 30000 - } - - if (process.env.MAX_FAILED_EPISODE_CHECKS) { - global.MaxFailedEpisodeChecks = process.env.MAX_FAILED_EPISODE_CHECKS - } else { - global.MaxFailedEpisodeChecks = 24 - } + global.PodcastDownloadTimeout = toNumber(process.env.PODCAST_DOWNLOAD_TIMEOUT, 30000) + global.MaxFailedEpisodeChecks = toNumber(process.env.MAX_FAILED_EPISODE_CHECKS, 24) if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) From eda7036f70c9a3a1cd699231c8ac9531368aee31 Mon Sep 17 00:00:00 2001 From: Jan Kubovy Date: Fri, 6 Jun 2025 10:43:52 +0000 Subject: [PATCH 15/49] Use fuse.js for podcast episode search Replace levenshtein distance with fuse.js fuzzy searching library. Search in episode's title and subtitle --- package-lock.json | 9 +++++++++ package.json | 1 + server/utils/podcastUtils.js | 39 +++++++++++++++++------------------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index a80747945..9147d9e55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", + "fuse.js": "^7.1.0", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", @@ -2105,6 +2106,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index d4831736d..2a77ec877 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cookie-parser": "^1.4.6", "express": "^4.17.1", "express-session": "^1.17.3", + "fuse.js": "^7.1.0", "graceful-fs": "^4.2.10", "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 3a1df198f..74a71cc1f 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -3,6 +3,7 @@ const ssrfFilter = require('ssrf-req-filter') const Logger = require('../Logger') const { xmlToJSON, levenshteinDistance, timestampToSeconds } = require('./index') const htmlSanitizer = require('../utils/htmlSanitizer') +const Fuse = require('fuse.js') /** * @typedef RssPodcastChapter @@ -407,7 +408,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { }) } -// Return array of episodes ordered by closest match (Levenshtein distance of 6 or less) +// Return array of episodes ordered by closest match using fuse.js module.exports.findMatchingEpisodes = async (feedUrl, searchTitle) => { const feed = await this.getPodcastFeed(feedUrl).catch(() => { return null @@ -420,32 +421,28 @@ module.exports.findMatchingEpisodes = async (feedUrl, searchTitle) => { * * @param {RssPodcast} feed * @param {string} searchTitle - * @returns {Array<{ episode: RssPodcastEpisode, levenshtein: number }>} + * @returns {Array<{ episode: RssPodcastEpisode }>} */ module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => { - searchTitle = searchTitle.toLowerCase().trim() if (!feed?.episodes) { return null } + const fuseOptions = { + ignoreDiacritics: true, + threshold: 0.4, // default 0.6 return too many matches + keys: [ + {name: 'title', weight: 0.7}, // prefer match in title + {name: 'subtitle', weight: 0.3} + ] + } + const fuse = new Fuse(feed.episodes, fuseOptions) + const matches = [] - feed.episodes.forEach((ep) => { - if (!ep.title) return - const epTitle = ep.title.toLowerCase().trim() - if (epTitle === searchTitle) { - matches.push({ - episode: ep, - levenshtein: 0 - }) - } else { - const levenshtein = levenshteinDistance(searchTitle, epTitle, true) - if (levenshtein <= 6 && epTitle.length > levenshtein) { - matches.push({ - episode: ep, - levenshtein - }) - } - } + fuse.search(searchTitle).forEach((match) => { + matches.push({ + episode: match.item + }) }) - return matches.sort((a, b) => a.levenshtein - b.levenshtein) + return matches } From 81640464ba71a917d199a885f86658ea38a9fb5a Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 6 Jun 2025 17:05:07 -0500 Subject: [PATCH 16/49] Update cleanDatabase to remove duplicate mediaProgresses --- server/Database.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/Database.js b/server/Database.js index 52827e3fb..2413a269e 100644 --- a/server/Database.js +++ b/server/Database.js @@ -765,6 +765,15 @@ class Database { if (badSessionsRemoved > 0) { Logger.warn(`Removed ${badSessionsRemoved} sessions that were 3 seconds or less`) } + + // Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt) + const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT id, mediaItemId FROM mediaProgresses WHERE (mediaItemId, updatedAt) IN (SELECT mediaItemId, MIN(updatedAt) FROM mediaProgresses GROUP BY mediaItemId HAVING COUNT(*) > 1)`) + for (const duplicateMediaProgress of duplicateMediaProgresses) { + Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`) + await this.mediaProgressModel.destroy({ + where: { id: duplicateMediaProgress.id } + }) + } } async createTextSearchQuery(query) { From 0c5d05d3199c3aea64769a1f5f2c1535d8b3c9c6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 7 Jun 2025 17:10:23 -0500 Subject: [PATCH 17/49] Fix chapter table on audiobook tools page uneven column widths --- client/pages/audiobook/_id/manage.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 377349345..40672af2e 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -28,14 +28,14 @@
-
{{ $strings.LabelMetaTag }}
-
{{ $strings.LabelValue }}
+
{{ $strings.LabelMetaTag }}
+
{{ $strings.LabelValue }}