mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-05-18 09:21:31 +00:00
Merge 2cfc175c61 into 47ea6b5092
This commit is contained in:
commit
12600ba04c
19 changed files with 2397 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ export default {
|
|||
autoScanCronExpression: null,
|
||||
hideSingleBookSeries: false,
|
||||
onlyShowLaterBooksInContinueSeries: false,
|
||||
openAIDirectoryGrouping: false,
|
||||
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
|
||||
markAsFinishedPercentComplete: null,
|
||||
markAsFinishedTimeRemaining: 10
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
<template>
|
||||
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
|
||||
<div class="flex items-center justify-between md:justify-start mb-4">
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch v-model="openAIDirectoryGrouping" @input="updated" />
|
||||
<p class="pl-4 text-sm text-gray-300">Use OpenAI to interpret poor directory trees during library scans</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-base md:text-lg text-gray-200">{{ $strings.HeaderMetadataOrderOfPrecedence }}</h2>
|
||||
<ui-btn small @click="resetToDefault">{{ $strings.ButtonResetToDefault }}</ui-btn>
|
||||
|
|
@ -66,6 +73,11 @@ export default {
|
|||
name: 'Audio file meta tags OR ebook metadata',
|
||||
include: true
|
||||
},
|
||||
openAIPathMetadata: {
|
||||
id: 'openAIPathMetadata',
|
||||
name: 'OpenAI path and filename inference',
|
||||
include: false
|
||||
},
|
||||
nfoFile: {
|
||||
id: 'nfoFile',
|
||||
name: 'NFO file',
|
||||
|
|
@ -87,7 +99,8 @@ export default {
|
|||
include: true
|
||||
}
|
||||
},
|
||||
metadataSourceMapped: []
|
||||
metadataSourceMapped: [],
|
||||
openAIDirectoryGrouping: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -126,6 +139,7 @@ export default {
|
|||
metadataSourceIds.reverse()
|
||||
return {
|
||||
settings: {
|
||||
openAIDirectoryGrouping: !!this.openAIDirectoryGrouping,
|
||||
metadataPrecedence: metadataSourceIds
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +154,7 @@ export default {
|
|||
this.$emit('update', this.getLibraryData())
|
||||
},
|
||||
init() {
|
||||
this.openAIDirectoryGrouping = !!this.librarySettings.openAIDirectoryGrouping
|
||||
const metadataPrecedence = this.librarySettings.metadataPrecedence || []
|
||||
this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s)
|
||||
|
||||
|
|
@ -157,4 +172,4 @@ export default {
|
|||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,34 @@
|
|||
<template>
|
||||
<div class="w-full h-full px-1 md:px-2 py-1 mb-4">
|
||||
<div v-if="isBookLibrary" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
<p class="text-lg">Detect Missing Series With AI</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Analyze books in this library and add missing series names and sequence values using OpenAI. Use the full re-evaluation option after editing book metadata and you want existing series assignments reconsidered.</p>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn class="mb-3 block" :disabled="processing || !openAIConfigured" @click.stop="detectSeriesWithAI">Detect Missing Series</ui-btn>
|
||||
<ui-btn :disabled="processing || !openAIConfigured" @click.stop="reEvaluateSeriesWithAI">Re-evaluate All Series</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!openAIConfigured" class="text-sm text-yellow-400 mt-3">Configure OpenAI first in server settings.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isBookLibrary" class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
<p class="text-lg">Dedupe Books With AI</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Analyze likely duplicate books in this library with OpenAI, keep the best copy, and remove the duplicate items. This deletes duplicate files from disk.</p>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn :disabled="processing || !openAIConfigured" @click.stop="dedupeBooksWithAI">Dedupe Books</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!openAIConfigured" class="text-sm text-yellow-400 mt-3">Configure OpenAI first in server settings.</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
|
|
@ -38,9 +67,86 @@ export default {
|
|||
},
|
||||
isBookLibrary() {
|
||||
return this.mediaType === 'book'
|
||||
},
|
||||
openAIConfigured() {
|
||||
return !!this.$store.getters['getServerSetting']('openAIConfigured')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
detectSeriesWithAI() {
|
||||
const payload = {
|
||||
message: 'Detect missing series in this library with AI? This only fills books that currently have no series metadata.',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.runSeriesDetection(true)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
reEvaluateSeriesWithAI() {
|
||||
const payload = {
|
||||
message: 'Re-evaluate all books in this library with AI? This can update sequence values for books that already have series metadata.',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.runSeriesDetection(false)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
dedupeBooksWithAI() {
|
||||
const payload = {
|
||||
message: 'Deduplicate books in this library with AI? Duplicate items chosen for removal will be deleted from the database and file system.',
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.runBookDedupe()
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
runSeriesDetection(onlyMissing = true) {
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$post(`/api/libraries/${this.libraryId}/detect-series-with-ai?onlyMissing=${onlyMissing ? 1 : 0}`)
|
||||
.then((data) => {
|
||||
if (!data.updated) {
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
} else {
|
||||
this.$toast.success(onlyMissing ? `AI added series data to ${data.updated} books` : `AI re-evaluated series data for ${data.updated} books`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to detect series with AI', error)
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('update:processing', false)
|
||||
})
|
||||
},
|
||||
runBookDedupe() {
|
||||
this.$emit('update:processing', true)
|
||||
this.$axios
|
||||
.$post(`/api/libraries/${this.libraryId}/dedupe-books-with-ai?hard=1`)
|
||||
.then((data) => {
|
||||
if (!data.duplicatesRemoved) {
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
} else {
|
||||
this.$toast.success(`AI removed ${data.duplicatesRemoved} duplicate books`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to dedupe books with AI', error)
|
||||
this.$toast.error(error.response?.data || this.$strings.ToastFailedToUpdate)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('update:processing', false)
|
||||
})
|
||||
},
|
||||
removeAllMetadataClick(ext) {
|
||||
const payload = {
|
||||
message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue