From 1dc01615dd26e0bf14171a631ed0ac07b0d22657 Mon Sep 17 00:00:00 2001 From: advplyr <advplyr@protonmail.com> Date: Wed, 25 Aug 2021 19:15:00 -0500 Subject: [PATCH] Fix listener for audiobook updates in edit modal, add remove cover button --- client/components/modals/EditModal.vue | 16 +++-- client/components/modals/edit-tabs/Cover.vue | 37 ++++++++-- .../components/modals/edit-tabs/Details.vue | 1 - client/components/modals/edit-tabs/Match.vue | 3 +- client/package.json | 2 +- package.json | 2 +- server/Book.js | 10 ++- server/BookFinder.js | 69 ++++++++++--------- server/Scanner.js | 7 +- 9 files changed, 96 insertions(+), 51 deletions(-) diff --git a/client/components/modals/EditModal.vue b/client/components/modals/EditModal.vue index b4e337f4..441d115a 100644 --- a/client/components/modals/EditModal.vue +++ b/client/components/modals/EditModal.vue @@ -25,18 +25,21 @@ export default { return { selectedTab: 'details', processing: false, - audiobook: null + audiobook: null, + fetchOnShow: false } }, watch: { show: { handler(newVal) { if (newVal) { - if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) return + if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) { + if (this.fetchOnShow) this.fetchFull() + return + } + this.fetchOnShow = false this.audiobook = null this.init() - } else { - this.$store.commit('audiobooks/removeListener', 'edit-modal') } } } @@ -75,7 +78,10 @@ export default { this.selectedTab = tab }, audiobookUpdated() { - this.fetchFull() + if (!this.show) this.fetchOnShow = true + else { + this.fetchFull() + } }, init() { this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedAudiobookId }) diff --git a/client/components/modals/edit-tabs/Cover.vue b/client/components/modals/edit-tabs/Cover.vue index 38ba5d60..201c6f89 100644 --- a/client/components/modals/edit-tabs/Cover.vue +++ b/client/components/modals/edit-tabs/Cover.vue @@ -1,7 +1,16 @@ <template> <div class="w-full h-full overflow-hidden overflow-y-auto px-1"> <div class="flex"> - <cards-book-cover :audiobook="audiobook" /> + <div class="relative"> + <cards-book-cover :audiobook="audiobook" /> + <!-- book cover overlay --> + <div v-if="book.cover" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100"> + <div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" /> + <div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover"> + <span class="material-icons">delete</span> + </div> + </div> + </div> <div class="flex-grow pl-6 pr-2"> <form @submit.prevent="submitForm"> <div class="flex items-center"> @@ -19,7 +28,6 @@ <div v-if="showLocalCovers" class="flex items-center justify-center"> <template v-for="cover in localCovers"> - <!-- <img :src="`/local/${cover.path}`" :key="cover.path" class="w-20 h-32 object-cover m-0.5" /> --> <div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover.localPath)"> <img :src="cover.localPath" class="h-24 object-cover" style="width: 60px" /> </div> @@ -43,9 +51,11 @@ <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-60 overflow-y-scroll mt-2 max-w-full"> <p v-if="!coversFound.length">No Covers Found</p> <template v-for="cover in coversFound"> - <div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)"> - <img :src="cover" class="h-24 object-cover" style="width: 60px" /> - </div> + <ui-tooltip :key="cover" direction="bottom" :text="cover"> + <div class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)"> + <img :src="cover" class="h-24 object-cover" style="width: 60px" /> + </div> + </ui-tooltip> </template> </div> </div> @@ -78,7 +88,9 @@ export default { audiobook: { immediate: true, handler(newVal) { - if (newVal) this.init() + if (newVal) { + this.init() + } } } }, @@ -118,10 +130,22 @@ export default { this.searchTitle = this.book.title || '' this.searchAuthor = this.book.author || '' }, + removeCover() { + if (!this.book.cover) { + this.imageUrl = '' + return + } + this.updateCover('') + }, submitForm() { this.updateCover(this.imageUrl) }, async updateCover(cover) { + if (cover === this.book.cover) { + console.warn('Cover has not changed..', cover) + return + } + this.isProcessing = true const updatePayload = { book: { @@ -134,7 +158,6 @@ export default { }) this.isProcessing = false if (updatedAudiobook) { - console.log('Update Successful', updatedAudiobook) this.$toast.success('Update Successful') this.$emit('close') } diff --git a/client/components/modals/edit-tabs/Details.vue b/client/components/modals/edit-tabs/Details.vue index 9b9cec3f..99c7e506 100644 --- a/client/components/modals/edit-tabs/Details.vue +++ b/client/components/modals/edit-tabs/Details.vue @@ -130,7 +130,6 @@ export default { }) this.isProcessing = false if (updatedAudiobook) { - console.log('Update Successful', updatedAudiobook) this.$toast.success('Update Successful') this.$emit('close') } diff --git a/client/components/modals/edit-tabs/Match.vue b/client/components/modals/edit-tabs/Match.vue index 93c547ae..9e522625 100644 --- a/client/components/modals/edit-tabs/Match.vue +++ b/client/components/modals/edit-tabs/Match.vue @@ -66,7 +66,7 @@ export default { }, methods: { getSearchQuery() { - var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` + var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${this.searchTitle}` if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}` return searchQuery }, @@ -129,7 +129,6 @@ export default { }) this.isProcessing = false if (updatedAudiobook) { - console.log('Update Successful', updatedAudiobook) this.$toast.success('Update Successful') this.$emit('close') } diff --git a/client/package.json b/client/package.json index 74fa3690..1642a98f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "0.9.77-beta", + "version": "0.9.78-beta", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/package.json b/package.json index fe5728f5..cc9e918c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "0.9.77-beta", + "version": "0.9.78-beta", "description": "Self-hosted audiobook server for managing and playing audiobooks.", "main": "index.js", "scripts": { diff --git a/server/Book.js b/server/Book.js index 601e0221..f636b86a 100644 --- a/server/Book.js +++ b/server/Book.js @@ -95,13 +95,21 @@ class Book { if (data.otherFiles && data.otherFiles.length) { var imageFile = data.otherFiles.find(f => f.filetype === 'image') if (imageFile) { - this.cover = Path.join('/local', imageFile.path) + this.cover = Path.normalize(Path.join('/local', imageFile.path)) } } } update(payload) { var hasUpdates = false + + if (payload.cover) { + // If updating to local cover then normalize path + if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) { + payload.cover = Path.normalize(payload.cover) + } + } + for (const key in payload) { if (payload[key] === undefined) continue; diff --git a/server/BookFinder.js b/server/BookFinder.js index 5e05cd09..7e4ea0c4 100644 --- a/server/BookFinder.js +++ b/server/BookFinder.js @@ -12,7 +12,7 @@ class BookFinder { async findByISBN(isbn) { var book = await this.openLibrary.isbnLookup(isbn) if (book.errorCode) { - console.error('Book not found') + Logger.error('Book not found') } return book } @@ -61,73 +61,82 @@ class BookFinder { return books.map(b => { b.cleanedTitle = this.cleanTitleForCompares(b.title) b.titleDistance = levenshteinDistance(b.cleanedTitle, title) + + // Total length of search (title or both title & author) + b.totalPossibleDistance = b.title.length + if (author) { if (!b.author) { b.authorDistance = author.length } else { + b.totalPossibleDistance += b.author.length b.cleanedAuthor = this.cleanAuthorForCompares(b.author) var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor) var authorDistance = levenshteinDistance(b.author || '', author) + // Use best distance - if (cleanedAuthorDistance > authorDistance) b.authorDistance = authorDistance - else b.authorDistance = cleanedAuthorDistance + b.authorDistance = Math.min(cleanedAuthorDistance, authorDistance) + + // Check book author contains searchAuthor + if (searchAuthor.length > 4 && b.cleanedAuthor.includes(searchAuthor)) b.includesAuthor = searchAuthor + else if (author.length > 4 && b.author.includes(author)) b.includesAuthor = author } } b.totalDistance = b.titleDistance + (b.authorDistance || 0) - b.totalPossibleDistance = b.title.length - if (b.cleanedTitle.includes(searchTitle) && searchTitle.length > 4) { - b.includesSearch = searchTitle - } else if (b.title.includes(searchTitle) && searchTitle.length > 4) { - b.includesSearch = searchTitle - } - - if (author && b.author) b.totalPossibleDistance += b.author.length + // Check book title contains the searchTitle + if (searchTitle.length > 4 && b.cleanedTitle.includes(searchTitle)) b.includesTitle = searchTitle + else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title return b }).filter(b => { - if (b.includesSearch) { // If search was found in result title exactly then skip over leven distance check - Logger.debug(`Exact search was found inside title ${b.cleanedTitle}/${b.includesSearch}`) + if (b.includesTitle) { // If search title was found in result title then skip over leven distance check + Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`) } else if (b.titleDistance > maxTitleDistance) { Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`) return false } - if (author && b.authorDistance > maxAuthorDistance) { - Logger.debug(`Filtering out search result "${b.title}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`) - return false + if (author) { + if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check + Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`) + } else if (b.authorDistance > maxAuthorDistance) { + Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`) + return false + } } - if (b.totalPossibleDistance < 4 && b.totalDistance > 0) return false + // If book total search length < 5 and was not exact match, then filter out + if (b.totalPossibleDistance < 5 && b.totalDistance > 0) return false return true }) } async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) { var books = await this.libGen.search(title) - Logger.info(`LibGen Book Search Results: ${books.length || 0}`) + Logger.debug(`LibGen Book Search Results: ${books.length || 0}`) if (books.errorCode) { Logger.error(`LibGen Search Error ${books.errorCode}`) return [] } var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) if (!booksFiltered.length && books.length) { - Logger.info(`Search has ${books.length} matches, but no close title matches`) + Logger.debug(`Search has ${books.length} matches, but no close title matches`) } return booksFiltered } async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) { var books = await this.openLibrary.searchTitle(title) - Logger.info(`OpenLib Book Search Results: ${books.length || 0}`) + Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`) if (books.errorCode) { Logger.error(`OpenLib Search Error ${books.errorCode}`) return [] } var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) if (!booksFiltered.length && books.length) { - Logger.info(`Search has ${books.length} matches, but no close title matches`) + Logger.debug(`Search has ${books.length} matches, but no close title matches`) } return booksFiltered } @@ -136,7 +145,7 @@ class BookFinder { var books = [] var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 - Logger.info(`Book Search, title: "${title}", author: "${author}", provider: ${provider}`) + Logger.debug(`Book Search, title: "${title}", author: "${author}", provider: ${provider}`) if (provider === 'libgen') { books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) @@ -147,18 +156,16 @@ class BookFinder { var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) books = books.concat(lbBooks, olBooks) } else { - var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) - var hasCloseMatch = olBooks.find(b => (b.totalDistance < 4 && b.totalPossibleDistance > 4)) - if (hasCloseMatch) { - books = olBooks - } else { - Logger.info(`Book Search, LibGen has no close matches - get openlib results also`) + books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) + var hasCloseMatch = books.find(b => (b.totalDistance < 2 && b.totalPossibleDistance > 6)) + if (!hasCloseMatch) { + Logger.debug(`Book Search, openlib has no super close matches - get libgen results also`) var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) books = books.concat(lbBooks) } - if (!books.length && author) { - Logger.info(`Book Search, no matches for title and author.. check title only`) + if (!books.length && author && options.fallbackTitleOnly) { + Logger.debug(`Book Search, no matches for title and author.. check title only`) return this.search(provider, title, null, options) } } @@ -170,7 +177,7 @@ class BookFinder { async findCovers(provider, title, author, options = {}) { var searchResults = await this.search(provider, title, author, options) - Logger.info(`[BookFinder] FindCovers search results: ${searchResults.length}`) + Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`) var covers = [] searchResults.forEach((result) => { diff --git a/server/Scanner.js b/server/Scanner.js index 0d844ae9..8e827993 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -247,7 +247,7 @@ class Scanner { } var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options) if (results.length) { - Logger.info(`[Scanner] Found best cover for "${audiobook.title}"`) + Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`) audiobook.book.cover = results[0] await this.db.updateAudiobook(audiobook) found++ @@ -294,7 +294,10 @@ class Scanner { async findCovers(req, res) { var query = req.query - var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null) + var options = { + fallbackTitleOnly: !!query.fallbackTitleOnly + } + var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null, options) res.json(result) } }