From 9b01d11b27fa0e56debac8610dd02b1cea3aeecc Mon Sep 17 00:00:00 2001 From: Lauri Vuorela Date: Tue, 22 Oct 2024 23:58:09 +0200 Subject: [PATCH 001/224] allow setting createdAt and respect set finishedAt when syncing progress --- server/models/User.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/models/User.js b/server/models/User.js index aa63aea8..84f471e9 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -607,13 +607,14 @@ class User extends Model { ebookLocation: progressPayload.ebookLocation || null, ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress), finishedAt: progressPayload.finishedAt || null, + createdAt: progressPayload.createdAt || new Date(), extraData: { libraryItemId: progressPayload.libraryItemId, progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress) } } if (newMediaProgressPayload.isFinished) { - newMediaProgressPayload.finishedAt = new Date() + newMediaProgressPayload.finishedAt = newMediaProgressPayload.finishedAt || new Date() newMediaProgressPayload.extraData.progress = 1 } else { newMediaProgressPayload.finishedAt = null From 33e0987d7325191b82bc99b51ef3426b5d300e80 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:09:14 +0100 Subject: [PATCH 002/224] Added mediaMetadata to playbackSessions --- server/managers/PlaybackSessionManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index ce43fc8c..d0daa3ba 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -175,6 +175,12 @@ class PlaybackSessionManager { // New session from local session = new PlaybackSession(sessionJson) session.deviceInfo = deviceInfo + // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change + if(session.mediaMetadata == null) { + // Only sync important metadata + const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) await Database.createPlaybackSession(session) From 89167543fa18e43eab06bf194bbc1996bb08b648 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:25:52 +0100 Subject: [PATCH 003/224] added author for podcasts --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index d0daa3ba..8dd7bc36 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From 2fbb31e0ea84fabe0e08564141d427e3ab385cbf Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:37:00 +0100 Subject: [PATCH 004/224] added null saftey and added displayTitle and displayAuthor --- server/managers/PlaybackSessionManager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 8dd7bc36..e8704470 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,14 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + } + if(session.displayTitle == null || session.displayTitle === '') { + session.displayTitle = libraryItem?.media?.metadata?.title ?? '' + } + if(session.displayAuthor == null || session.displayAuthor === '') { + session.displayAuthor = libraryItem?.media?.metadata?.authors?.map(a => a.name).join(', ') ?? libraryItem?.media?.metadata?.author ?? '' } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From f9bbd7117405e393c8bef56653cb279575896c32 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:27:37 +0100 Subject: [PATCH 005/224] added type to be saved. Should support podcasts It should support everything important from the podcast metadata: https://api.audiobookshelf.org/#podcast-metadata And the book metadata: https://api.audiobookshelf.org/#book-metadata --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index e8704470..1f40f38e 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + const { title, subtitle, narrators, authors, author, series, genres, type } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, author, series, genres, type } } if(session.displayTitle == null || session.displayTitle === '') { session.displayTitle = libraryItem?.media?.metadata?.title ?? '' From 7919a8b581bfa1d11d352d87e375f16951abc39e Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 20 Feb 2025 17:40:54 -0600 Subject: [PATCH 006/224] Fix get podcast library items endpoint when not including a limit query param #4014 --- server/utils/queries/libraryItemsPodcastFilters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index a0411381..7b54eed0 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -112,7 +112,7 @@ module.exports = { countCache.set(cacheKey, count) } - findOptions.limit = limit + findOptions.limit = limit || null findOptions.offset = offset const rows = await model.findAll(findOptions) From a34b01fcb471edd19024f25ec4edacfec85aa177 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 20 Feb 2025 17:45:09 -0600 Subject: [PATCH 007/224] Add localization strings for Cover Provider and Activities #4017 --- client/components/widgets/NotificationWidget.vue | 4 ++-- client/pages/config/index.vue | 2 +- client/strings/en-us.json | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/components/widgets/NotificationWidget.vue b/client/components/widgets/NotificationWidget.vue index 57bf0257..6ead559b 100644 --- a/client/components/widgets/NotificationWidget.vue +++ b/client/components/widgets/NotificationWidget.vue @@ -5,8 +5,8 @@ - - notifications + + notifications
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 5636113e..1fd7e8f0 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -67,7 +67,7 @@
- +
diff --git a/client/strings/en-us.json b/client/strings/en-us.json index e4e1f7cd..76a773a2 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -220,6 +220,7 @@ "LabelAccountTypeGuest": "Guest", "LabelAccountTypeUser": "User", "LabelActivity": "Activity", + "LabelActivities": "Activities", "LabelAddToCollection": "Add to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", @@ -283,6 +284,7 @@ "LabelContinueSeries": "Continue Series", "LabelCover": "Cover", "LabelCoverImageURL": "Cover Image URL", + "LabelCoverProvider": "Cover Provider", "LabelCreatedAt": "Created At", "LabelCronExpression": "Cron Expression", "LabelCurrent": "Current", From 0cc2e39367f2ae4168a49f5c472db35a374deec8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 20 Feb 2025 17:59:09 -0600 Subject: [PATCH 008/224] Update en-us string order --- 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 76a773a2..3e3bfe33 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -219,8 +219,8 @@ "LabelAccountTypeAdmin": "Admin", "LabelAccountTypeGuest": "Guest", "LabelAccountTypeUser": "User", - "LabelActivity": "Activity", "LabelActivities": "Activities", + "LabelActivity": "Activity", "LabelAddToCollection": "Add to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToPlaylist": "Add to Playlist", From e8b60defb6b04fb3654208c5e9365bbdbcfcdbda Mon Sep 17 00:00:00 2001 From: mikiher Date: Fri, 21 Feb 2025 09:45:10 +0200 Subject: [PATCH 009/224] Invalidate count cache on entity update --- server/models/Book.js | 4 ++++ server/models/Podcast.js | 4 ++++ server/models/PodcastEpisode.js | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/server/models/Book.js b/server/models/Book.js index 1f4193a2..811a7af0 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -201,6 +201,10 @@ class Book extends Model { Book.addHook('afterCreate', async (instance) => { libraryItemsBookFilters.clearCountCache('afterCreate') }) + + Book.addHook('afterUpdate', async (instance) => { + libraryItemsBookFilters.clearCountCache('afterUpdate') + }) } /** diff --git a/server/models/Podcast.js b/server/models/Podcast.js index fa27821d..c72bda27 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -157,6 +157,10 @@ class Podcast extends Model { Podcast.addHook('afterCreate', async (instance) => { libraryItemsPodcastFilters.clearCountCache('podcast', 'afterCreate') }) + + Podcast.addHook('afterUpdate', async (instance) => { + libraryItemsPodcastFilters.clearCountCache('podcast', 'afterUpdate') + }) } get hasMediaFiles() { diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 4746f315..38f1287a 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -140,6 +140,10 @@ class PodcastEpisode extends Model { PodcastEpisode.addHook('afterCreate', async (instance) => { libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterCreate') }) + + PodcastEpisode.addHook('afterUpdate', async (instance) => { + libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterUpdate') + }) } get size() { From 9d7f44f73aca6157e7a4be28361c510200494ad1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 21 Feb 2025 17:39:36 -0600 Subject: [PATCH 010/224] Fix RSS Feed Open query --- server/utils/queries/libraryItemsBookFilters.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index d446a5e9..3787cc84 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -434,19 +434,17 @@ module.exports = { const libraryItemIncludes = [] const bookIncludes = [] - if (includeRSSFeed) { + + if (filterGroup === 'feed-open' || includeRSSFeed) { + const rssFeedRequired = filterGroup === 'feed-open' libraryItemIncludes.push({ model: Database.feedModel, - required: filterGroup === 'feed-open', - separate: true + required: rssFeedRequired, + separate: !rssFeedRequired }) } - if (filterGroup === 'feed-open' && !includeRSSFeed) { - libraryItemIncludes.push({ - model: Database.feedModel, - required: true - }) - } else if (filterGroup === 'share-open') { + + if (filterGroup === 'share-open') { bookIncludes.push({ model: Database.mediaItemShareModel, required: true From 452d354b525f36906b3c1f1b327e19bf6d228fa6 Mon Sep 17 00:00:00 2001 From: alexshch09 Date: Sat, 22 Feb 2025 00:44:52 +0100 Subject: [PATCH 011/224] fix(auth): Add admin-level auth to LibraryController delete update and issue removal --- server/controllers/LibraryController.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 0ece483f..3585dc51 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -254,6 +254,11 @@ class LibraryController { * @param {Response} res */ async update(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to update library`) + return res.sendStatus(403) + } + // Validation const updatePayload = {} const keysToCheck = ['name', 'provider', 'mediaType', 'icon'] @@ -519,6 +524,11 @@ class LibraryController { * @param {Response} res */ async delete(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library`) + return res.sendStatus(403) + } + // Remove library watcher Watcher.removeLibrary(req.library) @@ -639,6 +649,11 @@ class LibraryController { * @param {Response} res */ async removeLibraryItemsWithIssues(req, res) { + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library items missing or invalid`) + return res.sendStatus(403) + } + const libraryItemsWithIssues = await Database.libraryItemModel.findAll({ where: { libraryId: req.library.id, From 799879d67d50bc9e0095d1d2d0033a67dfd83433 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Fri, 21 Feb 2025 18:45:29 -0500 Subject: [PATCH 012/224] prevent long author strings from pushing the player controls down by truncating (#3944) * prevent long author strings from pushing the player controls down by truncating * move truncate to single author, instead of the main container --- client/components/app/MediaPlayerContainer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 8aa6188b..962ec2e8 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -13,7 +13,7 @@
person -
{{ podcastAuthor }}
+
{{ podcastAuthor }}
{{ author.name }}
From 19a65dba984fab9996398154ea6c3bcf29ae5184 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 21 Feb 2025 18:18:16 -0600 Subject: [PATCH 013/224] Update backup schedule description translations #4017 --- client/pages/config/backups.vue | 2 +- client/plugins/i18n.js | 14 ++++++++++++++ client/plugins/utils.js | 18 +++++++++--------- client/strings/en-us.json | 1 + 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index d1dce014..98406434 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -122,7 +122,7 @@ export default { }, scheduleDescription() { if (!this.cronExpression) return '' - const parsed = this.$parseCronExpression(this.cronExpression) + const parsed = this.$parseCronExpression(this.cronExpression, this) return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}` }, nextBackupDate() { diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 5f6b1508..1769e6eb 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -107,6 +107,19 @@ Vue.prototype.$formatNumber = (num) => { return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num) } +/** + * Get the days of the week for the current language + * Starts with Sunday + * @returns {string[]} + */ +Vue.prototype.$getDaysOfWeek = () => { + const days = [] + for (let i = 0; i < 7; i++) { + days.push(new Date(2025, 0, 5 + i).toLocaleString(Vue.prototype.$languageCodes.current, { weekday: 'long' })) + } + return days +} + const translations = { [defaultCode]: enUsStrings } @@ -148,6 +161,7 @@ async function loadi18n(code) { Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) this?.$eventBus?.$emit('change-lang', code) + return true } diff --git a/client/plugins/utils.js b/client/plugins/utils.js index 5ad909d3..96b1f31c 100644 --- a/client/plugins/utils.js +++ b/client/plugins/utils.js @@ -93,7 +93,7 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t return strs.join(' ') } -Vue.prototype.$parseCronExpression = (expression) => { +Vue.prototype.$parseCronExpression = (expression, context) => { if (!expression) return null const pieces = expression.split(' ') if (pieces.length !== 5) { @@ -102,31 +102,31 @@ Vue.prototype.$parseCronExpression = (expression) => { const commonPatterns = [ { - text: 'Every 12 hours', + text: context.$strings.LabelIntervalEvery12Hours, value: '0 */12 * * *' }, { - text: 'Every 6 hours', + text: context.$strings.LabelIntervalEvery6Hours, value: '0 */6 * * *' }, { - text: 'Every 2 hours', + text: context.$strings.LabelIntervalEvery2Hours, value: '0 */2 * * *' }, { - text: 'Every hour', + text: context.$strings.LabelIntervalEveryHour, value: '0 * * * *' }, { - text: 'Every 30 minutes', + text: context.$strings.LabelIntervalEvery30Minutes, value: '*/30 * * * *' }, { - text: 'Every 15 minutes', + text: context.$strings.LabelIntervalEvery15Minutes, value: '*/15 * * * *' }, { - text: 'Every minute', + text: context.$strings.LabelIntervalEveryMinute, value: '* * * * *' } ] @@ -147,7 +147,7 @@ Vue.prototype.$parseCronExpression = (expression) => { return null } - const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + const weekdays = context.$getDaysOfWeek() var weekdayText = 'day' if (pieces[4] !== '*') weekdayText = pieces[4] diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 3e3bfe33..8a2206a4 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -393,6 +393,7 @@ "LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEveryDay": "Every day", "LabelIntervalEveryHour": "Every hour", + "LabelIntervalEveryMinute": "Every minute", "LabelInvert": "Invert", "LabelItem": "Item", "LabelJumpBackwardAmount": "Jump backward amount", From 007691ffe52ce7d000f6467bce84a9f2a76d63b5 Mon Sep 17 00:00:00 2001 From: Achim Date: Sat, 22 Feb 2025 17:08:29 +0100 Subject: [PATCH 014/224] add "sort by filename" --- .../tables/podcast/LazyEpisodesTable.vue | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index 8821ccef..04829909 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -1,3 +1,4 @@ + @@ -646,13 +646,11 @@ export default { }, rssFeedOpen(data) { if (data.entityId === this.libraryItemId) { - console.log('RSS Feed Opened', data) this.rssFeed = data } }, rssFeedClosed(data) { if (data.entityId === this.libraryItemId) { - console.log('RSS Feed Closed', data) this.rssFeed = null } }, diff --git a/server/objects/PodcastEpisodeDownload.js b/server/objects/PodcastEpisodeDownload.js index 463ec072..7ff89395 100644 --- a/server/objects/PodcastEpisodeDownload.js +++ b/server/objects/PodcastEpisodeDownload.js @@ -43,7 +43,8 @@ class PodcastEpisodeDownload { season: this.rssPodcastEpisode?.season ?? null, episode: this.rssPodcastEpisode?.episode ?? null, episodeType: this.rssPodcastEpisode?.episodeType ?? 'full', - publishedAt: this.rssPodcastEpisode?.publishedAt ?? null + publishedAt: this.rssPodcastEpisode?.publishedAt ?? null, + guid: this.rssPodcastEpisode?.guid ?? null } } From 68ef0f83e1795b938e5f87e85d7cbb34051a340e Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 26 Feb 2025 18:00:36 -0600 Subject: [PATCH 046/224] Update select all in feed modal to check downloading --- client/components/modals/podcast/EpisodeFeed.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 561a9af2..167344bc 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -204,13 +204,13 @@ export default { }, toggleSelectAll(val) { for (const episode of this.episodesList) { - if (this.getIsEpisodeDownloaded(episode)) this.selectedEpisodes[episode.cleanUrl] = false + if (episode.isDownloaded || episode.isDownloading) this.selectedEpisodes[episode.cleanUrl] = false else this.$set(this.selectedEpisodes, episode.cleanUrl, val) } }, checkSetIsSelectedAll() { for (const episode of this.episodesList) { - if (!this.getIsEpisodeDownloaded(episode) && !this.selectedEpisodes[episode.cleanUrl]) { + if (!episode.isDownloaded && !episode.isDownloading && !this.selectedEpisodes[episode.cleanUrl]) { this.selectAll = false return } From 0a00ebcde1f76c71fd5dd1d874243536a96ae5e4 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Wed, 26 Feb 2025 21:40:56 -0700 Subject: [PATCH 047/224] Fix: flaky 2.15.0 migration test --- .../v2.15.0-series-column-unique.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/server/migrations/v2.15.0-series-column-unique.test.js b/test/server/migrations/v2.15.0-series-column-unique.test.js index 34b5e52e..7fce7b7a 100644 --- a/test/server/migrations/v2.15.0-series-column-unique.test.js +++ b/test/server/migrations/v2.15.0-series-column-unique.test.js @@ -126,9 +126,9 @@ describe('migration-v2.15.0-series-column-unique', () => { it('upgrade with duplicate series and no sequence', async () => { // Add some entries to the Series table using the UUID for the ids await queryInterface.bulkInsert('Series', [ - { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(7), updatedAt: new Date(7) }, + { id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(7), updatedAt: new Date(8) }, + { id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(7), updatedAt: new Date(9) }, { id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }, { id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }, { id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) } @@ -203,8 +203,8 @@ describe('migration-v2.15.0-series-column-unique', () => { it('upgrade with one book in two of the same series, both sequence are null', async () => { // Create two different series with the same name in the same library await queryInterface.bulkInsert('Series', [ - { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(8), updatedAt: new Date(20) }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(5), updatedAt: new Date(10) } ]) // Create a book that is in both series await queryInterface.bulkInsert('BookSeries', [ @@ -236,8 +236,8 @@ describe('migration-v2.15.0-series-column-unique', () => { it('upgrade with one book in two of the same series, one sequence is null', async () => { // Create two different series with the same name in the same library await queryInterface.bulkInsert('Series', [ - { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(5), updatedAt: new Date(9) }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(5), updatedAt: new Date(7) } ]) // Create a book that is in both series await queryInterface.bulkInsert('BookSeries', [ @@ -268,8 +268,8 @@ describe('migration-v2.15.0-series-column-unique', () => { it('upgrade with one book in two of the same series, both sequence are not null', async () => { // Create two different series with the same name in the same library await queryInterface.bulkInsert('Series', [ - { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }, - { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() } + { id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(1), updatedAt: new Date(3) }, + { id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(2), updatedAt: new Date(2) } ]) // Create a book that is in both series await queryInterface.bulkInsert('BookSeries', [ From 828d5d2afc62a95cca7b2009df676a49ec234aa6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 28 Feb 2025 17:42:56 -0600 Subject: [PATCH 048/224] Update episode row to show filename when sorting by filename --- client/components/tables/podcast/LazyEpisodeRow.vue | 10 ++++++++-- client/components/tables/podcast/LazyEpisodesTable.vue | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 8709b1ad..5a4a3a85 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -10,8 +10,13 @@

+
-
+

+ {{ $strings.LabelFilename }}: {{ episode.audioFile.metadata.filename }} +

+

{{ $getString('LabelSeasonNumber', [episode.season]) }}

{{ $getString('LabelEpisodeNumber', [episode.episode]) }}

{{ $getString('LabelChapterCount', [episode.chapters.length]) }}

@@ -65,7 +70,8 @@ export default { episode: { type: Object, default: () => null - } + }, + sortKey: String }, data() { return { diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index 04829909..2f95b523 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -180,7 +180,7 @@ export default { let bValue if (this.sortKey.includes('.')) { - const getNestedValue = (ob, s) => s.split('.').reduce((o, k) => o?.[k], ob); + const getNestedValue = (ob, s) => s.split('.').reduce((o, k) => o?.[k], ob) aValue = getNestedValue(a, this.sortKey) bValue = getNestedValue(b, this.sortKey) } else { @@ -454,7 +454,8 @@ export default { propsData: { index, libraryItemId: this.libraryItem.id, - episode: this.episodesList[index] + episode: this.episodesList[index], + sortKey: this.sortKey }, created() { this.$on('selected', (payload) => { From c6b5d4aa26bbab03f3cb69775cde2b6f77a0facb Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 1 Mar 2025 17:48:11 -0600 Subject: [PATCH 049/224] Update author by string translation #4017 --- client/pages/item/_id/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index c2ec6e17..dadb8575 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -41,7 +41,7 @@

{{ $getString('LabelByAuthor', [podcastAuthor]) }}

- by {{ author.name }} + {{ $getString('LabelByAuthor', ['']) }}{{ author.name }}

by Unknown

From 5746e848b06693a3bcf0d989165f0e385f4556de Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 2 Mar 2025 17:13:27 -0600 Subject: [PATCH 050/224] Fix:Trim whitespace from custom metadata provider name & url #4069 --- .../modals/AddCustomMetadataProviderModal.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/components/modals/AddCustomMetadataProviderModal.vue b/client/components/modals/AddCustomMetadataProviderModal.vue index a68c63cc..da24588b 100644 --- a/client/components/modals/AddCustomMetadataProviderModal.vue +++ b/client/components/modals/AddCustomMetadataProviderModal.vue @@ -10,14 +10,14 @@
- +
- +
@@ -65,7 +65,11 @@ export default { } }, methods: { - submitForm() { + async submitForm() { + // Remove focus from active input + document.activeElement?.blur?.() + await this.$nextTick() + if (!this.newName || !this.newUrl) { this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired) return From a296ac61325e9e2abbc3d641883f57ab2a420b4a Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:06:58 +0100 Subject: [PATCH 051/224] fix crash --- server/providers/CustomProviderAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 6841eb8c..91b34b0d 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -78,7 +78,7 @@ class CustomProviderAdapter { narrator, publisher, publishedYear, - description: htmlSanitizer.sanitize(description), + description: typeof description === 'string' ? htmlSanitizer.sanitize(description) : description, cover, isbn, asin, From b17e6010fd0014dc4e273911d3e86d9a4289e9f3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 4 Mar 2025 17:50:40 -0600 Subject: [PATCH 052/224] Add validation for custom metadata provider responses --- server/providers/CustomProviderAdapter.js | 66 +++++++++++++++++------ 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index 91b34b0d..a5fed393 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -69,25 +69,57 @@ class CustomProviderAdapter { throw new Error('Custom provider returned malformed response') } + const toStringOrUndefined = (value) => { + if (typeof value === 'string' || typeof value === 'number') return String(value) + if (Array.isArray(value) && value.every((v) => typeof v === 'string' || typeof v === 'number')) return value.join(',') + return undefined + } + const validateSeriesArray = (series) => { + if (!Array.isArray(series) || !series.length) return undefined + return series + .map((s) => { + if (!s?.series || typeof s.series !== 'string') return undefined + const _series = { + series: s.series + } + if (s.sequence && (typeof s.sequence === 'string' || typeof s.sequence === 'number')) { + _series.sequence = String(s.sequence) + } + return _series + }) + .filter((s) => s !== undefined) + } + // re-map keys to throw out - return matches.map(({ title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration }) => { - return { - title, - subtitle, - author, - narrator, - publisher, - publishedYear, - description: typeof description === 'string' ? htmlSanitizer.sanitize(description) : description, - cover, - isbn, - asin, - genres, - tags: tags?.join(',') || null, - series: series?.length ? series : null, - language, - duration + return matches.map((match) => { + const { title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration } = match + + const payload = { + title: toStringOrUndefined(title), + subtitle: toStringOrUndefined(subtitle), + author: toStringOrUndefined(author), + narrator: toStringOrUndefined(narrator), + publisher: toStringOrUndefined(publisher), + publishedYear: toStringOrUndefined(publishedYear), + description: description && typeof description === 'string' ? htmlSanitizer.sanitize(description) : undefined, + cover: toStringOrUndefined(cover), + isbn: toStringOrUndefined(isbn), + asin: toStringOrUndefined(asin), + genres: Array.isArray(genres) && genres.every((g) => typeof g === 'string') ? genres : undefined, + tags: toStringOrUndefined(tags), + series: validateSeriesArray(series), + language: toStringOrUndefined(language), + duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined } + + // Remove undefined values + for (const key in payload) { + if (payload[key] === undefined) { + delete payload[key] + } + } + + return payload }) } } From c29935e57b99582e83807d2d26dc3e39eaf81b92 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 6 Mar 2025 17:24:33 -0600 Subject: [PATCH 053/224] Update migration manager to validate migration files #4042 --- server/managers/MigrationManager.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/server/managers/MigrationManager.js b/server/managers/MigrationManager.js index 003f8dfa..8635def1 100644 --- a/server/managers/MigrationManager.js +++ b/server/managers/MigrationManager.js @@ -130,7 +130,21 @@ class MigrationManager { async initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) { // This check is for dependency injection in tests - const files = (await fs.readdir(this.migrationsDir)).filter((file) => !file.startsWith('.')).map((file) => path.join(this.migrationsDir, file)) + const files = (await fs.readdir(this.migrationsDir)) + .filter((file) => { + // Only include .js files and exclude dot files + return !file.startsWith('.') && path.extname(file).toLowerCase() === '.js' + }) + .map((file) => path.join(this.migrationsDir, file)) + + // Validate migration names + for (const file of files) { + const migrationName = path.basename(file, path.extname(file)) + const migrationVersion = this.extractVersionFromTag(migrationName) + if (!migrationVersion) { + throw new Error(`Invalid migration file: "${migrationName}". Unable to extract version from filename.`) + } + } const parent = new Umzug({ migrations: { From 81cd6f6c7d84196e670d745d82519ead88dc8e37 Mon Sep 17 00:00:00 2001 From: mikiher Date: Fri, 7 Mar 2025 21:14:50 +0200 Subject: [PATCH 054/224] Fix RTL issue in LazyEpisodeRow --- client/components/tables/podcast/LazyEpisodeRow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 5a4a3a85..343a6e37 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -8,7 +8,7 @@
-

+
From d3fd19da6573e87c02889afaf3a5dd565f6cb332 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 7 Mar 2025 17:23:18 -0600 Subject: [PATCH 055/224] Fixes for screen readers on podcast page and episodes table --- .../components/content/LibraryItemDetails.vue | 10 ++++----- client/components/controls/FilterSelect.vue | 22 ++++++++++--------- .../tables/podcast/LazyEpisodeRow.vue | 13 ++++++----- client/components/ui/Checkbox.vue | 10 ++++++--- client/pages/item/_id/index.vue | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/client/components/content/LibraryItemDetails.vue b/client/components/content/LibraryItemDetails.vue index 78abc2d9..7ef17d6a 100644 --- a/client/components/content/LibraryItemDetails.vue +++ b/client/components/content/LibraryItemDetails.vue @@ -11,7 +11,7 @@
-
+
{{ $strings.LabelPublishYear }}
@@ -19,7 +19,7 @@ {{ publishedYear }}
-
+
{{ $strings.LabelPublisher }}
@@ -27,7 +27,7 @@ {{ publisher }}
-
+
{{ $strings.LabelPodcastType }}
@@ -65,7 +65,7 @@ {{ language }}
-
+
{{ $strings.LabelDuration }}
@@ -73,7 +73,7 @@ {{ durationPretty }}
-
+
{{ $strings.LabelSize }}
diff --git a/client/components/controls/FilterSelect.vue b/client/components/controls/FilterSelect.vue index 4ce4cd3f..a2c8a18c 100644 --- a/client/components/controls/FilterSelect.vue +++ b/client/components/controls/FilterSelect.vue @@ -1,23 +1,25 @@