From 77206d90cb73b39ab43e6a7f89e1b7f598a1a6ff Mon Sep 17 00:00:00 2001 From: korjik Date: Tue, 21 Apr 2026 19:38:34 -0700 Subject: [PATCH] Add OpenAI series evaluation --- client/components/app/BookShelfToolbar.vue | 45 ++ client/components/app/LazyBookshelf.vue | 7 + .../modals/libraries/LibraryTools.vue | 61 +++ client/pages/config/index.vue | 105 ++++- server/controllers/LibraryController.js | 243 ++++++++++ server/controllers/MiscController.js | 30 ++ server/controllers/SeriesController.js | 65 +++ server/objects/settings/ServerSettings.js | 40 +- server/providers/OpenAI.js | 428 ++++++++++++++++++ server/routers/ApiRouter.js | 2 + test/server/providers/OpenAI.test.js | 83 ++++ 11 files changed, 1107 insertions(+), 2 deletions(-) create mode 100644 server/providers/OpenAI.js create mode 100644 test/server/providers/OpenAI.test.js diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index b7ecff62..ea26bbc8 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -134,6 +134,13 @@ export default { } ] + if (this.userCanUpdate && this.openAIConfigured) { + items.push({ + text: 'Organize Story Order With AI', + action: 'organize-story-order-with-ai' + }) + } + if (this.userIsAdminOrUp || this.selectedSeries.rssFeed) { items.push({ text: this.$strings.LabelOpenRSSFeed, @@ -221,6 +228,9 @@ export default { userCanDownload() { return this.$store.getters['user/getUserCanDownload'] }, + openAIConfigured() { + return !!this.$store.getters['getServerSetting']('openAIConfigured') + }, currentLibraryId() { return this.$store.state.libraries.currentLibraryId }, @@ -427,6 +437,12 @@ export default { seriesContextMenuAction({ action }) { if (action === 'open-rss-feed') { this.showOpenSeriesRSSFeed() + } else if (action === 'organize-story-order-with-ai') { + if (this.processingSeries) { + console.warn('Already processing series') + return + } + this.organizeStoryOrderWithAI() } else if (action === 're-add-to-continue-listening') { if (this.processingSeries) { console.warn('Already processing series') @@ -453,6 +469,35 @@ export default { feed: this.selectedSeries.rssFeed }) }, + organizeStoryOrderWithAI() { + const payload = { + message: `Organize "${this.selectedSeries.name}" into AI-detected story order? This will update the series sequence on every book in the series.`, + callback: (confirmed) => { + if (!confirmed) return + + this.processingSeries = true + this.$axios + .$post(`/api/series/${this.seriesId}/organize-story-order`) + .then((data) => { + if (!data.updated) { + this.$toast.info(this.$strings.ToastNoUpdatesNecessary) + } else { + this.$toast.success(`Updated story order for ${data.updated} books`) + } + this.$eventBus.$emit('series-books-updated', { seriesId: this.seriesId }) + }) + .catch((error) => { + console.error('Failed to organize story order with AI', error) + this.$toast.error(error.response?.data || this.$strings.ToastFailedToUpdate) + }) + .finally(() => { + this.processingSeries = false + }) + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, reAddSeriesToContinueListening() { this.processingSeries = true this.$axios diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 4c72d0d7..4cf9d973 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -602,6 +602,11 @@ export default { this.libraryItemUpdated(ab) }) }, + seriesBooksUpdated(payload) { + if (this.entityName !== 'series-books') return + if (payload?.seriesId !== this.seriesId) return + this.resetEntities(this.currScrollTop) + }, collectionAdded(collection) { if (this.entityName !== 'collections') return console.log(`[LazyBookshelf] collectionAdded ${collection.id}`, collection) @@ -791,6 +796,7 @@ export default { this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$on('user-settings', this.settingsUpdated) + this.$eventBus.$on('series-books-updated', this.seriesBooksUpdated) if (this.$root.socket) { this.$root.socket.on('item_updated', this.libraryItemUpdated) @@ -822,6 +828,7 @@ export default { this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$off('user-settings', this.settingsUpdated) + this.$eventBus.$off('series-books-updated', this.seriesBooksUpdated) if (this.$root.socket) { this.$root.socket.off('item_updated', this.libraryItemUpdated) diff --git a/client/components/modals/libraries/LibraryTools.vue b/client/components/modals/libraries/LibraryTools.vue index 6e84ecf4..00ed4f8d 100644 --- a/client/components/modals/libraries/LibraryTools.vue +++ b/client/components/modals/libraries/LibraryTools.vue @@ -1,5 +1,20 @@