mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
This commit is contained in:
parent
7c1789a7c2
commit
ad4dad1c29
@ -6,11 +6,11 @@
|
||||
<div class="flex items-center">
|
||||
<h1>{{ book.title }}</h1>
|
||||
<div class="flex-grow" />
|
||||
<p>{{ book.year || book.first_publish_date }}</p>
|
||||
<p>{{ book.publishYear }}</p>
|
||||
</div>
|
||||
<p class="text-gray-400">{{ book.author }}</p>
|
||||
<div class="w-full max-h-12 overflow-hidden">
|
||||
<p class="text-gray-500 text-xs" v-html="book.description"></p>
|
||||
<p class="text-gray-500 text-xs">{{ book.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -53,7 +53,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : null
|
||||
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null
|
||||
}
|
||||
}
|
||||
</script>
|
@ -20,7 +20,7 @@
|
||||
|
||||
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
|
||||
<keep-alive>
|
||||
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" />
|
||||
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</modals-modal>
|
||||
@ -44,11 +44,6 @@ export default {
|
||||
title: 'Cover',
|
||||
component: 'modals-edit-tabs-cover'
|
||||
},
|
||||
// {
|
||||
// id: 'match',
|
||||
// title: 'Match',
|
||||
// component: 'modals-edit-tabs-match'
|
||||
// },
|
||||
{
|
||||
id: 'tracks',
|
||||
title: 'Tracks',
|
||||
@ -68,6 +63,11 @@ export default {
|
||||
id: 'download',
|
||||
title: 'Download',
|
||||
component: 'modals-edit-tabs-download'
|
||||
},
|
||||
{
|
||||
id: 'match',
|
||||
title: 'Match',
|
||||
component: 'modals-edit-tabs-match'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -123,12 +123,16 @@ export default {
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
availableTabs() {
|
||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||
return this.tabs.filter((tab) => {
|
||||
if (tab.id === 'download' && this.isMissing) return false
|
||||
if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true
|
||||
if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true
|
||||
if (tab.id === 'match' && this.showExperimentalFeatures) return true
|
||||
return false
|
||||
})
|
||||
},
|
||||
@ -194,7 +198,9 @@ export default {
|
||||
}
|
||||
},
|
||||
selectTab(tab) {
|
||||
this.selectedTab = tab
|
||||
if (this.availableTabs.find((t) => t.id === tab)) {
|
||||
this.selectedTab = tab
|
||||
}
|
||||
},
|
||||
audiobookUpdated() {
|
||||
if (!this.show) this.fetchOnShow = true
|
||||
|
@ -1,15 +1,17 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-hidden px-4 py-6">
|
||||
<div class="w-full h-full overflow-hidden px-4 py-6 relative">
|
||||
<form @submit.prevent="submitSearch">
|
||||
<div class="flex items-center justify-start -mx-1 h-20">
|
||||
<div class="w-72 px-1">
|
||||
<ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" />
|
||||
<div class="w-40 px-1">
|
||||
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
|
||||
</div>
|
||||
<div class="w-72 px-1">
|
||||
<ui-text-input-with-label v-model="searchAuthor" label="Author" :disabled="processing" />
|
||||
<ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" />
|
||||
</div>
|
||||
<div class="w-72 px-1">
|
||||
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
|
||||
</div>
|
||||
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
</form>
|
||||
<div v-show="processing" class="flex h-full items-center justify-center">
|
||||
@ -23,6 +25,51 @@
|
||||
<cards-book-match-card :key="index" :book="res" @select="selectMatch" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||
<div class="flex mb-2">
|
||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-3">Update Book Details</p>
|
||||
</div>
|
||||
<form @submit.prevent="submitMatchUpdate">
|
||||
<div v-if="selectedMatch.cover" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.cover" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" label="Cover" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.title" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.title" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.subtitle" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.subtitle" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.author" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.author" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.description" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.description" />
|
||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.publisher" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publisher" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.publishYear" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.publishYear" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.publishYear" :disabled="!selectedMatchUsage.publishYear" label="Publish Year" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div v-if="selectedMatch.isbn" class="flex items-center py-2">
|
||||
<ui-checkbox v-model="selectedMatchUsage.isbn" />
|
||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" class="flex-grow ml-4" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end py-2">
|
||||
<ui-btn color="success" type="submit">Update</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -41,9 +88,30 @@ export default {
|
||||
searchTitle: null,
|
||||
searchAuthor: null,
|
||||
lastSearch: null,
|
||||
provider: 'best',
|
||||
providers: [
|
||||
{
|
||||
text: 'Google Books',
|
||||
value: 'google'
|
||||
},
|
||||
{
|
||||
text: 'Open Library',
|
||||
value: 'openlibrary'
|
||||
}
|
||||
],
|
||||
provider: 'google',
|
||||
searchResults: [],
|
||||
hasSearched: false
|
||||
hasSearched: false,
|
||||
selectedMatch: null,
|
||||
selectedMatchUsage: {
|
||||
title: true,
|
||||
subtitle: true,
|
||||
cover: true,
|
||||
author: true,
|
||||
description: true,
|
||||
isbn: true,
|
||||
publisher: true,
|
||||
publishYear: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -95,6 +163,18 @@ export default {
|
||||
this.hasSearched = true
|
||||
},
|
||||
init() {
|
||||
this.selectedMatch = null
|
||||
this.selectedMatchUsage = {
|
||||
title: true,
|
||||
subtitle: true,
|
||||
cover: true,
|
||||
author: true,
|
||||
description: true,
|
||||
isbn: true,
|
||||
publisher: true,
|
||||
publishYear: true
|
||||
}
|
||||
|
||||
if (this.audiobook.id !== this.audiobookId) {
|
||||
this.searchResults = []
|
||||
this.hasSearched = false
|
||||
@ -107,31 +187,63 @@ export default {
|
||||
return
|
||||
}
|
||||
this.searchTitle = this.audiobook.book.title
|
||||
this.searchAuthor = this.audiobook.book.author || ''
|
||||
this.searchAuthor = this.audiobook.book.authorFL || ''
|
||||
},
|
||||
async selectMatch(match) {
|
||||
selectMatch(match) {
|
||||
this.selectedMatch = match
|
||||
},
|
||||
buildMatchUpdatePayload() {
|
||||
var updatePayload = {}
|
||||
for (const key in this.selectedMatchUsage) {
|
||||
if (this.selectedMatchUsage[key] && this.selectedMatch[key]) {
|
||||
updatePayload[key] = this.selectedMatch[key]
|
||||
}
|
||||
}
|
||||
return updatePayload
|
||||
},
|
||||
async submitMatchUpdate() {
|
||||
var updatePayload = this.buildMatchUpdatePayload()
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
return
|
||||
}
|
||||
this.isProcessing = true
|
||||
const updatePayload = {
|
||||
book: {}
|
||||
|
||||
if (updatePayload.cover) {
|
||||
var coverPayload = {
|
||||
url: updatePayload.cover
|
||||
}
|
||||
var success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
if (success) {
|
||||
this.$toast.success('Book Cover Updated')
|
||||
} else {
|
||||
this.$toast.error('Book Cover Failed to Update')
|
||||
}
|
||||
console.log('Updated cover')
|
||||
delete updatePayload.cover
|
||||
}
|
||||
if (match.cover) {
|
||||
updatePayload.book.cover = match.cover
|
||||
|
||||
if (Object.keys(updatePayload).length) {
|
||||
var bookUpdatePayload = {
|
||||
book: updatePayload
|
||||
}
|
||||
var success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
if (success) {
|
||||
this.$toast.success('Book Details Updated')
|
||||
this.selectedMatch = null
|
||||
this.$emit('selectTab', 'details')
|
||||
} else {
|
||||
this.$toast.error('Book Details Failed to Update')
|
||||
}
|
||||
} else {
|
||||
this.selectedMatch = null
|
||||
}
|
||||
if (match.title) {
|
||||
updatePayload.book.title = match.title
|
||||
}
|
||||
if (match.description) {
|
||||
updatePayload.book.description = match.description
|
||||
}
|
||||
var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
this.isProcessing = false
|
||||
if (updatedAudiobook) {
|
||||
this.$toast.success('Update Successful')
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
client/components/ui/Checkbox.vue
Normal file
33
client/components/ui/Checkbox.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<label class="flex justify-start items-start">
|
||||
<div class="bg-white border-2 rounded border-gray-400 w-6 h-6 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500">
|
||||
<input v-model="selected" type="checkbox" class="opacity-0 absolute" />
|
||||
<svg v-if="selected" class="fill-current w-4 h-4 text-green-500 pointer-events-none" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
||||
</div>
|
||||
<div v-if="label" class="select-none">{{ label }}</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
label: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', !!val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="relative w-full" v-click-outside="clickOutside">
|
||||
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
|
||||
<button type="button" :disabled="disabled" class="relative h-10 w-full border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<p class="text-sm font-semibold">{{ label }}</p>
|
||||
<button type="button" :disabled="disabled" class="relative w-full border border-gray-500 rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" :class="small ? 'h-9' : 'h-10'" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate">{{ selectedText }}</span>
|
||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-icons text-gray-100">expand_more</span>
|
||||
@ -36,7 +36,8 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: Boolean
|
||||
disabled: Boolean,
|
||||
small: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -80,6 +80,7 @@ input {
|
||||
border-style: inherit !important;
|
||||
}
|
||||
input:read-only {
|
||||
color: #aaa;
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold">
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">
|
||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</p>
|
||||
<ui-text-input v-model="inputValue" :disabled="disabled" :type="type" class="w-full" />
|
||||
|
@ -38,10 +38,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
textarea {
|
||||
border-style: inherit !important;
|
||||
}
|
||||
input:read-only {
|
||||
background-color: #eee;
|
||||
textarea:read-only {
|
||||
color: #aaa;
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="px-1 text-sm font-semibold">{{ label }}</p>
|
||||
<ui-textarea-input v-model="inputValue" :rows="rows" class="w-full" />
|
||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||
<ui-textarea-input v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,7 @@ export default {
|
||||
props: {
|
||||
value: [String, Number],
|
||||
label: String,
|
||||
disabled: Boolean,
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 2
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.5.5",
|
||||
"version": "1.5.6",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.5.5",
|
||||
"version": "1.5.6",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -6,6 +6,8 @@ const Logger = require('./Logger')
|
||||
const { isObject } = require('./utils/index')
|
||||
const audioFileScanner = require('./utils/audioFileScanner')
|
||||
|
||||
const BookFinder = require('./BookFinder')
|
||||
|
||||
const Library = require('./objects/Library')
|
||||
const User = require('./objects/User')
|
||||
|
||||
@ -24,6 +26,8 @@ class ApiController {
|
||||
this.clientEmitter = clientEmitter
|
||||
this.MetadataPath = MetadataPath
|
||||
|
||||
this.bookFinder = new BookFinder()
|
||||
|
||||
this.router = express()
|
||||
this.init()
|
||||
}
|
||||
@ -51,6 +55,7 @@ class ApiController {
|
||||
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
|
||||
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
|
||||
this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this))
|
||||
this.router.get('/audiobook/:id/match', this.matchAudiobookBook.bind(this))
|
||||
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
|
||||
|
||||
this.router.patch('/match/:id', this.match.bind(this))
|
||||
@ -85,8 +90,12 @@ class ApiController {
|
||||
this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this))
|
||||
}
|
||||
|
||||
find(req, res) {
|
||||
this.scanner.find(req, res)
|
||||
async find(req, res) {
|
||||
var provider = req.query.provider || 'google'
|
||||
var title = req.query.title || ''
|
||||
var author = req.query.author || ''
|
||||
var results = await this.bookFinder.search(provider, title, author)
|
||||
res.json(results)
|
||||
}
|
||||
|
||||
findCovers(req, res) {
|
||||
@ -497,6 +506,18 @@ class ApiController {
|
||||
else res.status(200).send('No update was made to cover')
|
||||
}
|
||||
|
||||
async matchAudiobookBook(req, res) {
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||
if (!audiobook) return res.sendStatus(404)
|
||||
|
||||
var provider = req.query.provider || 'google'
|
||||
var excludeAuthor = req.query.excludeAuthor === '1'
|
||||
var authorSearch = excludeAuthor ? null : audiobook.authorFL
|
||||
|
||||
var results = await this.bookFinder.search(provider, audiobook.title, authorSearch)
|
||||
res.json(results)
|
||||
}
|
||||
|
||||
async updateAudiobook(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn('User attempted to update without permission', req.user)
|
||||
|
@ -1,5 +1,6 @@
|
||||
const OpenLibrary = require('./providers/OpenLibrary')
|
||||
const LibGen = require('./providers/LibGen')
|
||||
const GoogleBooks = require('./providers/GoogleBooks')
|
||||
const Logger = require('./Logger')
|
||||
const { levenshteinDistance } = require('./utils/index')
|
||||
|
||||
@ -7,6 +8,7 @@ class BookFinder {
|
||||
constructor() {
|
||||
this.openLibrary = new OpenLibrary()
|
||||
this.libGen = new LibGen()
|
||||
this.googleBooks = new GoogleBooks()
|
||||
|
||||
this.verbose = false
|
||||
}
|
||||
@ -143,13 +145,26 @@ class BookFinder {
|
||||
return booksFiltered
|
||||
}
|
||||
|
||||
async getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||
var books = await this.googleBooks.search(title, author)
|
||||
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
||||
if (books.errorCode) {
|
||||
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
||||
return []
|
||||
}
|
||||
// Google has good sort
|
||||
return books
|
||||
}
|
||||
|
||||
async search(provider, title, author, options = {}) {
|
||||
var books = []
|
||||
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
||||
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
||||
Logger.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
||||
|
||||
if (provider === 'libgen') {
|
||||
if (provider === 'google') {
|
||||
return this.getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||
} else if (provider === 'libgen') {
|
||||
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||
} else if (provider === 'openlibrary') {
|
||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||
|
@ -10,7 +10,6 @@ const { comparePaths, getIno } = require('./utils/index')
|
||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||
const { ScanResult, CoverDestination } = require('./utils/constants')
|
||||
|
||||
// Classes
|
||||
const BookFinder = require('./BookFinder')
|
||||
const Audiobook = require('./objects/Audiobook')
|
||||
|
||||
|
@ -91,6 +91,10 @@ class Audiobook {
|
||||
return this.book ? this.book.authorLF : null
|
||||
}
|
||||
|
||||
get authorFL() {
|
||||
return this.book ? this.book.authorFL : null
|
||||
}
|
||||
|
||||
get genres() {
|
||||
return this.book ? this.book.genres || [] : []
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const parseAuthors = require('../utils/parseAuthors')
|
||||
@ -16,6 +15,7 @@ class Book {
|
||||
this.publishYear = null
|
||||
this.publisher = null
|
||||
this.description = null
|
||||
this.isbn = null
|
||||
this.cover = null
|
||||
this.coverFullPath = null
|
||||
this.genres = []
|
||||
@ -56,6 +56,7 @@ class Book {
|
||||
this.publishYear = book.publishYear
|
||||
this.publisher = book.publisher
|
||||
this.description = book.description
|
||||
this.isbn = book.isbn || null
|
||||
this.cover = book.cover
|
||||
this.coverFullPath = book.coverFullPath || null
|
||||
this.genres = book.genres
|
||||
@ -78,6 +79,7 @@ class Book {
|
||||
publishYear: this.publishYear,
|
||||
publisher: this.publisher,
|
||||
description: this.description,
|
||||
isbn: this.isbn,
|
||||
cover: this.cover,
|
||||
coverFullPath: this.coverFullPath,
|
||||
genres: this.genres,
|
||||
@ -116,6 +118,7 @@ class Book {
|
||||
this.volumeNumber = data.volumeNumber || null
|
||||
this.publishYear = data.publishYear || null
|
||||
this.description = data.description || null
|
||||
this.isbn = data.isbn || null
|
||||
this.cover = data.cover || null
|
||||
this.coverFullPath = data.coverFullPath || null
|
||||
this.genres = data.genres || []
|
||||
|
50
server/providers/GoogleBooks.js
Normal file
50
server/providers/GoogleBooks.js
Normal file
@ -0,0 +1,50 @@
|
||||
const axios = require('axios')
|
||||
const Logger = require('../Logger')
|
||||
|
||||
class GoogleBooks {
|
||||
constructor() { }
|
||||
|
||||
extractIsbn(industryIdentifiers) {
|
||||
if (!industryIdentifiers || !industryIdentifiers.length) return null
|
||||
|
||||
var isbnObj = industryIdentifiers.find(i => i.type === 'ISBN_13') || industryIdentifiers.find(i => i.type === 'ISBN_10')
|
||||
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
|
||||
return null
|
||||
}
|
||||
|
||||
cleanResult(item) {
|
||||
var { id, volumeInfo } = item
|
||||
if (!volumeInfo) return null
|
||||
var { title, subtitle, authors, publisher, publisherDate, description, industryIdentifiers, categories, imageLinks } = volumeInfo
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
subtitle: subtitle || null,
|
||||
author: authors ? authors.join(', ') : null,
|
||||
publisher,
|
||||
publishYear: publisherDate ? publisherDate.split('-')[0] : null,
|
||||
description,
|
||||
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
|
||||
genres: categories ? categories.join(', ') : null,
|
||||
isbn: this.extractIsbn(industryIdentifiers)
|
||||
}
|
||||
}
|
||||
|
||||
async search(title, author) {
|
||||
var queryString = `q=intitle:${title}`
|
||||
if (author) queryString += `+inauthor:${author}`
|
||||
var url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
|
||||
Logger.debug(`[GoogleBooks] Search url: ${url}`)
|
||||
var items = await axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.items) return []
|
||||
return res.data.items
|
||||
}).catch(error => {
|
||||
Logger.error('[GoogleBooks] Volume search error', error)
|
||||
return []
|
||||
})
|
||||
return items.map(item => this.cleanResult(item))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleBooks
|
@ -51,12 +51,21 @@ class OpenLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
parsePublishYear(doc, worksData) {
|
||||
if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return doc.first_publish_year
|
||||
if (worksData.first_publish_date) {
|
||||
var year = worksData.first_publish_date.split('-')[0]
|
||||
if (!isNaN(year)) return year
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async cleanSearchDoc(doc) {
|
||||
var worksData = await this.getWorksData(doc.key)
|
||||
return {
|
||||
title: doc.title,
|
||||
author: doc.author_name ? doc.author_name.join(', ') : null,
|
||||
year: doc.first_publish_year,
|
||||
publishYear: this.parsePublishYear(doc, worksData),
|
||||
edition: doc.cover_edition_key,
|
||||
cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null,
|
||||
...worksData
|
||||
|
Loading…
Reference in New Issue
Block a user