From e018f8341e3df69f20c06d1841296bd5c650d230 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Tue, 21 Mar 2023 13:27:21 +0100 Subject: [PATCH 01/51] EPUB progress persistence --- client/components/readers/EpubReader.vue | 181 +++++++++++----------- client/components/readers/EpubReader2.vue | 88 ----------- client/components/readers/Reader.vue | 3 +- 3 files changed, 89 insertions(+), 183 deletions(-) delete mode 100644 client/components/readers/EpubReader2.vue diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 9c19c3ec..47678c17 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -2,128 +2,123 @@
- chevron_left + chevron_left
-
+
- -
-

{{ progress }}%

-
- chevron_right + chevron_right
diff --git a/client/components/readers/EpubReader2.vue b/client/components/readers/EpubReader2.vue deleted file mode 100644 index 69839d78..00000000 --- a/client/components/readers/EpubReader2.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index 5481f250..c8d5c26b 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -37,8 +37,7 @@ export default { } }, componentName() { - if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2' - else if (this.ebookType === 'epub') return 'readers-epub-reader' + if (this.ebookType === 'epub') return 'readers-epub-reader' else if (this.ebookType === 'mobi') return 'readers-mobi-reader' else if (this.ebookType === 'pdf') return 'readers-pdf-reader' else if (this.ebookType === 'comic') return 'readers-comic-reader' From 17b8cf19b7e02305894876921c8aedc3bc309ba9 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Tue, 21 Mar 2023 13:34:21 +0100 Subject: [PATCH 02/51] Add Location Storage --- client/components/readers/EpubReader.vue | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 47678c17..6fd652f8 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -107,18 +107,29 @@ export default { height: window.innerHeight * 0.9 }); - reader.rendition.display(cfi); + reader.rendition.display(this.userMediaProgress?.currentTime); reader.book.ready.then(() => { reader.rendition.on('relocated', reader.relocated); reader.rendition.on('keydown', reader.keyUp) document.addEventListener('keydown', reader.keyUp, false); - reader.book.locations.generate(); + if (reader.userMediaProgress?.duration) { + reader.book.locations.load(reader.userMediaProgress.duration) + } else { + reader.book.locations.generate().then(() => { + var updatePayload = { + duration: reader.book.locations.save(), + } + this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload).catch((error) => { + console.error('Failed', error) + }) + }); + } }); }, }, mounted() { - this.initEpub(this.userMediaProgress?.currentTime); + this.initEpub(); }, }; From 6c618d77604ccf2e7f744d9eb17128912cb97b55 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Tue, 21 Mar 2023 13:36:06 +0100 Subject: [PATCH 03/51] Adjust height to fit metadata --- client/components/readers/EpubReader.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 6fd652f8..d60e41f4 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -6,7 +6,7 @@ class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left
-
+
@@ -104,7 +104,7 @@ export default { /** @type {ePub.Rendition} */ reader.rendition = reader.book.renderTo("viewer", { width: window.innerWidth - 200, - height: window.innerHeight * 0.9 + height: window.innerHeight * 0.8 }); reader.rendition.display(this.userMediaProgress?.currentTime); From 5078818295547dba0cb3662eef12131e73312667 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Wed, 22 Mar 2023 11:16:01 +0100 Subject: [PATCH 04/51] Add MediaProgress fields Add Table of Contents --- client/components/readers/EpubReader.vue | 105 ++++++++++++----------- client/components/readers/Reader.vue | 58 +++++++++++-- server/objects/user/MediaProgress.js | 11 ++- 3 files changed, 114 insertions(+), 60 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index d60e41f4..72205dcd 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -2,15 +2,16 @@
- chevron_left
-
+
- chevron_right
@@ -21,7 +22,7 @@ import ePub from "epubjs"; /** - * @typedef {EpubReader} + * @typedef {object} EpubReader * @property {ePub.Book} book * @property {ePub.Rendition} rendition */ @@ -30,7 +31,7 @@ export default { url: String, libraryItem: { type: Object, - default: () => {} + default: () => { } } }, data() { @@ -42,61 +43,59 @@ export default { }; }, computed: { - libraryItemId() { return this.libraryItem ? this.libraryItem.id : null }, - hasPrev() { return !this.rendition?.location.atStart }, - hasNext() { return !this.rendition?.location.atEnd }, + /** @returns {string} */ + libraryItemId() { return this.libraryItem?.id }, + hasPrev() { return !this.rendition?.location?.atStart }, + hasNext() { return !this.rendition?.location?.atEnd }, + /** @returns {Array} */ chapters() { return this.book ? this.book.navigation.toc : [] }, - title() { return this.book ? this.book.metadata.title : "" }, - author() { return this.book ? this.book.metadata.creator : "" }, userMediaProgress() { if (!this.libraryItemId) return return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) }, }, methods: { - changedChapter() { this.rendition?.display(this.selectedChapter) }, - prev() { this.rendition?.prev() }, - next() { this.rendition?.next() }, + prev() { return this.rendition?.prev() }, + next() { return this.rendition?.next() }, + goToChapter(href) { return this.rendition?.display(href) }, keyUp(e) { + const rtl = this.book.package.metadata.direction === 'rtl' if ((e.keyCode || e.which) == 37) { - this.prev(); + return rtl ? this.next() : this.prev(); } else if ((e.keyCode || e.which) == 39) { - this.next(); + return rtl ? this.prev() : this.next(); } }, + /** + * @param {object} payload + * @param {string} payload.ebookLocation - CFI of the current location + * @param {string} payload.ebookLocations - list of CFI tags + * @param {number} payload.progress - Progress Percentage + */ + updateProgress(payload) { + this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { + console.error('EpubReader.updateProgress failed:', error) + }) + }, + /** @param {string} location - CFI of the new location */ relocated(location) { - var cfi = location.start.cfi; - var cfiFragment = "#" + cfi; - - if(window.location.hash != cfiFragment) { - const url = new URL(window.location); - url.hash = cfiFragment; - history.pushState({}, '', url); - - var updatePayload = { - currentTime: cfi, - } - - var percentage = this.book.locations.percentageFromCfi(cfi); - if (percentage) { - updatePayload.progress = percentage - } - - this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload).catch((error) => { - console.error('Failed', error) - }) + if (location.end.percentage) { + this.updateProgress({ + ebookLocation: location.start.cfi, + progress: location.end.percentage, + }); + } else { + this.updateProgress({ + ebookLocation: location.start.cfi, + }); } }, - initEpub(cfi) { + initEpub() { + /** @type {EpubReader} */ var reader = this; /** @type {ePub.Book} */ reader.book = new ePub(reader.url, { - storage: false, - worker: false, - manager: "continuous", - flow: "scrolled", - spreads: false, width: window.innerWidth - 200, height: window.innerHeight - 50, }); @@ -107,23 +106,27 @@ export default { height: window.innerHeight * 0.8 }); - reader.rendition.display(this.userMediaProgress?.currentTime); + // load saved progress + reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start); + + // load style + reader.rendition.themes.default({ "*": { "color": "#fff!important" } }); + reader.book.ready.then(() => { + // set up event listeners reader.rendition.on('relocated', reader.relocated); reader.rendition.on('keydown', reader.keyUp) document.addEventListener('keydown', reader.keyUp, false); - if (reader.userMediaProgress?.duration) { - reader.book.locations.load(reader.userMediaProgress.duration) + // load ebook cfi locations + if (this.userMediaProgress?.ebookLocations) { + reader.book.locations.load(this.userMediaProgress?.ebookLocations) } else { reader.book.locations.generate().then(() => { - var updatePayload = { - duration: reader.book.locations.save(), - } - this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload).catch((error) => { - console.error('Failed', error) - }) - }); + this.updateProgress({ + ebookLocations: reader.book.locations.save(), + }); + }); } }); }, diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index c8d5c26b..1d7ef6d6 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -1,24 +1,52 @@ diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index 1d7ef6d6..a7383fb7 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -1,13 +1,11 @@ @@ -145,13 +141,10 @@ export default { }, methods: { toggleToC() { - this.tocOpen = !this.tocOpen; - this.chapters = this.$refs.readerComponent.chapters; - this.$refs.tocContainer.classList.toggle('invisible') - this.$refs.tocOverlay.classList.toggle('opacity-0') - this.$refs.tocOverlay.classList.toggle('opacity-50') + this.tocOpen = !this.tocOpen + this.chapters = this.$refs.readerComponent.chapters }, - openSettings() { }, + openSettings() {}, hotkey(action) { console.log('Reader hotkey', action) if (!this.$refs.readerComponent) return @@ -192,4 +185,8 @@ export default { .ebook-viewer { height: calc(100% - 96px); } +.tocContent { + height: calc(100% - 36px); + overflow-y: auto; +} \ No newline at end of file diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 988b89cc..dfcdf38b 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -156,7 +156,7 @@

{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%

{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}

-

{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}

+

{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}

{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}

@@ -471,8 +471,13 @@ export default { const duration = this.userMediaProgress.duration || this.duration return duration - this.userMediaProgress.currentTime }, + useEBookProgress() { + if (!this.userMediaProgress || this.userMediaProgress.progress) return false + return this.userMediaProgress.ebookProgress > 0 + }, progressPercent() { - return this.userMediaProgress ? Math.max(Math.min(1, Math.max(this.userMediaProgress.progress || 0, this.userMediaProgress.ebookProgress || 0)), 0) : 0 + if (this.useEBookProgress) return Math.max(Math.min(1, this.userMediaProgress.ebookProgress), 0) + return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0 }, userProgressStartedAt() { return this.userMediaProgress ? this.userMediaProgress.startedAt : 0 From a99257e758864837bb09f0bebd5298ec29735519 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 25 Mar 2023 14:07:35 -0500 Subject: [PATCH 07/51] Fix getAllLibraryItemsInProgress route --- server/controllers/MeController.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 9ca2c7a9..9697ebe7 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -256,13 +256,13 @@ class MeController { } // GET: api/me/items-in-progress - async getAllLibraryItemsInProgress(req, res) { + getAllLibraryItemsInProgress(req, res) { const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25 - var itemsInProgress = [] + let itemsInProgress = [] for (const mediaProgress of req.user.mediaProgress) { - if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || libraryItem.ebookProgress > 0)) { - const libraryItem = await this.db.getLibraryItem(mediaProgress.libraryItemId) + if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) { + const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId) if (libraryItem) { if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') { const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId) From 936de686224e0895e06a158387e96fd2b1d79473 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 25 Mar 2023 15:53:19 -0500 Subject: [PATCH 08/51] Update epub reader only store up to 3MB of locations cache --- client/components/readers/EpubReader.vue | 94 +++++++++++++++++++++--- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 8e7d016c..fd7751c8 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -89,15 +89,90 @@ export default { console.error('EpubReader.updateProgress failed:', error) }) }, + getAllEbookLocationData() { + const locations = [] + let totalSize = 0 // Total in bytes + + for (const key in localStorage) { + if (!localStorage.hasOwnProperty(key) || !key.startsWith('ebookLocations-')) { + continue + } + + try { + const ebookLocations = JSON.parse(localStorage[key]) + if (!ebookLocations.locations) throw new Error('Invalid locations object') + + ebookLocations.key = key + ebookLocations.size = (localStorage[key].length + key.length) * 2 + locations.push(ebookLocations) + totalSize += ebookLocations.size + } catch (error) { + console.error('Failed to parse ebook locations', key, error) + localStorage.removeItem(key) + } + } + + // Sort by oldest lastAccessed first + locations.sort((a, b) => a.lastAccessed - b.lastAccessed) + + return { + locations, + totalSize + } + }, + /** @param {string} locationString */ + checkSaveLocations(locationString) { + const maxSizeInBytes = 3000000 // Allow epub locations to take up to 3MB of space + const newLocationsSize = JSON.stringify({ lastAccessed: Date.now(), locations: locationString }).length * 2 + + // Too large overall + if (newLocationsSize > maxSizeInBytes) { + console.error('Epub locations are too large to store. Size =', newLocationsSize) + return + } + + const ebookLocationsData = this.getAllEbookLocationData() + + let availableSpace = maxSizeInBytes - ebookLocationsData.totalSize + + // Remove epub locations until there is room for locations + while (availableSpace < newLocationsSize && ebookLocationsData.locations.length) { + const oldestLocation = ebookLocationsData.locations.shift() + console.log(`Removing cached locations for epub "${oldestLocation.key}" taking up ${oldestLocation.size} bytes`) + availableSpace += oldestLocation.size + localStorage.removeItem(oldestLocation.key) + } + + console.log(`Cacheing epub locations with key "${this.localStorageLocationsKey}" taking up ${newLocationsSize} bytes`) + this.saveLocations(locationString) + }, /** @param {string} locationString */ saveLocations(locationString) { - localStorage.setItem(this.localStorageLocationsKey, locationString) - }, - hasSavedLocations() { - return localStorage.getItem(this.localStorageLocationsKey) !== null + localStorage.setItem( + this.localStorageLocationsKey, + JSON.stringify({ + lastAccessed: Date.now(), + locations: locationString + }) + ) }, loadLocations() { - return localStorage.getItem(this.localStorageLocationsKey) + const locationsObjString = localStorage.getItem(this.localStorageLocationsKey) + if (!locationsObjString) return null + + const locationsObject = JSON.parse(locationsObjString) + + // Remove invalid location objects + if (!locationsObject.locations) { + console.error('Invalid epub locations stored', this.localStorageLocationsKey) + localStorage.removeItem(this.localStorageLocationsKey) + return null + } + + // Update lastAccessed + this.saveLocations(locationsObject.locations) + + return locationsObject.locations }, /** @param {string} location - CFI of the new location */ relocated(location) { @@ -114,7 +189,7 @@ export default { }, initEpub() { /** @type {EpubReader} */ - var reader = this + const reader = this /** @type {ePub.Book} */ reader.book = new ePub(reader.url, { @@ -141,11 +216,12 @@ export default { document.addEventListener('keydown', reader.keyUp, false) // load ebook cfi locations - if (this.hasSavedLocations()) { - reader.book.locations.load(this.loadLocations()) + const savedLocations = this.loadLocations() + if (savedLocations) { + reader.book.locations.load(savedLocations) } else { reader.book.locations.generate().then(() => { - this.saveLocations(reader.book.locations.save()) + this.checkSaveLocations(reader.book.locations.save()) }) } }) From e248b6d8d8a89730a2b37b457b6198822a1d5da6 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 25 Mar 2023 16:09:41 -0500 Subject: [PATCH 09/51] Fix:Parsing id3 tags case insensitive --- server/utils/prober.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/utils/prober.js b/server/utils/prober.js index 552a2304..92df3f01 100644 --- a/server/utils/prober.js +++ b/server/utils/prober.js @@ -73,7 +73,8 @@ function tryGrabChannelLayout(stream) { function tryGrabTags(stream, ...tags) { if (!stream.tags) return null for (let i = 0; i < tags.length; i++) { - const value = stream.tags[tags[i]] || stream.tags[tags[i].toUpperCase()] + const tagKey = Object.keys(stream.tags).find(t => t.toLowerCase() === tags[i].toLowerCase()) + const value = stream.tags[tagKey] if (value && value.trim()) return value.trim() } return null @@ -161,6 +162,7 @@ function parseTags(format, verbose) { if (verbose) { Logger.debug('Tags', format.tags) } + const tags = { file_tag_encoder: tryGrabTags(format, 'encoder', 'tsse', 'tss'), file_tag_encodedby: tryGrabTags(format, 'encoded_by', 'tenc', 'ten'), From 916fd039ca3a3f18b4f5477c4056d4772130fef7 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Mar 2023 13:40:47 -0500 Subject: [PATCH 10/51] Remove keydown event listener in epub reader --- client/components/readers/EpubReader.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index fd7751c8..8a71bb0a 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -213,7 +213,6 @@ export default { // set up event listeners reader.rendition.on('relocated', reader.relocated) reader.rendition.on('keydown', reader.keyUp) - document.addEventListener('keydown', reader.keyUp, false) // load ebook cfi locations const savedLocations = this.loadLocations() From f2baf3fafd1a92c51c3e9bf11bbf9d0ae0789734 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Mar 2023 13:50:44 -0500 Subject: [PATCH 11/51] Update epub media progress update --- client/components/readers/EpubReader.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 8a71bb0a..351850e2 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -176,6 +176,10 @@ export default { }, /** @param {string} location - CFI of the new location */ relocated(location) { + if (this.userMediaProgress?.ebookLocation === location.start.cfi) { + return + } + if (location.end.percentage) { this.updateProgress({ ebookLocation: location.start.cfi, From d2e084449376707ea0ab746ec1657a11238546fa Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 26 Mar 2023 14:44:59 -0500 Subject: [PATCH 12/51] Epub reader updates for mobile --- client/components/readers/EpubReader.vue | 42 +++++++++++++++++++++--- client/components/readers/Reader.vue | 12 +++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 351850e2..20b8f203 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -1,13 +1,13 @@