From 286185329d50695761653b87d2d8e54d2ad5431b Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 22 Jan 2025 08:53:23 +0200 Subject: [PATCH 01/43] Support rich text book descriptions --- client/assets/defaultStyles.css | 15 ++- client/assets/trix.css | 11 +- client/components/cards/BookMatchCard.vue | 2 +- client/components/modals/item/tabs/Match.vue | 4 +- .../components/modals/podcast/ViewEpisode.vue | 2 +- client/components/ui/RichTextEditor.vue | 51 ++------- client/components/ui/VueTrix.vue | 103 +++++++++++++----- client/components/widgets/BookDetailsEdit.vue | 2 +- client/pages/item/_id/index.vue | 5 +- server/finders/BookFinder.js | 7 ++ server/models/Book.js | 4 + server/providers/Audible.js | 3 +- server/providers/iTunes.js | 2 +- server/utils/htmlSanitizer.js | 8 +- 14 files changed, 136 insertions(+), 83 deletions(-) diff --git a/client/assets/defaultStyles.css b/client/assets/defaultStyles.css index 027ccdf2..e0ca79e2 100644 --- a/client/assets/defaultStyles.css +++ b/client/assets/defaultStyles.css @@ -52,4 +52,17 @@ text-indent: 0px !important; text-align: start !important; text-align-last: start !important; -} \ No newline at end of file +} + +.default-style.less-spacing p { + margin-block-start: 0; +} + +.default-style.less-spacing ul { + margin-block-start: 0; +} + +.default-style.less-spacing ol { + margin-block-start: 0; +} + diff --git a/client/assets/trix.css b/client/assets/trix.css index 8f88c61f..7432b25f 100644 --- a/client/assets/trix.css +++ b/client/assets/trix.css @@ -446,7 +446,7 @@ trix-editor .attachment__metadata .attachment__size { } .trix-content { - line-height: 1.5; + line-height: inherit; } .trix-content * { @@ -455,6 +455,13 @@ trix-editor .attachment__metadata .attachment__size { padding: 0; } +.trix-content p { + box-sizing: border-box; + margin-top: 0; + margin-bottom: 0.5em; + padding: 0; +} + .trix-content h1 { font-size: 1.2em; line-height: 1.2; @@ -560,4 +567,4 @@ trix-editor .attachment__metadata .attachment__size { .trix-content .attachment-gallery.attachment-gallery--4 .attachment { flex-basis: 50%; max-width: 50%; -} \ No newline at end of file +} diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index d5355e91..4fa24c1f 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -24,7 +24,7 @@
-

{{ book.description }}

+

{{ book.descriptionPlain }}

diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index c7247d51..623ef2a1 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -94,9 +94,9 @@ diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue index af67242a..6c1a678c 100644 --- a/client/components/modals/podcast/ViewEpisode.vue +++ b/client/components/modals/podcast/ViewEpisode.vue @@ -16,7 +16,7 @@

{{ title }}

-
+

{{ $strings.MessageNoDescription }}

diff --git a/client/components/ui/RichTextEditor.vue b/client/components/ui/RichTextEditor.vue index c5ae6d83..51bdc5ae 100644 --- a/client/components/ui/RichTextEditor.vue +++ b/client/components/ui/RichTextEditor.vue @@ -1,9 +1,9 @@ @@ -12,7 +12,10 @@ export default { props: { value: String, label: String, - disabled: Boolean + disabled: { + type: Boolean, + default: false + } }, data() { return {} @@ -25,49 +28,19 @@ export default { set(val) { this.$emit('input', val) } - }, - config() { - return { - toolbar: { - getDefaultHTML: () => `
- - - - - - - - - - - - - - - - -
-
- -
` - } - } } }, methods: { trixFileAccept(e) { e.preventDefault() + }, + blur() { + if (this.$refs.input && this.$refs.input.blur) { + this.$refs.input.blur() + } } }, mounted() {}, beforeDestroy() {} } - \ No newline at end of file + diff --git a/client/components/ui/VueTrix.vue b/client/components/ui/VueTrix.vue index 5d351c72..8bbb42df 100644 --- a/client/components/ui/VueTrix.vue +++ b/client/components/ui/VueTrix.vue @@ -1,6 +1,37 @@ @@ -14,6 +45,30 @@ import Trix from 'trix' import '@/assets/trix.css' +function enableBreakParagraphOnReturn() { + // Trix works with divs by default, we want paragraphs instead + Trix.config.blockAttributes.default.tagName = 'p' + // Enable break paragraph on Enter (Shift + Enter will still create a line break) + Trix.config.blockAttributes.default.breakOnReturn = true + + // Hack to fix buggy paragraph breaks + // Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942 + Trix.Block.prototype.breaksOnReturn = function () { + const attr = this.getLastAttribute() + const config = Trix.getBlockConfig(attr ? attr : 'default') + return config ? config.breakOnReturn : false + } + Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () { + if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) { + return this.startLocation.offset > 0 + } else { + return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false + } + } +} + +enableBreakParagraphOnReturn() + export default { name: 'vue-trix', model: { @@ -134,6 +189,9 @@ export default { * Compute a random id of hidden input * when it haven't been specified. */ + toolbarId() { + return `trix-toolbar-${this.generateId}` + }, generateId() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { var r = (Math.random() * 16) | 0 @@ -223,13 +281,17 @@ export default { decorateDisabledEditor(editorState) { /** Disable toolbar and editor by pointer events styling */ if (editorState) { - this.$refs.trix.toolbarElement.style['pointer-events'] = 'none' + this.$refs.trix.disabled = true this.$refs.trix.contentEditable = false - this.$refs.trix.style['background'] = '#e9ecef' + this.$refs.trix.style['pointer-events'] = 'none' + this.$refs.trix.style['background-color'] = '#444' + this.$refs.trix.style['color'] = '#bbb' } else { - this.$refs.trix.toolbarElement.style['pointer-events'] = 'unset' + this.$refs.trix.disabled = false + this.$refs.trix.contentEditable = true this.$refs.trix.style['pointer-events'] = 'unset' - this.$refs.trix.style['background'] = 'transparent' + this.$refs.trix.style['background-color'] = '' + this.$refs.trix.style['color'] = '' } }, overrideConfig(config) { @@ -250,32 +312,15 @@ export default { } return target }, - enableBreakParagraphOnReturn() { - // Trix works with divs by default, we want paragraphs instead - Trix.config.blockAttributes.default.tagName = 'p' - // Enable break paragraph on Enter (Shift + Enter will still create a line break) - Trix.config.blockAttributes.default.breakOnReturn = true - - // Hack to fix buggy paragraph breaks - // Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942 - Trix.Block.prototype.breaksOnReturn = function () { - const attr = this.getLastAttribute() - const config = Trix.getBlockConfig(attr ? attr : 'default') - return config ? config.breakOnReturn : false - } - Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () { - if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) { - return this.startLocation.offset > 0 - } else { - return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false - } + blur() { + if (this.$refs.trix && this.$refs.trix.blur) { + this.$refs.trix.blur() } } }, mounted() { /** Override editor configuration */ this.overrideConfig(this.config) - this.enableBreakParagraphOnReturn() /** Check if editor read-only mode is required */ this.decorateDisabledEditor(this.disabledEditor) this.$nextTick(() => { @@ -305,4 +350,12 @@ export default { .trix_container .trix-content { background-color: white; } +trix-editor { + max-height: calc(4 * 1lh); + overflow-y: auto; +} + +trix-editor * { + pointer-events: inherit; +} diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue index 5fbcaa20..fa26bcf5 100644 --- a/client/components/widgets/BookDetailsEdit.vue +++ b/client/components/widgets/BookDetailsEdit.vue @@ -26,7 +26,7 @@
- +
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index a0cadc1d..714e326c 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -123,7 +123,7 @@
-

{{ description }}

+

@@ -804,8 +804,7 @@ export default { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; - max-height: 6.25rem; - transition: all 0.3s ease-in-out; + max-height: calc(6 * 1lh); } #item-description.show-full { -webkit-line-clamp: unset; diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js index f4323094..8fde7bc4 100644 --- a/server/finders/BookFinder.js +++ b/server/finders/BookFinder.js @@ -8,6 +8,7 @@ const AudiobookCovers = require('../providers/AudiobookCovers') const CustomProviderAdapter = require('../providers/CustomProviderAdapter') const Logger = require('../Logger') const { levenshteinDistance, escapeRegExp } = require('../utils/index') +const htmlSanitizer = require('../utils/htmlSanitizer') class BookFinder { #providerResponseTimeout = 30000 @@ -463,6 +464,12 @@ class BookFinder { } else { books = await this.getGoogleBooksResults(title, author) } + books.forEach((book) => { + if (book.description) { + book.description = htmlSanitizer.sanitize(book.description) + book.descriptionPlain = htmlSanitizer.stripAllTags(book.description) + } + }) return books } diff --git a/server/models/Book.js b/server/models/Book.js index 5a4eee54..4f7d1269 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -2,6 +2,7 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const parseNameString = require('../utils/parsers/parseNameString') +const htmlSanitizer = require('../utils/htmlSanitizer') /** * @typedef EBookFileObject @@ -343,6 +344,7 @@ class Book extends Model { publishedDate: this.publishedDate, publisher: this.publisher, description: this.description, + descriptionPlain: this.description ? htmlSanitizer.stripAllTags(this.description) : null, isbn: this.isbn, asin: this.asin, language: this.language, @@ -542,6 +544,7 @@ class Book extends Model { publishedDate: this.publishedDate, publisher: this.publisher, description: this.description, + descriptionPlain: this.description ? htmlSanitizer.stripAllTags(this.description) : null, isbn: this.isbn, asin: this.asin, language: this.language, @@ -564,6 +567,7 @@ class Book extends Model { publishedDate: this.publishedDate, publisher: this.publisher, description: this.description, + descriptionPlain: this.description ? htmlSanitizer.stripAllTags(this.description) : null, isbn: this.isbn, asin: this.asin, language: this.language, diff --git a/server/providers/Audible.js b/server/providers/Audible.js index 505b8f0e..e6816082 100644 --- a/server/providers/Audible.js +++ b/server/providers/Audible.js @@ -1,5 +1,4 @@ const axios = require('axios').default -const htmlSanitizer = require('../utils/htmlSanitizer') const Logger = require('../Logger') const { isValidASIN } = require('../utils/index') @@ -68,7 +67,7 @@ class Audible { narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null, publisher: publisherName, publishedYear: releaseDate ? releaseDate.split('-')[0] : null, - description: summary ? htmlSanitizer.stripAllTags(summary) : null, + description: summary || null, cover: image, asin, genres: genresFiltered.length ? genresFiltered : null, diff --git a/server/providers/iTunes.js b/server/providers/iTunes.js index 1ec051d1..57a47d0d 100644 --- a/server/providers/iTunes.js +++ b/server/providers/iTunes.js @@ -112,7 +112,7 @@ class iTunes { artistId: data.artistId, title: data.collectionName, author, - description: htmlSanitizer.stripAllTags(data.description || ''), + description: data.description || null, publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null, genres: data.primaryGenreName ? [data.primaryGenreName] : null, cover: this.getCoverArtwork(data) diff --git a/server/utils/htmlSanitizer.js b/server/utils/htmlSanitizer.js index 68d92c85..cab92392 100644 --- a/server/utils/htmlSanitizer.js +++ b/server/utils/htmlSanitizer.js @@ -1,11 +1,9 @@ const sanitizeHtml = require('../libs/sanitizeHtml') -const { entities } = require("./htmlEntities"); +const { entities } = require('./htmlEntities') function sanitize(html) { const sanitizerOptions = { - allowedTags: [ - 'p', 'ol', 'ul', 'li', 'a', 'strong', 'em', 'del', 'br' - ], + allowedTags: ['p', 'ol', 'ul', 'li', 'a', 'strong', 'em', 'del', 'br', 'b', 'i'], disallowedTagsMode: 'discard', allowedAttributes: { a: ['href', 'name', 'target'] @@ -34,6 +32,6 @@ function decodeHTMLEntities(strToDecode) { if (entity in entities) { return entities[entity] } - return entity; + return entity }) } From 598a93d224d4f94610cc321405f1b8ddbce447a4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 22 Jan 2025 17:56:46 -0600 Subject: [PATCH 02/43] Update copy to clipboard buttons to be standardized --- .../components/modals/AudioFileDataModal.vue | 16 +++++++++------ client/components/modals/ShareModal.vue | 2 +- .../modals/rssfeed/OpenCloseModal.vue | 13 ++---------- .../modals/rssfeed/ViewFeedModal.vue | 17 +++------------- client/components/ui/TextInput.vue | 20 +++++++++++-------- client/components/ui/TextInputWithLabel.vue | 5 +++-- client/pages/config/users/_id/index.vue | 9 +-------- client/plugins/init.client.js | 4 +--- 8 files changed, 33 insertions(+), 53 deletions(-) diff --git a/client/components/modals/AudioFileDataModal.vue b/client/components/modals/AudioFileDataModal.vue index 7e33a980..eb70f1c3 100644 --- a/client/components/modals/AudioFileDataModal.vue +++ b/client/components/modals/AudioFileDataModal.vue @@ -90,8 +90,8 @@
-
@@ -113,14 +113,13 @@ export default { return { probingFile: false, ffprobeData: null, - copiedToClipboard: false + hasCopied: null } }, watch: { show(newVal) { if (newVal) { this.ffprobeData = null - this.copiedToClipboard = false this.probingFile = false } } @@ -165,8 +164,13 @@ export default { this.probingFile = false }) }, - async copyFfprobeData() { - this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData) + copyToClipboard() { + clearTimeout(this.hasCopied) + this.$copyToClipboard(this.prettyFfprobeData).then((success) => { + this.hasCopied = setTimeout(() => { + this.hasCopied = null + }, 2000) + }) } }, mounted() {} diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index 5b379884..0ae65ec6 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -16,7 +16,7 @@ @@ -47,7 +47,7 @@ export default { showPassword: false, isHovering: false, isFocused: false, - hasCopied: false, + hasCopied: null, isInvalidDate: false } }, @@ -62,7 +62,12 @@ export default { }, classList() { var _list = [] - _list.push(`px-${this.paddingX}`) + if (this.showCopy) { + _list.push('pl-3', 'pr-8') + } else { + _list.push(`px-${this.paddingX}`) + } + _list.push(`py-${this.paddingY}`) if (this.noSpinner) _list.push('no-spinner') if (this.textCenter) _list.push('text-center') @@ -80,11 +85,10 @@ export default { }, methods: { copyToClipboard() { - if (this.hasCopied) return + clearTimeout(this.hasCopied) this.$copyToClipboard(this.inputValue).then((success) => { - this.hasCopied = success - setTimeout(() => { - this.hasCopied = false + this.hasCopied = setTimeout(() => { + this.hasCopied = null }, 2000) }) }, diff --git a/client/components/ui/TextInputWithLabel.vue b/client/components/ui/TextInputWithLabel.vue index ee9ffb7a..a10394bd 100644 --- a/client/components/ui/TextInputWithLabel.vue +++ b/client/components/ui/TextInputWithLabel.vue @@ -6,7 +6,7 @@ {{ note }} - +
@@ -23,7 +23,8 @@ export default { }, readonly: Boolean, disabled: Boolean, - inputClass: String + inputClass: String, + showCopy: Boolean }, data() { return {} diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue index d19337af..fbef359b 100644 --- a/client/pages/config/users/_id/index.vue +++ b/client/pages/config/users/_id/index.vue @@ -14,11 +14,7 @@

{{ username }}

- - -
- content_copy -
+
@@ -140,9 +136,6 @@ export default { } }, methods: { - copyToClipboard(str) { - this.$copyToClipboard(str, this) - }, async init() { this.listeningSessions = await this.$axios .$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`) diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 984ec9d0..015cd919 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -128,12 +128,11 @@ Vue.prototype.$sanitizeSlug = (str) => { return str } -Vue.prototype.$copyToClipboard = (str, ctx) => { +Vue.prototype.$copyToClipboard = (str) => { return new Promise((resolve) => { if (navigator.clipboard) { navigator.clipboard.writeText(str).then( () => { - if (ctx) ctx.$toast.success('Copied to clipboard') resolve(true) }, (err) => { @@ -152,7 +151,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => { document.execCommand('copy') document.body.removeChild(el) - if (ctx) ctx.$toast.success('Copied to clipboard') resolve(true) } }) From 9fbf57bbefeb0ed5074a392e0b86bf680ec5055c Mon Sep 17 00:00:00 2001 From: adjokic <15988225+adjokic@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:10:38 -0600 Subject: [PATCH 03/43] Update README on using websockets with Apache as a reverse proxy --- readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/readme.md b/readme.md index 19ede3ce..34f770c5 100644 --- a/readme.md +++ b/readme.md @@ -165,6 +165,15 @@ For this to work you must enable at least the following mods using `a2enmod`: ``` +If using Apache >= 2.4.47 you can use the following, without having to use any of the `RewriteEngine`, `RewriteCond`, or `RewriteRule` directives. For example: +```xml + + ProxyPreserveHost on + ProxyPass http://localhost:/audiobookshelf upgrade=websocket + ProxyPassReverse http://localhost:/audiobookshelf + +``` + Some SSL certificates like those signed by Let's Encrypt require ACME validation. To allow Let's Encrypt to write and confirm the ACME challenge, edit your VirtualHost definition to prevent proxying traffic that queries `/.well-known` and instead serve that directly: ```bash From 79acc41d1617fd550fcfbeabe812b495c5009f20 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 23 Jan 2025 17:49:58 -0600 Subject: [PATCH 04/43] Add populate from buttons to batch edit --- client/pages/batch/index.vue | 94 +++++++++++++++++++++++++++++++++++- client/strings/en-us.json | 4 ++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue index 5cc83176..263dee58 100644 --- a/client/pages/batch/index.vue +++ b/client/pages/batch/index.vue @@ -86,7 +86,12 @@
-
+
+ {{ $strings.ButtonReset }} + + {{ $strings.ButtonBatchEditPopulateFromExisting }} + +
{{ $strings.ButtonApply }}
@@ -97,6 +102,11 @@
@@ -35,7 +38,9 @@ export default { { text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 } ], jumpForwardAmount: 10, - jumpBackwardAmount: 10 + jumpBackwardAmount: 10, + playbackRateIncrementDecrementValues: [0.1, 0.05], + playbackRateIncrementDecrement: 0.1 } }, computed: { @@ -60,10 +65,15 @@ export default { this.jumpBackwardAmount = val this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val }) }, + setPlaybackRateIncrementDecrementAmount(val) { + this.playbackRateIncrementDecrement = val + this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: val }) + }, settingsUpdated() { this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') + this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement') } }, mounted() { From 1ea1e60d4bcb088279eb5a2e762d1ca804605c13 Mon Sep 17 00:00:00 2001 From: Greg Lorenzen Date: Sat, 25 Jan 2025 01:58:48 +0000 Subject: [PATCH 26/43] Add string for LabelPlaybackRateIncrementDecrement --- client/strings/en-us.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b4ac1389..01d94ac6 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -485,6 +485,7 @@ "LabelPermissionsUpload": "Can Upload", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlaybackRateIncrementDecrement": "Playback Rate Increment/Decrement Amount", "LabelPlayMethod": "Play Method", "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", From f258782e2e00fc8c68a901bb93d0ffab37f3c7eb Mon Sep 17 00:00:00 2001 From: Greg Lorenzen Date: Sat, 25 Jan 2025 01:59:24 +0000 Subject: [PATCH 27/43] Handle playback rate increment and decrmenet value in UI --- .../controls/PlaybackSpeedControl.vue | 20 +++++++++++-------- client/components/player/PlayerUi.vue | 9 ++++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/client/components/controls/PlaybackSpeedControl.vue b/client/components/controls/PlaybackSpeedControl.vue index 9e9f0d54..2c910547 100644 --- a/client/components/controls/PlaybackSpeedControl.vue +++ b/client/components/controls/PlaybackSpeedControl.vue @@ -33,6 +33,10 @@ export default { value: { type: [String, Number], default: 1 + }, + playbackRateIncrementDecrement: { + type: Number, + default: 0.1 } }, data() { @@ -58,10 +62,10 @@ export default { return [0.5, 1, 1.2, 1.5, 2] }, canIncrement() { - return this.playbackRate + 0.1 <= this.MAX_SPEED + return this.playbackRate + this.playbackRateIncrementDecrement <= this.MAX_SPEED }, canDecrement() { - return this.playbackRate - 0.1 >= this.MIN_SPEED + return this.playbackRate - this.playbackRateIncrementDecrement >= this.MIN_SPEED } }, methods: { @@ -73,14 +77,14 @@ export default { this.$nextTick(() => this.setShowMenu(false)) }, increment() { - if (this.playbackRate + 0.1 > this.MAX_SPEED) return - var newPlaybackRate = this.playbackRate + 0.1 - this.playbackRate = Number(newPlaybackRate.toFixed(1)) + if (this.playbackRate + this.playbackRateIncrementDecrement > this.MAX_SPEED) return + var newPlaybackRate = this.playbackRate + this.playbackRateIncrementDecrement + this.playbackRate = Number(newPlaybackRate.toFixed(2)) }, decrement() { - if (this.playbackRate - 0.1 < this.MIN_SPEED) return - var newPlaybackRate = this.playbackRate - 0.1 - this.playbackRate = Number(newPlaybackRate.toFixed(1)) + if (this.playbackRate - this.playbackRateIncrementDecrement < this.MIN_SPEED) return + var newPlaybackRate = this.playbackRate - this.playbackRateIncrementDecrement + this.playbackRate = Number(newPlaybackRate.toFixed(2)) }, updateMenuPositions() { if (!this.$refs.wrapper) return diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index 31267c7a..f4ad59d1 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -2,7 +2,7 @@
- +
-

+

+
From 9b4732c207c092711137afb13477181d9cac7b1a Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 26 Jan 2025 12:21:54 +0200 Subject: [PATCH 30/43] Add bookSeries id attribute to findAllExpandedWhere --- server/models/LibraryItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index d581c309..3ed4e31e 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -155,7 +155,7 @@ class LibraryItem extends Model { { model: this.sequelize.models.series, through: { - attributes: ['sequence'] + attributes: ['id', 'sequence'] } } ] From 23067e1818751dcc8aed9087745db266e846b0be Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 26 Jan 2025 13:44:57 +0200 Subject: [PATCH 31/43] Allows setting of some pragma values through environment variables --- server/Database.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/Database.js b/server/Database.js index 82a8fbd1..c9e2d52a 100644 --- a/server/Database.js +++ b/server/Database.js @@ -226,6 +226,28 @@ class Database { try { await this.sequelize.authenticate() + + // Set SQLite pragmas from environment variables + const allowedPragmas = [ + { name: 'mmap_size', env: 'SQLITE_MMAP_SIZE' }, + { name: 'cache_size', env: 'SQLITE_CACHE_SIZE' }, + { name: 'temp_store', env: 'SQLITE_TEMP_STORE' } + ] + + for (const pragma of allowedPragmas) { + const value = process.env[pragma.env] + if (value !== undefined) { + try { + Logger.info(`[Database] Running "PRAGMA ${pragma.name} = ${value}"`) + await this.sequelize.query(`PRAGMA ${pragma.name} = ${value}`) + const [result] = await this.sequelize.query(`PRAGMA ${pragma.name}`) + Logger.debug(`[Database] "PRAGMA ${pragma.name}" query result:`, result) + } catch (error) { + Logger.error(`[Database] Failed to set SQLite pragma ${pragma.name}`, error) + } + } + } + if (process.env.NUSQLITE3_PATH) { await this.loadExtension(process.env.NUSQLITE3_PATH) Logger.info(`[Database] Db supports unaccent and unicode foldings`) From 558173e086298d63dd99e39cfeb8a57109f14c52 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Jan 2025 10:51:18 -0600 Subject: [PATCH 32/43] Update custom metadata provider results to sanitize html descriptions #3880 --- server/libs/sanitizeHtml/index.js | 112 ---------------------- server/providers/CustomProviderAdapter.js | 3 +- server/utils/htmlSanitizer.js | 10 ++ 3 files changed, 12 insertions(+), 113 deletions(-) diff --git a/server/libs/sanitizeHtml/index.js b/server/libs/sanitizeHtml/index.js index 3fee985e..701a36f2 100644 --- a/server/libs/sanitizeHtml/index.js +++ b/server/libs/sanitizeHtml/index.js @@ -7,12 +7,6 @@ */ const htmlparser = require('htmlparser2'); -// const escapeStringRegexp = require('escape-string-regexp'); -// const { isPlainObject } = require('is-plain-object'); -// const deepmerge = require('deepmerge'); -// const parseSrcset = require('parse-srcset'); -// const { parse: postcssParse } = require('postcss'); -// Tags that can conceivably represent stand-alone media. // ABS UPDATE: Packages not necessary // SOURCE: https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js @@ -76,17 +70,6 @@ function has(obj, key) { return ({}).hasOwnProperty.call(obj, key); } -// Returns those elements of `a` for which `cb(a)` returns truthy -function filter(a, cb) { - const n = []; - each(a, function (v) { - if (cb(v)) { - n.push(v); - } - }); - return n; -} - function isEmptyObject(obj) { for (const key in obj) { if (has(obj, key)) { @@ -96,21 +79,6 @@ function isEmptyObject(obj) { return true; } -function stringifySrcset(parsedSrcset) { - return parsedSrcset.map(function (part) { - if (!part.url) { - throw new Error('URL missing'); - } - - return ( - part.url + - (part.w ? ` ${part.w}w` : '') + - (part.h ? ` ${part.h}h` : '') + - (part.d ? ` ${part.d}x` : '') - ); - }).join(', '); -} - module.exports = sanitizeHtml; // A valid attribute name. @@ -714,86 +682,6 @@ function sanitizeHtml(html, options, _recursing) { return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1; } - /** - * Filters user input css properties by allowlisted regex attributes. - * Modifies the abstractSyntaxTree object. - * - * @param {object} abstractSyntaxTree - Object representation of CSS attributes. - * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }. - * @param {object} allowedStyles - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i). - * @return {object} - The modified tree. - */ - // function filterCss(abstractSyntaxTree, allowedStyles) { - // if (!allowedStyles) { - // return abstractSyntaxTree; - // } - - // const astRules = abstractSyntaxTree.nodes[0]; - // let selectedRule; - - // // Merge global and tag-specific styles into new AST. - // if (allowedStyles[astRules.selector] && allowedStyles['*']) { - // selectedRule = deepmerge( - // allowedStyles[astRules.selector], - // allowedStyles['*'] - // ); - // } else { - // selectedRule = allowedStyles[astRules.selector] || allowedStyles['*']; - // } - - // if (selectedRule) { - // abstractSyntaxTree.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []); - // } - - // return abstractSyntaxTree; - // } - - /** - * Extracts the style attributes from an AbstractSyntaxTree and formats those - * values in the inline style attribute format. - * - * @param {AbstractSyntaxTree} filteredAST - * @return {string} - Example: "color:yellow;text-align:center !important;font-family:helvetica;" - */ - function stringifyStyleAttributes(filteredAST) { - return filteredAST.nodes[0].nodes - .reduce(function (extractedAttributes, attrObject) { - extractedAttributes.push( - `${attrObject.prop}:${attrObject.value}${attrObject.important ? ' !important' : ''}` - ); - return extractedAttributes; - }, []) - .join(';'); - } - - /** - * Filters the existing attributes for the given property. Discards any attributes - * which don't match the allowlist. - * - * @param {object} selectedRule - Example: { color: red, font-family: helvetica } - * @param {array} allowedDeclarationsList - List of declarations which pass the allowlist. - * @param {object} attributeObject - Object representing the current css property. - * @property {string} attributeObject.type - Typically 'declaration'. - * @property {string} attributeObject.prop - The CSS property, i.e 'color'. - * @property {string} attributeObject.value - The corresponding value to the css property, i.e 'red'. - * @return {function} - When used in Array.reduce, will return an array of Declaration objects - */ - function filterDeclarations(selectedRule) { - return function (allowedDeclarationsList, attributeObject) { - // If this property is allowlisted... - if (has(selectedRule, attributeObject.prop)) { - const matchesRegex = selectedRule[attributeObject.prop].some(function (regularExpression) { - return regularExpression.test(attributeObject.value); - }); - - if (matchesRegex) { - allowedDeclarationsList.push(attributeObject); - } - } - return allowedDeclarationsList; - }; - } - function filterClasses(classes, allowed, allowedGlobs) { if (!allowed) { // The class attribute is allowed without filtering on this tag diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js index fe6537fd..911a09e9 100644 --- a/server/providers/CustomProviderAdapter.js +++ b/server/providers/CustomProviderAdapter.js @@ -1,6 +1,7 @@ const axios = require('axios').default const Database = require('../Database') const Logger = require('../Logger') +const htmlSanitizer = require('../utils/htmlSanitizer') class CustomProviderAdapter { #responseTimeout = 30000 @@ -74,7 +75,7 @@ class CustomProviderAdapter { narrator, publisher, publishedYear, - description, + description: htmlSanitizer.sanitize(description), cover, isbn, asin, diff --git a/server/utils/htmlSanitizer.js b/server/utils/htmlSanitizer.js index cab92392..4ed30e72 100644 --- a/server/utils/htmlSanitizer.js +++ b/server/utils/htmlSanitizer.js @@ -1,7 +1,17 @@ const sanitizeHtml = require('../libs/sanitizeHtml') const { entities } = require('./htmlEntities') +/** + * + * @param {string} html + * @returns {string} + * @throws {Error} if input is not a string + */ function sanitize(html) { + if (typeof html !== 'string') { + throw new Error('sanitizeHtml: input must be a string') + } + const sanitizerOptions = { allowedTags: ['p', 'ol', 'ul', 'li', 'a', 'strong', 'em', 'del', 'br', 'b', 'i'], disallowedTagsMode: 'discard', From e701a0a32e6d2ab1d51fbedd73c4bedcd12e90bf Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 27 Jan 2025 16:46:32 -0600 Subject: [PATCH 33/43] Update playback rate display value number of decimals --- client/components/controls/PlaybackSpeedControl.vue | 11 +++++++++-- client/strings/en-us.json | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/components/controls/PlaybackSpeedControl.vue b/client/components/controls/PlaybackSpeedControl.vue index 2c910547..98f40866 100644 --- a/client/components/controls/PlaybackSpeedControl.vue +++ b/client/components/controls/PlaybackSpeedControl.vue @@ -1,7 +1,7 @@
-
-

{{ $strings.MessageNoCollections }}

+
+
+

{{ $strings.MessageNoUserPlaylists }}

+

+ {{ $strings.MessageBookshelfNoCollectionsHelp }} + + + help_outline + + +

+
+
diff --git a/client/components/modals/playlists/AddCreateModal.vue b/client/components/modals/playlists/AddCreateModal.vue index d1f910b8..51f7e475 100644 --- a/client/components/modals/playlists/AddCreateModal.vue +++ b/client/components/modals/playlists/AddCreateModal.vue @@ -19,8 +19,18 @@
-
-

{{ $strings.MessageNoUserPlaylists }}

+
+
+

{{ $strings.MessageNoUserPlaylists }}

+

+ {{ $strings.MessageNoUserPlaylistsHelp }} + + + help_outline + + +

+
From c3aad9486c6036c6fcc30102a2e988a8ae198fbd Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 30 Jan 2025 17:27:32 -0600 Subject: [PATCH 42/43] Fix Logger.fatal to be a promise to ensure crash_logs.txt write --- server/Logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Logger.js b/server/Logger.js index 5d1a7fa5..e4487f0a 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -117,7 +117,7 @@ class Logger { if (level < LogLevel.FATAL && level < this.logLevel) return const consoleMethod = Logger.ConsoleMethods[levelName] console[consoleMethod](`[${this.timestamp}] ${levelName}:`, ...args) - this.#logToFileAndListeners(level, levelName, args, source) + return this.#logToFileAndListeners(level, levelName, args, source) } trace(...args) { @@ -141,7 +141,7 @@ class Logger { } fatal(...args) { - this.#log('FATAL', this.source, ...args) + return this.#log('FATAL', this.source, ...args) } note(...args) { From 2e13c5bd5021adf37ba63e3c16e2e5647996240d Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 30 Jan 2025 17:47:41 -0600 Subject: [PATCH 43/43] Fix no collections message, ui updates --- .../components/modals/collections/AddCreateModal.vue | 12 ++++++------ .../components/modals/playlists/AddCreateModal.vue | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/components/modals/collections/AddCreateModal.vue b/client/components/modals/collections/AddCreateModal.vue index ee465187..c4878adc 100644 --- a/client/components/modals/collections/AddCreateModal.vue +++ b/client/components/modals/collections/AddCreateModal.vue @@ -19,20 +19,20 @@
-
+
-

{{ $strings.MessageNoUserPlaylists }}

-

- {{ $strings.MessageBookshelfNoCollectionsHelp }} +

{{ $strings.MessageNoCollections }}

+
+

{{ $strings.MessageBookshelfNoCollectionsHelp }}

help_outline -

+
-
+
diff --git a/client/components/modals/playlists/AddCreateModal.vue b/client/components/modals/playlists/AddCreateModal.vue index 51f7e475..4a7daad9 100644 --- a/client/components/modals/playlists/AddCreateModal.vue +++ b/client/components/modals/playlists/AddCreateModal.vue @@ -19,17 +19,17 @@
-
+
-

{{ $strings.MessageNoUserPlaylists }}

-

- {{ $strings.MessageNoUserPlaylistsHelp }} +

{{ $strings.MessageNoUserPlaylists }}

+
+

{{ $strings.MessageNoUserPlaylistsHelp }}

help_outline -

+