From 9b01d11b27fa0e56debac8610dd02b1cea3aeecc Mon Sep 17 00:00:00 2001 From: Lauri Vuorela Date: Tue, 22 Oct 2024 23:58:09 +0200 Subject: [PATCH 001/141] 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 aa63aea82..84f471e96 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 007691ffe52ce7d000f6467bce84a9f2a76d63b5 Mon Sep 17 00:00:00 2001 From: Achim Date: Sat, 22 Feb 2025 17:08:29 +0100 Subject: [PATCH 002/141] 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 8821ccef1..04829909b 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 463ec0721..7ff893957 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 010/141] 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 561a9af27..167344bc1 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 011/141] 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 34b5e52ed..7fce7b7a1 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 012/141] 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 8709b1ad6..5a4a3a856 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 04829909b..2f95b523e 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 013/141] 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 c2ec6e17d..dadb85751 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 014/141] 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 a68c63cc3..da24588b1 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 015/141] 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 6841eb8cd..91b34b0d4 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 016/141] 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 91b34b0d4..a5fed3939 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 017/141] 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 003f8dfa1..8635def10 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 018/141] 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 5a4a3a856..343a6e371 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 019/141] 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 78abc2d98..7ef17d6a3 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 4ce4cd3f6..a2c8a18cd 100644 --- a/client/components/controls/FilterSelect.vue +++ b/client/components/controls/FilterSelect.vue @@ -1,23 +1,25 @@