mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-04 01:17:19 +02:00
Support multi library 1.4.0
This commit is contained in:
parent
a65f7e6fad
commit
d9d34e87e0
@ -81,14 +81,17 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit('audiobooks/setKeywordFilter', val)
|
this.$store.commit('audiobooks/setKeywordFilter', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
searchBackArrow() {
|
searchBackArrow() {
|
||||||
this.$router.replace('/library')
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
},
|
},
|
||||||
seriesBackArrow() {
|
seriesBackArrow() {
|
||||||
this.$router.replace('/library/series')
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/series`)
|
||||||
this.$emit('update:selectedSeries', null)
|
this.$emit('update:selectedSeries', null)
|
||||||
},
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
|
@ -63,12 +63,15 @@ export default {
|
|||||||
},
|
},
|
||||||
playlistUrl() {
|
playlistUrl() {
|
||||||
return this.stream ? this.stream.clientPlaylistUri : null
|
return this.stream ? this.stream.clientPlaylistUri : null
|
||||||
|
},
|
||||||
|
libraryId() {
|
||||||
|
return this.streamAudiobook ? this.streamAudiobook.libraryId : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterByAuthor() {
|
filterByAuthor() {
|
||||||
if (this.$route.name !== 'index') {
|
if (this.$route.name !== 'index') {
|
||||||
this.$router.push('/library')
|
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
||||||
}
|
}
|
||||||
var settingsUpdate = {
|
var settingsUpdate = {
|
||||||
filterBy: `authors.${this.$encode(this.author)}`
|
filterBy: `authors.${this.$encode(this.author)}`
|
||||||
|
@ -50,12 +50,15 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
audiobooks() {
|
audiobooks() {
|
||||||
return this.$store.state.audiobooks.audiobooks
|
return this.$store.state.audiobooks.audiobooks
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.search) return
|
if (!this.search) return
|
||||||
this.$router.push(`/library/search?query=${this.search}`)
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${this.search}`)
|
||||||
|
|
||||||
this.search = null
|
this.search = null
|
||||||
this.items = []
|
this.items = []
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<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)">
|
<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)">
|
||||||
<div class="h-24 bg-primary" style="width: 60px">
|
<div class="h-24 bg-primary" style="width: 60px">
|
||||||
<img :src="cover.localPath" class="h-full w-full object-contain" />
|
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -124,21 +124,31 @@ export default {
|
|||||||
this.$emit('update:processing', val)
|
this.$emit('update:processing', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook ? this.audiobook.id : null
|
||||||
|
},
|
||||||
book() {
|
book() {
|
||||||
return this.audiobook ? this.audiobook.book || {} : {}
|
return this.audiobook ? this.audiobook.book || {} : {}
|
||||||
},
|
},
|
||||||
|
audiobookPath() {
|
||||||
|
return this.audiobook ? this.audiobook.path : null
|
||||||
|
},
|
||||||
otherFiles() {
|
otherFiles() {
|
||||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
||||||
},
|
},
|
||||||
userCanUpload() {
|
userCanUpload() {
|
||||||
return this.$store.getters['user/getUserCanUpload']
|
return this.$store.getters['user/getUserCanUpload']
|
||||||
},
|
},
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
localCovers() {
|
localCovers() {
|
||||||
return this.otherFiles
|
return this.otherFiles
|
||||||
.filter((f) => f.filetype === 'image')
|
.filter((f) => f.filetype === 'image')
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
var _file = { ...file }
|
var _file = { ...file }
|
||||||
_file.localPath = Path.join('local', _file.path)
|
var imgRelPath = _file.path.replace(this.audiobookPath, '')
|
||||||
|
_file.localPath = `/s/book/${this.audiobookId}${imgRelPath}`
|
||||||
return _file
|
return _file
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,8 @@
|
|||||||
<ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
|
<ui-btn v-if="isRootUser" :loading="savingMetadata" color="bg" type="button" class="h-full" small @click.stop.prevent="saveMetadata">Save Metadata</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="ml-4">
|
<ui-tooltip :disabled="libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="ml-4">
|
||||||
<ui-btn v-if="isRootUser" :loading="rescanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
<ui-btn v-if="isRootUser" :loading="rescanning" :disabled="libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@ -138,6 +138,13 @@ export default {
|
|||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
return this.$store.state.audiobooks.series
|
return this.$store.state.audiobooks.series
|
||||||
|
},
|
||||||
|
libraryId() {
|
||||||
|
return this.audiobook ? this.audiobook.libraryId : null
|
||||||
|
},
|
||||||
|
libraryScan() {
|
||||||
|
if (!this.libraryId) return null
|
||||||
|
return this.$store.getters['scanners/getLibraryScan'](this.libraryId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -207,6 +214,8 @@ export default {
|
|||||||
this.details.volumeNumber = this.book.volumeNumber
|
this.details.volumeNumber = this.book.volumeNumber
|
||||||
this.details.publishYear = this.book.publishYear
|
this.details.publishYear = this.book.publishYear
|
||||||
|
|
||||||
|
console.log('INIT', this.details)
|
||||||
|
|
||||||
this.newTags = this.audiobook.tags || []
|
this.newTags = this.audiobook.tags || []
|
||||||
},
|
},
|
||||||
resetProgress() {
|
resetProgress() {
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<th class="text-left">Duration</th>
|
<th class="text-left">Duration</th>
|
||||||
<th v-if="showDownload" class="text-center">Download</th>
|
<th v-if="showDownload" class="text-center">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracks">
|
<template v-for="track in tracksCleaned">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p>{{ track.index }}</p>
|
<p>{{ track.index }}</p>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showDownload" class="font-mono text-center">
|
<td v-if="showDownload" class="font-mono text-center">
|
||||||
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -59,6 +59,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
audiobookPath() {
|
||||||
|
return this.audiobook.path
|
||||||
|
},
|
||||||
|
tracksCleaned() {
|
||||||
|
return this.tracks.map((track) => {
|
||||||
|
var trackPath = track.path.replace(/\\/g, '/')
|
||||||
|
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
relativePath: trackPath.replace(audiobookPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
<p class="text-xl font-book pl-4" :class="mouseover ? 'underline' : ''">{{ library.name }}</p>
|
<p class="text-xl font-book pl-4" :class="mouseover ? 'underline' : ''">{{ library.name }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
|
<ui-btn v-show="mouseover && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn>
|
||||||
<span v-show="mouseover && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
|
<span v-show="mouseover && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4" @click.stop="editClick">edit</span>
|
||||||
<span v-show="mouseover && showEdit && canDelete" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
|
<span v-show="!libraryScan && mouseover && showEdit && canDelete" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50'" @click.stop="deleteClick">delete</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<th class="text-left">Duration</th>
|
<th class="text-left">Duration</th>
|
||||||
<th v-if="userCanDownload" class="text-center">Download</th>
|
<th v-if="userCanDownload" class="text-center">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracks">
|
<template v-for="track in tracksCleaned">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p>{{ track.index }}</p>
|
<p>{{ track.index }}</p>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="text-center">
|
<td v-if="userCanDownload" class="text-center">
|
||||||
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -53,7 +53,10 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
audiobookId: String
|
audiobook: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -61,6 +64,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook.id
|
||||||
|
},
|
||||||
|
audiobookPath() {
|
||||||
|
return this.audiobook.path
|
||||||
|
},
|
||||||
|
tracksCleaned() {
|
||||||
|
return this.tracks.map((track) => {
|
||||||
|
var trackPath = track.path.replace(/\\/g, '/')
|
||||||
|
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
relativePath: trackPath.replace(audiobookPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-44" v-click-outside="clickOutside">
|
<div class="relative w-full" v-click-outside="clickOutside">
|
||||||
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
|
<p class="text-sm text-opacity-75 mb-1">{{ label }}</p>
|
||||||
<button type="button" class="relative w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
|
<button type="button" :disabled="disabled" class="relative h-10 w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate">{{ selectedText }}</span>
|
<span class="block truncate">{{ selectedText }}</span>
|
||||||
</span>
|
</span>
|
||||||
@ -11,11 +11,11 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate font-sans text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -35,7 +35,8 @@ export default {
|
|||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
},
|
||||||
|
disabled: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -59,6 +60,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
clickOutside() {
|
clickOutside() {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tooltip: null,
|
tooltip: null,
|
||||||
|
tooltipId: null,
|
||||||
isShowing: false
|
isShowing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -45,13 +46,14 @@ export default {
|
|||||||
'font-size': '0.75rem'
|
'font-size': '0.75rem'
|
||||||
}
|
}
|
||||||
var size = this.$calculateTextSize(this.text, styles)
|
var size = this.$calculateTextSize(this.text, styles)
|
||||||
console.log('Text Size', size.width, size.height)
|
|
||||||
return size.width
|
return size.width
|
||||||
},
|
},
|
||||||
createTooltip() {
|
createTooltip() {
|
||||||
if (!this.$refs.box) return
|
if (!this.$refs.box) return
|
||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
tooltip.className = 'absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
|
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
||||||
|
tooltip.id = this.tooltipId
|
||||||
|
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
|
||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
@ -91,8 +93,14 @@ export default {
|
|||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
if (!this.tooltip) {
|
if (!this.tooltip) {
|
||||||
this.createTooltip()
|
this.createTooltip()
|
||||||
|
if (!this.tooltip) return
|
||||||
|
}
|
||||||
|
if (!this.$refs.box) return // Ensure element is not destroyed
|
||||||
|
try {
|
||||||
|
document.body.appendChild(this.tooltip)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
document.body.appendChild(this.tooltip)
|
|
||||||
this.isShowing = true
|
this.isShowing = true
|
||||||
},
|
},
|
||||||
hideTooltip() {
|
hideTooltip() {
|
||||||
|
@ -67,6 +67,11 @@ export default {
|
|||||||
if (payload.serverSettings) {
|
if (payload.serverSettings) {
|
||||||
this.$store.commit('setServerSettings', payload.serverSettings)
|
this.$store.commit('setServerSettings', payload.serverSettings)
|
||||||
}
|
}
|
||||||
|
if (payload.librariesScanning) {
|
||||||
|
payload.librariesScanning.forEach((libraryScan) => {
|
||||||
|
this.scanStart(libraryScan)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
streamOpen(stream) {
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream)
|
if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream)
|
||||||
@ -123,7 +128,7 @@ export default {
|
|||||||
|
|
||||||
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
||||||
if (existingScan && !isNaN(existingScan.toastId)) {
|
if (existingScan && !isNaN(existingScan.toastId)) {
|
||||||
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, position: 'bottom-center' } }, true)
|
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, position: 'bottom-center', onClose: () => null } }, true)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(message, { timeout: 5000, position: 'bottom-center' })
|
this.$toast.success(message, { timeout: 5000, position: 'bottom-center' })
|
||||||
}
|
}
|
||||||
@ -131,20 +136,19 @@ export default {
|
|||||||
this.$store.commit('scanners/remove', data)
|
this.$store.commit('scanners/remove', data)
|
||||||
},
|
},
|
||||||
onScanToastCancel(id) {
|
onScanToastCancel(id) {
|
||||||
console.log('On Scan Toast Cancel', id)
|
|
||||||
this.$root.socket.emit('cancel_scan', id)
|
this.$root.socket.emit('cancel_scan', id)
|
||||||
},
|
},
|
||||||
scanStart(data) {
|
scanStart(data) {
|
||||||
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
|
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
|
||||||
console.log('Scan start toast id', data.toastId)
|
|
||||||
this.$store.commit('scanners/addUpdate', data)
|
this.$store.commit('scanners/addUpdate', data)
|
||||||
},
|
},
|
||||||
scanProgress(data) {
|
scanProgress(data) {
|
||||||
console.log('scan progress', data)
|
|
||||||
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
||||||
if (existingScan && !isNaN(existingScan.toastId)) {
|
if (existingScan && !isNaN(existingScan.toastId)) {
|
||||||
data.toastId = existingScan.toastId
|
data.toastId = existingScan.toastId
|
||||||
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
|
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
|
||||||
|
} else {
|
||||||
|
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('scanners/addUpdate', data)
|
this.$store.commit('scanners/addUpdate', data)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.3.5",
|
"version": "1.4.0",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
<div class="flex-grow px-10">
|
<div class="flex-grow px-10">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<h1 class="text-2xl font-book leading-7">{{ title }}</h1>
|
<h1 class="text-2xl font-book leading-7">
|
||||||
|
{{ title }}<span v-if="isDeveloperMode"> ({{ audiobook.ino }})</span>
|
||||||
|
</h1>
|
||||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
||||||
<div class="w-min">
|
<div class="w-min">
|
||||||
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
<ui-tooltip :text="authorTooltipText" direction="bottom">
|
||||||
@ -84,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-tracks-table :tracks="tracks" :audiobook-id="audiobook.id" class="mt-6" />
|
<tables-tracks-table :tracks="tracks" :audiobook="audiobook" class="mt-6" />
|
||||||
|
|
||||||
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
|
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="updatingServerSettings" />
|
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="updateScannerFindCovers" />
|
||||||
<ui-tooltip :text="scannerFindCoversTooltip">
|
<ui-tooltip :text="scannerFindCoversTooltip">
|
||||||
<p class="pl-4 text-lg">Scanner find covers <span class="material-icons icon-text">info_outlined</span></p>
|
<p class="pl-4 text-lg">Scanner find covers <span class="material-icons icon-text">info_outlined</span></p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@ -123,8 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed bottom-0 left-0 w-10 h-10" @dblclick="setDeveloperMode"></div>
|
<div class="fixed bottom-0 left-0 w-10 h-10" @dblclick="setDeveloperMode"></div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -187,6 +185,11 @@ export default {
|
|||||||
toggleShowExperimentalFeatures() {
|
toggleShowExperimentalFeatures() {
|
||||||
this.$store.commit('setExperimentalFeatures', !this.showExperimentalFeatures)
|
this.$store.commit('setExperimentalFeatures', !this.showExperimentalFeatures)
|
||||||
},
|
},
|
||||||
|
updateScannerFindCovers(val) {
|
||||||
|
this.updateServerSettings({
|
||||||
|
scannerFindCovers: !!val
|
||||||
|
})
|
||||||
|
},
|
||||||
updateCoverStorageDestination(val) {
|
updateCoverStorageDestination(val) {
|
||||||
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
|
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
@ -194,10 +197,9 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateScannerParseSubtitle(val) {
|
updateScannerParseSubtitle(val) {
|
||||||
var payload = {
|
this.updateServerSettings({
|
||||||
scannerParseSubtitle: val
|
scannerParseSubtitle: !!val
|
||||||
}
|
})
|
||||||
this.updateServerSettings(payload)
|
|
||||||
},
|
},
|
||||||
updateServerSettings(payload) {
|
updateServerSettings(payload) {
|
||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
@ -216,15 +218,6 @@ export default {
|
|||||||
var value = !this.$store.state.developerMode
|
var value = !this.$store.state.developerMode
|
||||||
this.$store.commit('setDeveloperMode', value)
|
this.$store.commit('setDeveloperMode', value)
|
||||||
this.$toast.info(`Developer Mode ${value ? 'Enabled' : 'Disabled'}`)
|
this.$toast.info(`Developer Mode ${value ? 'Enabled' : 'Disabled'}`)
|
||||||
|
|
||||||
this.$axios
|
|
||||||
.$get('/test-fs')
|
|
||||||
.then((res) => {
|
|
||||||
console.log('Test FS Result', res)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to test FS', error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
scan() {
|
scan() {
|
||||||
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
|
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<p class="text-2xl">Logger</p>
|
<p class="text-2xl">Logger</p>
|
||||||
|
|
||||||
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" />
|
<div class="w-44">
|
||||||
|
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
@ -37,7 +37,7 @@ export default {
|
|||||||
if (this.$route.query.redirect) {
|
if (this.$route.query.redirect) {
|
||||||
this.$router.replace(this.$route.query.redirect)
|
this.$router.replace(this.$route.query.redirect)
|
||||||
} else {
|
} else {
|
||||||
this.$router.replace('/')
|
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,15 @@
|
|||||||
<article class="max-h-full overflow-y-auto relative flex flex-col rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter">
|
<article class="max-h-full overflow-y-auto relative flex flex-col rounded-md" @drop="drop" @dragover="dragover" @dragleave="dragleave" @dragenter="dragenter">
|
||||||
<h1 class="text-xl font-book px-8 pt-4 pb-2">Audiobook Uploader</h1>
|
<h1 class="text-xl font-book px-8 pt-4 pb-2">Audiobook Uploader</h1>
|
||||||
|
|
||||||
|
<div class="flex my-2 px-6">
|
||||||
|
<div class="w-1/3 px-2">
|
||||||
|
<!-- <ui-text-input-with-label v-model="title" label="Title" /> -->
|
||||||
|
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" label="Library" @input="libraryChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="w-2/3 px-2">
|
||||||
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId" label="Folder" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex my-2 px-6">
|
<div class="flex my-2 px-6">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="title" label="Title" />
|
<ui-text-input-with-label v-model="title" label="Title" />
|
||||||
@ -127,7 +136,9 @@ export default {
|
|||||||
showUploader: true,
|
showUploader: true,
|
||||||
validAudioFiles: [],
|
validAudioFiles: [],
|
||||||
validImageFiles: [],
|
validImageFiles: [],
|
||||||
invalidFiles: []
|
invalidFiles: [],
|
||||||
|
selectedLibraryId: null,
|
||||||
|
selectedFolderId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -140,13 +151,55 @@ export default {
|
|||||||
directory() {
|
directory() {
|
||||||
if (!this.author || !this.title) return ''
|
if (!this.author || !this.title) return ''
|
||||||
if (this.series) {
|
if (this.series) {
|
||||||
return Path.join('/audiobooks', this.author, this.series, this.title)
|
return Path.join(this.author, this.series, this.title)
|
||||||
} else {
|
} else {
|
||||||
return Path.join('/audiobooks', this.author, this.title)
|
return Path.join(this.author, this.title)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
libraries() {
|
||||||
|
return this.$store.state.libraries.libraries
|
||||||
|
},
|
||||||
|
libraryItems() {
|
||||||
|
return this.libraries.map((lib) => {
|
||||||
|
return {
|
||||||
|
value: lib.id,
|
||||||
|
text: lib.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectedLibrary() {
|
||||||
|
return this.libraries.find((lib) => lib.id === this.selectedLibraryId)
|
||||||
|
},
|
||||||
|
selectedFolder() {
|
||||||
|
if (!this.selectedLibrary) return null
|
||||||
|
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
|
||||||
|
},
|
||||||
|
folderItems() {
|
||||||
|
if (!this.selectedLibrary) return []
|
||||||
|
return this.selectedLibrary.folders.map((fold) => {
|
||||||
|
return {
|
||||||
|
value: fold.id,
|
||||||
|
text: fold.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
libraryChanged() {
|
||||||
|
if (!this.selectedLibrary && this.selectedFolderId) {
|
||||||
|
this.selectedFolderId = null
|
||||||
|
} else if (this.selectedFolderId) {
|
||||||
|
if (!this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)) {
|
||||||
|
this.selectedFolderId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setDefaultFolder()
|
||||||
|
},
|
||||||
|
setDefaultFolder() {
|
||||||
|
if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
|
||||||
|
this.selectedFolderId = this.selectedLibrary.folders[0].id
|
||||||
|
}
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.title = ''
|
this.title = ''
|
||||||
this.author = ''
|
this.author = ''
|
||||||
@ -218,12 +271,18 @@ export default {
|
|||||||
this.$toast.error('Must enter a title and author')
|
this.$toast.error('Must enter a title and author')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!this.selectedLibraryId || !this.selectedFolderId) {
|
||||||
|
this.$toast.error('Must select a library and folder')
|
||||||
|
return
|
||||||
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
var form = new FormData()
|
var form = new FormData()
|
||||||
form.set('title', this.title)
|
form.set('title', this.title)
|
||||||
form.set('author', this.author)
|
form.set('author', this.author)
|
||||||
form.set('series', this.series)
|
form.set('series', this.series)
|
||||||
|
form.set('library', this.selectedLibraryId)
|
||||||
|
form.set('folder', this.selectedFolderId)
|
||||||
|
|
||||||
var index = 0
|
var index = 0
|
||||||
var files = this.validAudioFiles.concat(this.validImageFiles)
|
var files = this.validAudioFiles.concat(this.validImageFiles)
|
||||||
@ -234,21 +293,21 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$post('/upload', form)
|
.$post('/upload', form)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
this.$toast.success('Audiobook Uploaded Successfully')
|
||||||
this.$toast.error(data.error)
|
this.reset()
|
||||||
} else {
|
|
||||||
this.$toast.success('Audiobook Uploaded Successfully')
|
|
||||||
this.reset()
|
|
||||||
}
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error('Oops, something went wrong...')
|
var errorMessage = error.response && error.response.data ? error.response.data : 'Oops, something went wrong...'
|
||||||
|
this.$toast.error(errorMessage)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
|
||||||
|
this.setDefaultFolder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -35,7 +35,7 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
return this.$axios.$patch('/api/serverSettings', updatePayload).then((result) => {
|
return this.$axios.$patch('/api/serverSettings', updatePayload).then((result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
commit('setServerSettings', result.settings)
|
commit('setServerSettings', result.serverSettings)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -78,6 +78,7 @@ export const mutations = {
|
|||||||
state.versionData = versionData
|
state.versionData = versionData
|
||||||
},
|
},
|
||||||
setServerSettings(state, settings) {
|
setServerSettings(state, settings) {
|
||||||
|
if (!settings) return
|
||||||
state.serverSettings = settings
|
state.serverSettings = settings
|
||||||
},
|
},
|
||||||
setStreamAudiobook(state, audiobook) {
|
setStreamAudiobook(state, audiobook) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.3.5",
|
"version": "1.4.0",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -557,7 +557,7 @@ class ApiController {
|
|||||||
}
|
}
|
||||||
var settingsUpdate = req.body
|
var settingsUpdate = req.body
|
||||||
if (!settingsUpdate || !isObject(settingsUpdate)) {
|
if (!settingsUpdate || !isObject(settingsUpdate)) {
|
||||||
return res.sendStatus(500)
|
return res.status(500).send('Invalid settings update object')
|
||||||
}
|
}
|
||||||
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
var madeUpdates = this.db.serverSettings.update(settingsUpdate)
|
||||||
if (madeUpdates) {
|
if (madeUpdates) {
|
||||||
|
@ -7,6 +7,8 @@ class BookFinder {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.openLibrary = new OpenLibrary()
|
this.openLibrary = new OpenLibrary()
|
||||||
this.libGen = new LibGen()
|
this.libGen = new LibGen()
|
||||||
|
|
||||||
|
this.verbose = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByISBN(isbn) {
|
async findByISBN(isbn) {
|
||||||
@ -92,17 +94,17 @@ class BookFinder {
|
|||||||
return b
|
return b
|
||||||
}).filter(b => {
|
}).filter(b => {
|
||||||
if (b.includesTitle) { // If search title was found in result title then skip over leven distance check
|
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}"`)
|
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
||||||
} else if (b.titleDistance > maxTitleDistance) {
|
} else if (b.titleDistance > maxTitleDistance) {
|
||||||
Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author) {
|
if (author) {
|
||||||
if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check
|
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}"`)
|
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
||||||
} else if (b.authorDistance > maxAuthorDistance) {
|
} else if (b.authorDistance > maxAuthorDistance) {
|
||||||
Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,28 +117,28 @@ class BookFinder {
|
|||||||
|
|
||||||
async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var books = await this.libGen.search(title)
|
var books = await this.libGen.search(title)
|
||||||
Logger.debug(`LibGen Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`LibGen Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`LibGen Search Error ${books.errorCode}`)
|
Logger.error(`LibGen Search Error ${books.errorCode}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
if (!booksFiltered.length && books.length) {
|
if (!booksFiltered.length && books.length) {
|
||||||
Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
||||||
}
|
}
|
||||||
return booksFiltered
|
return booksFiltered
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var books = await this.openLibrary.searchTitle(title)
|
var books = await this.openLibrary.searchTitle(title)
|
||||||
Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
if (!booksFiltered.length && books.length) {
|
if (!booksFiltered.length && books.length) {
|
||||||
Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
||||||
}
|
}
|
||||||
return booksFiltered
|
return booksFiltered
|
||||||
}
|
}
|
||||||
@ -145,7 +147,7 @@ class BookFinder {
|
|||||||
var books = []
|
var books = []
|
||||||
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
||||||
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
||||||
Logger.debug(`Book Search, title: "${title}", author: "${author}", provider: ${provider}`)
|
Logger.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
||||||
|
|
||||||
if (provider === 'libgen') {
|
if (provider === 'libgen') {
|
||||||
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
|
@ -117,7 +117,7 @@ class CoverController {
|
|||||||
|
|
||||||
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
|
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
|
||||||
|
|
||||||
audiobook.updateBookCover(coverPath)
|
audiobook.updateBookCover(coverPath, coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverPath
|
cover: coverPath
|
||||||
}
|
}
|
||||||
@ -169,7 +169,7 @@ class CoverController {
|
|||||||
|
|
||||||
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
|
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)
|
||||||
|
|
||||||
audiobook.updateBookCover(coverPath)
|
audiobook.updateBookCover(coverPath, coverFullPath)
|
||||||
return {
|
return {
|
||||||
cover: coverPath
|
cover: coverPath
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,32 @@ class Scanner {
|
|||||||
return filesUpdated
|
return filesUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, forceAudioFileScan) {
|
async searchForCover(audiobook) {
|
||||||
|
var options = {
|
||||||
|
titleDistance: 2,
|
||||||
|
authorDistance: 2
|
||||||
|
}
|
||||||
|
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
|
||||||
|
if (results.length) {
|
||||||
|
Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
|
||||||
|
|
||||||
|
// If the first cover result fails, attempt to download the second
|
||||||
|
for (let i = 0; i < results.length && i < 2; i++) {
|
||||||
|
|
||||||
|
// Downloads and updates the book cover
|
||||||
|
var result = await this.coverController.downloadCoverFromUrl(audiobook, results[i])
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan) {
|
||||||
// Always sync files and inode values
|
// Always sync files and inode values
|
||||||
var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData)
|
var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData)
|
||||||
if (hasUpdatedIno || filesInodeUpdated > 0) {
|
if (hasUpdatedIno || filesInodeUpdated > 0) {
|
||||||
@ -204,7 +229,7 @@ class Scanner {
|
|||||||
return ScanResult.REMOVED
|
return ScanResult.REMOVED
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdates = hasUpdatedIno || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
|
var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
|
||||||
|
|
||||||
// Check that audio tracks are in sequential order with no gaps
|
// Check that audio tracks are in sequential order with no gaps
|
||||||
if (existingAudiobook.checkUpdateMissingParts()) {
|
if (existingAudiobook.checkUpdateMissingParts()) {
|
||||||
@ -230,18 +255,13 @@ class Scanner {
|
|||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`)
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save changes and notify users
|
if (hasUpdates || version !== existingAudiobook.scanVersion) {
|
||||||
if (hasUpdates || !existingAudiobook.scanVersion) {
|
|
||||||
if (!existingAudiobook.scanVersion) {
|
|
||||||
Logger.debug(`[Scanner] No scan version "${existingAudiobook.title}" - updating`)
|
|
||||||
}
|
|
||||||
existingAudiobook.setChapters()
|
existingAudiobook.setChapters()
|
||||||
|
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
|
|
||||||
existingAudiobook.setLastScan(version)
|
existingAudiobook.setLastScan(version)
|
||||||
await this.db.updateAudiobook(existingAudiobook)
|
await this.db.updateAudiobook(existingAudiobook)
|
||||||
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
|
||||||
|
|
||||||
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated`)
|
||||||
|
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +271,7 @@ class Scanner {
|
|||||||
async scanNewAudiobook(audiobookData) {
|
async scanNewAudiobook(audiobookData) {
|
||||||
if (!audiobookData.audioFiles.length) {
|
if (!audiobookData.audioFiles.length) {
|
||||||
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
|
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path)
|
||||||
return ScanResult.NOTHING
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var audiobook = new Audiobook()
|
var audiobook = new Audiobook()
|
||||||
@ -261,13 +281,14 @@ class Scanner {
|
|||||||
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles)
|
||||||
if (!audiobook.tracks.length) {
|
if (!audiobook.tracks.length) {
|
||||||
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
|
||||||
return ScanResult.NOTHING
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for desc.txt and reader.txt and update
|
// Look for desc.txt and reader.txt and update
|
||||||
await audiobook.saveDataFromTextFiles()
|
await audiobook.saveDataFromTextFiles()
|
||||||
|
|
||||||
if (audiobook.hasEmbeddedCoverArt) {
|
// Extract embedded cover art if cover is not already in directory
|
||||||
|
if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) {
|
||||||
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
var outputCoverDirs = this.getCoverDirectory(audiobook)
|
||||||
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath)
|
||||||
if (relativeDir) {
|
if (relativeDir) {
|
||||||
@ -289,31 +310,104 @@ class Scanner {
|
|||||||
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
|
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
|
||||||
await this.db.insertEntity('audiobook', audiobook)
|
await this.db.insertEntity('audiobook', audiobook)
|
||||||
this.emitter('audiobook_added', audiobook.toJSONMinified())
|
this.emitter('audiobook_added', audiobook.toJSONMinified())
|
||||||
return ScanResult.ADDED
|
return audiobook
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanAudiobookData(audiobookData, forceAudioFileScan = false) {
|
async scanAudiobookData(audiobookData, forceAudioFileScan = false) {
|
||||||
var scannerFindCovers = this.db.serverSettings.scannerFindCovers
|
var scannerFindCovers = this.db.serverSettings.scannerFindCovers
|
||||||
var libraryId = audiobookData.libraryId
|
var libraryId = audiobookData.libraryId
|
||||||
var audiobooksInLibrary = this.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var folderId = audiobookData.folderId
|
||||||
var existingAudiobook = audiobooksInLibrary.find(a => a.ino === audiobookData.ino)
|
|
||||||
|
var hasUpdatedLibraryOrFolder = false
|
||||||
|
|
||||||
|
var existingAudiobook = this.audiobooks.find(ab => ab.ino === audiobookData.ino)
|
||||||
|
|
||||||
|
// Make sure existing audiobook has the same library & folder id
|
||||||
|
if (existingAudiobook && (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId)) {
|
||||||
|
var existingAudiobookLibrary = this.db.libraries.find(lib => lib.id === existingAudiobook.libraryId)
|
||||||
|
|
||||||
|
if (!existingAudiobookLibrary) {
|
||||||
|
Logger.error(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library that no longer exists ${existingAudiobook.libraryId}`)
|
||||||
|
} else if (existingAudiobook.libraryId !== libraryId) {
|
||||||
|
Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library "${existingAudiobookLibrary.name}"`)
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different folder "${existingAudiobook.folderId}" of library "${existingAudiobookLibrary.name}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingAudiobook.libraryId = libraryId
|
||||||
|
existingAudiobook.folderId = folderId
|
||||||
|
hasUpdatedLibraryOrFolder = true
|
||||||
|
Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// var audiobooksInLibrary = this.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||||
|
// var existingAudiobook = audiobooksInLibrary.find(a => a.ino === audiobookData.ino)
|
||||||
|
|
||||||
// inode value may change when using shared drives, update inode if matching path is found
|
// inode value may change when using shared drives, update inode if matching path is found
|
||||||
// Note: inode will not change on rename
|
// Note: inode will not change on rename
|
||||||
var hasUpdatedIno = false
|
var hasUpdatedIno = false
|
||||||
if (!existingAudiobook) {
|
if (!existingAudiobook) {
|
||||||
// check an audiobook exists with matching path, then update inodes
|
// check an audiobook exists with matching path, then update inodes
|
||||||
existingAudiobook = audiobooksInLibrary.find(a => a.path === audiobookData.path)
|
existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path)
|
||||||
if (existingAudiobook) {
|
if (existingAudiobook) {
|
||||||
|
var oldIno = existingAudiobook.ino
|
||||||
existingAudiobook.ino = audiobookData.ino
|
existingAudiobook.ino = audiobookData.ino
|
||||||
|
Logger.debug(`[Scanner] Scan Audiobook Data: Updated inode from "${oldIno}" to "${existingAudiobook.ino}"`)
|
||||||
hasUpdatedIno = true
|
hasUpdatedIno = true
|
||||||
|
|
||||||
|
if (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId) {
|
||||||
|
Logger.warn(`[Scanner] Audiobook found by path is in a different library or folder, ${existingAudiobook.libraryId}/${existingAudiobook.folderId} should be ${libraryId}/${folderId}`)
|
||||||
|
|
||||||
|
existingAudiobook.libraryId = libraryId
|
||||||
|
existingAudiobook.folderId = folderId
|
||||||
|
hasUpdatedLibraryOrFolder = true
|
||||||
|
Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scanResult = null
|
||||||
|
var finalAudiobook = null
|
||||||
|
|
||||||
if (existingAudiobook) {
|
if (existingAudiobook) {
|
||||||
return this.scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, forceAudioFileScan)
|
finalAudiobook = existingAudiobook
|
||||||
|
|
||||||
|
scanResult = await this.scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan)
|
||||||
|
|
||||||
|
if (scanResult === ScanResult.REMOVED || scanResult === ScanResult.NOTHING) {
|
||||||
|
finalAudiobook = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalAudiobook = await this.scanNewAudiobook(audiobookData)
|
||||||
|
|
||||||
|
scanResult = finalAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
|
||||||
|
|
||||||
|
if (finalAudiobook === ScanResult.NOTHING) {
|
||||||
|
finalAudiobook = null
|
||||||
|
scanResult = ScanResult.NOTHING
|
||||||
|
} else {
|
||||||
|
scanResult = ScanResult.ADDED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.scanNewAudiobook(audiobookData)
|
|
||||||
|
// Scan for cover if enabled and has no cover
|
||||||
|
if (finalAudiobook && scannerFindCovers && !finalAudiobook.cover) {
|
||||||
|
if (finalAudiobook.book.shouldSearchForCover) {
|
||||||
|
var updatedCover = await this.searchForCover(finalAudiobook)
|
||||||
|
|
||||||
|
finalAudiobook.book.updateLastCoverSearch(updatedCover)
|
||||||
|
|
||||||
|
if (updatedCover && scanResult === ScanResult.UPTODATE) {
|
||||||
|
scanResult = ScanResult.UPDATED
|
||||||
|
}
|
||||||
|
await this.db.updateAudiobook(finalAudiobook)
|
||||||
|
this.emitter('audiobook_updated', finalAudiobook.toJSONMinified())
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[Scanner] Audiobook "${finalAudiobook.title}" cover already scanned - not re-scanning`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanResult
|
||||||
}
|
}
|
||||||
|
|
||||||
async scan(libraryId, forceAudioFileScan = false) {
|
async scan(libraryId, forceAudioFileScan = false) {
|
||||||
@ -331,15 +425,19 @@ class Scanner {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emitter('scan_start', {
|
var scanPayload = {
|
||||||
id: libraryId,
|
id: libraryId,
|
||||||
name: library.name,
|
name: library.name,
|
||||||
scanType: 'library',
|
scanType: 'library',
|
||||||
folders: library.folders.length
|
folders: library.folders.length
|
||||||
})
|
}
|
||||||
|
this.emitter('scan_start', scanPayload)
|
||||||
Logger.info(`[Scanner] Starting scan of library "${library.name}" with ${library.folders.length} folders`)
|
Logger.info(`[Scanner] Starting scan of library "${library.name}" with ${library.folders.length} folders`)
|
||||||
|
|
||||||
this.librariesScanning.push(libraryId)
|
library.lastScan = Date.now()
|
||||||
|
await this.db.updateEntity('library', library)
|
||||||
|
|
||||||
|
this.librariesScanning.push(scanPayload)
|
||||||
|
|
||||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||||
|
|
||||||
@ -351,7 +449,11 @@ class Scanner {
|
|||||||
// Update ino if inos are not set
|
// Update ino if inos are not set
|
||||||
var shouldUpdateIno = ab.hasMissingIno
|
var shouldUpdateIno = ab.hasMissingIno
|
||||||
if (shouldUpdateIno) {
|
if (shouldUpdateIno) {
|
||||||
Logger.debug(`Updating inos for ${ab.title}`)
|
var filesWithMissingIno = ab.getFilesWithMissingIno()
|
||||||
|
|
||||||
|
Logger.debug(`\n\Updating inos for "${ab.title}"`)
|
||||||
|
Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno)
|
||||||
|
|
||||||
var hasUpdates = await ab.checkUpdateInos()
|
var hasUpdates = await ab.checkUpdateInos()
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
await this.db.updateAudiobook(ab)
|
await this.db.updateAudiobook(ab)
|
||||||
@ -373,10 +475,9 @@ class Scanner {
|
|||||||
audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
|
audiobookDataFound = audiobookDataFound.filter(abd => abd.ino)
|
||||||
|
|
||||||
if (this.cancelLibraryScan[libraryId]) {
|
if (this.cancelLibraryScan[libraryId]) {
|
||||||
console.log('2', this.cancelLibraryScan)
|
|
||||||
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
||||||
delete this.cancelLibraryScan[libraryId]
|
delete this.cancelLibraryScan[libraryId]
|
||||||
this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
|
this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
|
||||||
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: null })
|
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: null })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -401,10 +502,9 @@ class Scanner {
|
|||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||||
}
|
}
|
||||||
if (this.cancelLibraryScan[libraryId]) {
|
if (this.cancelLibraryScan[libraryId]) {
|
||||||
console.log('1', this.cancelLibraryScan)
|
|
||||||
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
||||||
delete this.cancelLibraryScan[libraryId]
|
delete this.cancelLibraryScan[libraryId]
|
||||||
this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
|
this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
|
||||||
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
|
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -429,7 +529,6 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (this.cancelLibraryScan[libraryId]) {
|
if (this.cancelLibraryScan[libraryId]) {
|
||||||
console.log(this.cancelLibraryScan)
|
|
||||||
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
Logger.info(`[Scanner] Canceling scan ${libraryId}`)
|
||||||
delete this.cancelLibraryScan[libraryId]
|
delete this.cancelLibraryScan[libraryId]
|
||||||
break
|
break
|
||||||
@ -437,7 +536,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
|
||||||
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`)
|
||||||
this.librariesScanning = this.librariesScanning.filter(l => l !== libraryId)
|
this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId)
|
||||||
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
|
this.emitter('scan_complete', { id: libraryId, name: library.name, scanType: 'library', results: scanResults })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,6 +556,10 @@ class Scanner {
|
|||||||
Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`)
|
Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
|
if (!folder.libraryId) {
|
||||||
|
Logger.fatal(`[Scanner] Folder does not have a library id set...`, folder)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`)
|
Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`)
|
||||||
return this.scanAudiobook(folder, audiobook.fullPath, true)
|
return this.scanAudiobook(folder, audiobook.fullPath, true)
|
||||||
@ -525,6 +628,7 @@ class Scanner {
|
|||||||
Logger.error(`[Scanner] Folder "${folderId}" not found in library "${library.name}" for scan library updates`)
|
Logger.error(`[Scanner] Folder "${folderId}" not found in library "${library.name}" for scan library updates`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`)
|
||||||
|
|
||||||
var bookGroupingResults = {}
|
var bookGroupingResults = {}
|
||||||
@ -594,6 +698,22 @@ class Scanner {
|
|||||||
// Group files by book
|
// Group files by book
|
||||||
for (const folderId in folderGroups) {
|
for (const folderId in folderGroups) {
|
||||||
var libraryId = folderGroups[folderId].libraryId
|
var libraryId = folderGroups[folderId].libraryId
|
||||||
|
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||||
|
if (!library) {
|
||||||
|
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var folder = library.getFolderById(folderId)
|
||||||
|
if (!folder) {
|
||||||
|
Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`)
|
||||||
|
|
||||||
|
Logger.debug(`Looking at folders in library "${library.name}" for folderid ${folderId}`)
|
||||||
|
library.folders.forEach((fold) => {
|
||||||
|
Logger.debug(`Folder "${fold.id}" "${fold.fullPath}"`)
|
||||||
|
})
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
|
var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
|
||||||
var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true)
|
var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true)
|
||||||
var folderScanResults = await this.scanFolderUpdates(libraryId, folderId, fileUpdateBookGroup)
|
var folderScanResults = await this.scanFolderUpdates(libraryId, folderId, fileUpdateBookGroup)
|
||||||
@ -602,18 +722,6 @@ class Scanner {
|
|||||||
|
|
||||||
Logger.debug(`[Scanner] Finished scanning file changes, results:`, libraryScanResults)
|
Logger.debug(`[Scanner] Finished scanning file changes, results:`, libraryScanResults)
|
||||||
return libraryScanResults
|
return libraryScanResults
|
||||||
// var relfilepaths = filepaths.map(path => path.replace(this.AudiobookPath, ''))
|
|
||||||
// var fileGroupings = groupFilesIntoAudiobookPaths(relfilepaths, true)
|
|
||||||
|
|
||||||
// var results = []
|
|
||||||
// for (const dir in fileGroupings) {
|
|
||||||
// Logger.debug(`[Scanner] Check dir ${dir}`)
|
|
||||||
// var fullPath = Path.join(this.AudiobookPath, dir)
|
|
||||||
// var result = await this.checkDir(fullPath)
|
|
||||||
// Logger.debug(`[Scanner] Check dir result ${result}`)
|
|
||||||
// results.push(result)
|
|
||||||
// }
|
|
||||||
// return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanCovers() {
|
async scanCovers() {
|
||||||
|
@ -178,8 +178,6 @@ class Server {
|
|||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/test-fs', this.authMiddleware.bind(this), this.testFileSystem.bind(this))
|
|
||||||
|
|
||||||
// Used in development to set-up streams without authentication
|
// Used in development to set-up streams without authentication
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.use('/test-hls', this.hlsController.router)
|
app.use('/test-hls', this.hlsController.router)
|
||||||
@ -292,7 +290,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelScan(id) {
|
cancelScan(id) {
|
||||||
console.log('Cancel scan', id)
|
Logger.debug('[Server] Cancel scan', id)
|
||||||
this.scanner.cancelLibraryScan[id] = true
|
this.scanner.cancelLibraryScan[id] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,26 +342,33 @@ class Server {
|
|||||||
var title = req.body.title
|
var title = req.body.title
|
||||||
var author = req.body.author
|
var author = req.body.author
|
||||||
var series = req.body.series
|
var series = req.body.series
|
||||||
|
var libraryId = req.body.library
|
||||||
|
var folderId = req.body.folder
|
||||||
|
|
||||||
|
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
||||||
|
if (!library) {
|
||||||
|
return res.status(500).error(`Library not found with id ${libraryId}`)
|
||||||
|
}
|
||||||
|
var folder = library.folders.find(fold => fold.id === folderId)
|
||||||
|
if (!folder) {
|
||||||
|
return res.status(500).error(`Folder not found with id ${folderId} in library ${library.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (!files.length || !title || !author) {
|
if (!files.length || !title || !author) {
|
||||||
return res.json({
|
return res.status(500).error(`Invalid post data`)
|
||||||
error: 'Invalid post data received'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputDirectory = ''
|
var outputDirectory = ''
|
||||||
if (series && series.length && series !== 'null') {
|
if (series && series.length && series !== 'null') {
|
||||||
outputDirectory = Path.join(this.AudiobookPath, author, series, title)
|
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
||||||
} else {
|
} else {
|
||||||
outputDirectory = Path.join(this.AudiobookPath, author, title)
|
outputDirectory = Path.join(folder.fullPath, author, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
var exists = await fs.pathExists(outputDirectory)
|
var exists = await fs.pathExists(outputDirectory)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
||||||
return res.json({
|
return res.status(500).error(`Directory "${outputDirectory}" already exists`)
|
||||||
error: `Directory "${outputDirectory}" already exists`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.ensureDir(outputDirectory)
|
await fs.ensureDir(outputDirectory)
|
||||||
@ -438,7 +443,8 @@ class Server {
|
|||||||
metadataPath: this.MetadataPath,
|
metadataPath: this.MetadataPath,
|
||||||
configPath: this.ConfigPath,
|
configPath: this.ConfigPath,
|
||||||
user: client.user.toJSONForBrowser(),
|
user: client.user.toJSONForBrowser(),
|
||||||
stream: client.stream || null
|
stream: client.stream || null,
|
||||||
|
librariesScanning: this.scanner.librariesScanning
|
||||||
}
|
}
|
||||||
client.socket.emit('init', initialPayload)
|
client.socket.emit('init', initialPayload)
|
||||||
|
|
||||||
@ -463,26 +469,5 @@ class Server {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async testFileSystem(req, res) {
|
|
||||||
Logger.debug(`[Server] Running fs test`)
|
|
||||||
var paths = await fs.readdir(global.appRoot)
|
|
||||||
Logger.debug(paths)
|
|
||||||
var pathMap = {}
|
|
||||||
if (paths && paths.length) {
|
|
||||||
for (let i = 0; i < paths.length; i++) {
|
|
||||||
var fullPath = Path.join(global.appRoot, paths[i])
|
|
||||||
Logger.debug('Checking path', fullPath)
|
|
||||||
var isDirectory = fs.lstatSync(fullPath).isDirectory()
|
|
||||||
if (isDirectory) {
|
|
||||||
var _paths = await fs.readdir(fullPath)
|
|
||||||
Logger.debug(_paths)
|
|
||||||
pathMap[paths[i]] = _paths
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.debug('Finished fs test')
|
|
||||||
res.json(pathMap)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = Server
|
module.exports = Server
|
@ -45,9 +45,9 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}).on('rename', (path, pathNext) => {
|
}).on('rename', (path, pathNext) => {
|
||||||
this.onRename(library.id, path, pathNext)
|
this.onRename(library.id, path, pathNext)
|
||||||
}).on('error', (error) => {
|
}).on('error', (error) => {
|
||||||
Logger.error(`[FolderWatcher] ${error}`)
|
Logger.error(`[Watcher] ${error}`)
|
||||||
}).on('ready', () => {
|
}).on('ready', () => {
|
||||||
Logger.info('[FolderWatcher] Ready')
|
Logger.info(`[Watcher] "${library.name}" Ready`)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.libraryWatchers.push({
|
this.libraryWatchers.push({
|
||||||
@ -107,18 +107,6 @@ class FolderWatcher extends EventEmitter {
|
|||||||
onFileRemoved(libraryId, path) {
|
onFileRemoved(libraryId, path) {
|
||||||
Logger.debug('[Watcher] File Removed', path)
|
Logger.debug('[Watcher] File Removed', path)
|
||||||
this.addFileUpdate(libraryId, path, 'deleted')
|
this.addFileUpdate(libraryId, path, 'deleted')
|
||||||
// var dir = Path.dirname(path)
|
|
||||||
// if (dir === this.AudiobookPath) {
|
|
||||||
// Logger.debug('New File added to root dir, ignoring it')
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.pendingFiles.push(path)
|
|
||||||
// clearTimeout(this.pendingTimeout)
|
|
||||||
// this.pendingTimeout = setTimeout(() => {
|
|
||||||
// this.emit('files', this.pendingFiles.map(f => f))
|
|
||||||
// this.pendingFiles = []
|
|
||||||
// }, this.pendingDelay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileUpdated(path) {
|
onFileUpdated(path) {
|
||||||
@ -128,18 +116,6 @@ class FolderWatcher extends EventEmitter {
|
|||||||
onRename(libraryId, pathFrom, pathTo) {
|
onRename(libraryId, pathFrom, pathTo) {
|
||||||
Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
|
Logger.debug(`[Watcher] Rename ${pathFrom} => ${pathTo}`)
|
||||||
this.addFileUpdate(libraryId, pathTo, 'renamed')
|
this.addFileUpdate(libraryId, pathTo, 'renamed')
|
||||||
// var dir = Path.dirname(pathTo)
|
|
||||||
// if (dir === this.AudiobookPath) {
|
|
||||||
// Logger.debug('New File added to root dir, ignoring it')
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.pendingFiles.push(pathTo)
|
|
||||||
// clearTimeout(this.pendingTimeout)
|
|
||||||
// this.pendingTimeout = setTimeout(() => {
|
|
||||||
// this.emit('files', this.pendingFiles.map(f => f))
|
|
||||||
// this.pendingFiles = []
|
|
||||||
// }, this.pendingDelay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addFileUpdate(libraryId, path, type) {
|
addFileUpdate(libraryId, path, type) {
|
||||||
@ -167,7 +143,7 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var relPath = path.replace(folder.fullPath, '')
|
var relPath = path.replace(folder.fullPath, '')
|
||||||
Logger.debug(`[Watcher] New File in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
|
Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
|
||||||
|
|
||||||
this.pendingFileUpdates.push({
|
this.pendingFileUpdates.push({
|
||||||
path,
|
path,
|
||||||
|
@ -118,6 +118,7 @@ class Audiobook {
|
|||||||
|
|
||||||
get _audioFiles() { return this.audioFiles || [] }
|
get _audioFiles() { return this.audioFiles || [] }
|
||||||
get _otherFiles() { return this.otherFiles || [] }
|
get _otherFiles() { return this.otherFiles || [] }
|
||||||
|
get _tracks() { return this.tracks || [] }
|
||||||
|
|
||||||
get ebooks() {
|
get ebooks() {
|
||||||
return this.otherFiles.filter(file => file.filetype === 'ebook')
|
return this.otherFiles.filter(file => file.filetype === 'ebook')
|
||||||
@ -128,13 +129,21 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get hasMissingIno() {
|
get hasMissingIno() {
|
||||||
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || (this.tracks || []).find(t => !t.ino)
|
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasEmbeddedCoverArt() {
|
get hasEmbeddedCoverArt() {
|
||||||
return !!this._audioFiles.find(af => af.embeddedCoverArt)
|
return !!this._audioFiles.find(af => af.embeddedCoverArt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMP: Issue with inodes not always being set for files
|
||||||
|
getFilesWithMissingIno() {
|
||||||
|
var afs = this._audioFiles.filter(af => !af.ino)
|
||||||
|
var ofs = this._otherFiles.filter(f => !f.ino)
|
||||||
|
var ts = this._tracks.filter(t => !t.ino)
|
||||||
|
return afs.concat(ofs).concat(ts)
|
||||||
|
}
|
||||||
|
|
||||||
bookToJSON() {
|
bookToJSON() {
|
||||||
return this.book ? this.book.toJSON() : null
|
return this.book ? this.book.toJSON() : null
|
||||||
}
|
}
|
||||||
@ -332,8 +341,9 @@ class Audiobook {
|
|||||||
if (this.otherFiles && this.otherFiles.length) {
|
if (this.otherFiles && this.otherFiles.length) {
|
||||||
var imageFile = this.otherFiles.find(f => f.filetype === 'image')
|
var imageFile = this.otherFiles.find(f => f.filetype === 'image')
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
data.coverFullPath = imageFile.fullPath
|
data.coverFullPath = Path.normalize(imageFile.fullPath)
|
||||||
data.cover = Path.normalize(Path.join(`/s/book/${this.id}`, imageFile.path))
|
var relImagePath = imageFile.path.replace(this.path, '')
|
||||||
|
data.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,9 +397,9 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cover Url may be the same, this ensures the lastUpdate is updated
|
// Cover Url may be the same, this ensures the lastUpdate is updated
|
||||||
updateBookCover(cover) {
|
updateBookCover(cover, coverFullPath) {
|
||||||
if (!this.book) return false
|
if (!this.book) return false
|
||||||
return this.book.updateCover(cover)
|
return this.book.updateCover(cover, coverFullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAudioTracks(orderedFileData) {
|
updateAudioTracks(orderedFileData) {
|
||||||
@ -479,7 +489,7 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
|
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
|
||||||
var descriptionTxt = this.otherFiles.find(file => file.filename === 'desc.txt')
|
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
|
||||||
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
|
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
|
||||||
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
||||||
if (newDescription) {
|
if (newDescription) {
|
||||||
@ -489,7 +499,7 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If reader.txt is new or forcing rescan then read it and update narrarator (will overwrite)
|
// If reader.txt is new or forcing rescan then read it and update narrarator (will overwrite)
|
||||||
var readerTxt = this.otherFiles.find(file => file.filename === 'reader.txt')
|
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
|
||||||
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
||||||
var newReader = await readTextFile(readerTxt.fullPath)
|
var newReader = await readTextFile(readerTxt.fullPath)
|
||||||
if (newReader) {
|
if (newReader) {
|
||||||
@ -523,7 +533,7 @@ class Audiobook {
|
|||||||
var oldFormat = this.book.cover
|
var oldFormat = this.book.cover
|
||||||
|
|
||||||
// Update book cover path to new format
|
// Update book cover path to new format
|
||||||
this.book.fullCoverPath = Path.join(this.fullPath, this.book.cover.substr(7))
|
this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(7)))
|
||||||
this.book.cover = Path.normalize(coverStripped.replace(this.path, `/s/book/${this.id}`))
|
this.book.cover = Path.normalize(coverStripped.replace(this.path, `/s/book/${this.id}`))
|
||||||
Logger.debug(`[Audiobook] updated book cover to new format "${oldFormat}" => "${this.book.cover}"`)
|
Logger.debug(`[Audiobook] updated book cover to new format "${oldFormat}" => "${this.book.cover}"`)
|
||||||
}
|
}
|
||||||
@ -531,10 +541,10 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if book was removed from book dir
|
// Check if book was removed from book dir
|
||||||
if (this.book.cover && this.book.cover.substr(1).startsWith('s/book/')) {
|
if (this.book.cover && this.book.cover.substr(1).startsWith('s\\book\\')) {
|
||||||
// Fixing old cover paths
|
// Fixing old cover paths
|
||||||
if (!this.book.coverFullPath) {
|
if (!this.book.coverFullPath) {
|
||||||
this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length))
|
this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length)))
|
||||||
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
@ -550,7 +560,7 @@ class Audiobook {
|
|||||||
if (this.book.cover && this.book.cover.substr(1).startsWith('metadata')) {
|
if (this.book.cover && this.book.cover.substr(1).startsWith('metadata')) {
|
||||||
// Fixing old cover paths
|
// Fixing old cover paths
|
||||||
if (!this.book.coverFullPath) {
|
if (!this.book.coverFullPath) {
|
||||||
this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length))
|
this.book.coverFullPath = Path.normalize(Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)))
|
||||||
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`)
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
@ -575,11 +585,12 @@ class Audiobook {
|
|||||||
// If no cover set and image file exists then use it
|
// If no cover set and image file exists then use it
|
||||||
if (!this.book.cover && imageFiles.length) {
|
if (!this.book.cover && imageFiles.length) {
|
||||||
var imagePathRelativeToBook = imageFiles[0].path.replace(this.path, '')
|
var imagePathRelativeToBook = imageFiles[0].path.replace(this.path, '')
|
||||||
this.book.cover = Path.join(`/s/book/${this.id}`, imagePathRelativeToBook)
|
this.book.cover = Path.normalize(Path.join(`/s/book/${this.id}`, imagePathRelativeToBook))
|
||||||
this.book.coverFullPath = imageFiles[0].fullPath
|
this.book.coverFullPath = imageFiles[0].fullPath
|
||||||
Logger.info(`[Audiobook] Local cover was set to "${this.book.cover}" | "${this.title}"`)
|
Logger.info(`[Audiobook] Local cover was set to "${this.book.cover}" | "${this.title}"`)
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,14 @@ class Book {
|
|||||||
this.cover = null
|
this.cover = null
|
||||||
this.coverFullPath = null
|
this.coverFullPath = null
|
||||||
this.genres = []
|
this.genres = []
|
||||||
|
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
|
|
||||||
|
// Should not continue looking up a cover when it is not findable
|
||||||
|
this.lastCoverSearch = null
|
||||||
|
this.lastCoverSearchTitle = null
|
||||||
|
this.lastCoverSearchAuthor = null
|
||||||
|
|
||||||
if (book) {
|
if (book) {
|
||||||
this.construct(book)
|
this.construct(book)
|
||||||
}
|
}
|
||||||
@ -33,6 +39,12 @@ class Book {
|
|||||||
get _author() { return this.author || '' }
|
get _author() { return this.author || '' }
|
||||||
get _series() { return this.series || '' }
|
get _series() { return this.series || '' }
|
||||||
|
|
||||||
|
get shouldSearchForCover() {
|
||||||
|
if (this.author !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
|
||||||
|
var timeSinceLastSearch = Date.now() - this.lastCoverSearch
|
||||||
|
return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
|
||||||
|
}
|
||||||
|
|
||||||
construct(book) {
|
construct(book) {
|
||||||
this.olid = book.olid
|
this.olid = book.olid
|
||||||
this.title = book.title
|
this.title = book.title
|
||||||
@ -50,6 +62,9 @@ class Book {
|
|||||||
this.coverFullPath = book.coverFullPath || null
|
this.coverFullPath = book.coverFullPath || null
|
||||||
this.genres = book.genres
|
this.genres = book.genres
|
||||||
this.lastUpdate = book.lastUpdate || Date.now()
|
this.lastUpdate = book.lastUpdate || Date.now()
|
||||||
|
this.lastCoverSearch = book.lastCoverSearch || null
|
||||||
|
this.lastCoverSearchTitle = book.lastCoverSearchTitle || null
|
||||||
|
this.lastCoverSearchAuthor = book.lastCoverSearchAuthor || null
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -69,7 +84,10 @@ class Book {
|
|||||||
cover: this.cover,
|
cover: this.cover,
|
||||||
coverFullPath: this.coverFullPath,
|
coverFullPath: this.coverFullPath,
|
||||||
genres: this.genres,
|
genres: this.genres,
|
||||||
lastUpdate: this.lastUpdate
|
lastUpdate: this.lastUpdate,
|
||||||
|
lastCoverSearch: this.lastCoverSearch,
|
||||||
|
lastCoverSearchTitle: this.lastCoverSearchTitle,
|
||||||
|
lastCoverSearchAuthor: this.lastCoverSearchAuthor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +124,9 @@ class Book {
|
|||||||
this.coverFullPath = data.coverFullPath || null
|
this.coverFullPath = data.coverFullPath || null
|
||||||
this.genres = data.genres || []
|
this.genres = data.genres || []
|
||||||
this.lastUpdate = Date.now()
|
this.lastUpdate = Date.now()
|
||||||
|
this.lastCoverSearch = data.lastCoverSearch || null
|
||||||
|
this.lastCoverSearchTitle = data.lastCoverSearchTitle || null
|
||||||
|
this.lastCoverSearchAuthor = data.lastCoverSearchAuthor || null
|
||||||
|
|
||||||
if (data.author) {
|
if (data.author) {
|
||||||
this.setParseAuthor(this.author)
|
this.setParseAuthor(this.author)
|
||||||
@ -119,6 +140,7 @@ class Book {
|
|||||||
// If updating to local cover then normalize path
|
// If updating to local cover then normalize path
|
||||||
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
|
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
|
||||||
payload.cover = Path.normalize(payload.cover)
|
payload.cover = Path.normalize(payload.cover)
|
||||||
|
if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,10 +176,19 @@ class Book {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCover(cover) {
|
updateLastCoverSearch(coverWasFound) {
|
||||||
|
this.lastCoverSearch = coverWasFound ? null : Date.now()
|
||||||
|
this.lastCoverSearchAuthor = coverWasFound ? null : this.author
|
||||||
|
this.lastCoverSearchTitle = coverWasFound ? null : this.title
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCover(cover, coverFullPath) {
|
||||||
if (!cover) return false
|
if (!cover) return false
|
||||||
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
||||||
cover = Path.normalize(cover)
|
cover = Path.normalize(cover)
|
||||||
|
this.coverFullPath = Path.normalize(coverFullPath)
|
||||||
|
} else {
|
||||||
|
this.coverFullPath = cover
|
||||||
}
|
}
|
||||||
this.cover = cover
|
this.cover = cover
|
||||||
this.lastUpdate = Date.now()
|
this.lastUpdate = Date.now()
|
||||||
|
@ -6,6 +6,8 @@ class Library {
|
|||||||
this.name = null
|
this.name = null
|
||||||
this.folders = []
|
this.folders = []
|
||||||
|
|
||||||
|
this.lastScan = 0
|
||||||
|
|
||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
|
|
||||||
@ -74,6 +76,7 @@ class Library {
|
|||||||
|
|
||||||
if (newFolders.length) {
|
if (newFolders.length) {
|
||||||
newFolders.forEach((folderData) => {
|
newFolders.forEach((folderData) => {
|
||||||
|
folderData.libraryId = this.id
|
||||||
var newFolder = new Folder()
|
var newFolder = new Folder()
|
||||||
newFolder.setData(folderData)
|
newFolder.setData(folderData)
|
||||||
this.folders.push(newFolder)
|
this.folders.push(newFolder)
|
||||||
@ -91,5 +94,9 @@ class Library {
|
|||||||
checkFullPathInLibrary(fullPath) {
|
checkFullPathInLibrary(fullPath) {
|
||||||
return this.folders.find(folder => fullPath.startsWith(folder.fullPath))
|
return this.folders.find(folder => fullPath.startsWith(folder.fullPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFolderById(id) {
|
||||||
|
return this.folders.find(folder => folder.id === id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Library
|
module.exports = Library
|
@ -107,6 +107,7 @@ function getFileType(ext) {
|
|||||||
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
||||||
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
|
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
|
||||||
if (ext_cleaned === 'nfo') return 'info'
|
if (ext_cleaned === 'nfo') return 'info'
|
||||||
|
if (ext_cleaned === 'txt') return 'text'
|
||||||
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
|
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
|
||||||
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
|
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
@ -243,6 +244,7 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {})
|
|||||||
var audiobookDir = Path.normalize(audiobookPath).replace(folder.fullPath, '').slice(1)
|
var audiobookDir = Path.normalize(audiobookPath).replace(folder.fullPath, '').slice(1)
|
||||||
var audiobookData = getAudiobookDataFromDir(folder.fullPath, audiobookDir, parseSubtitle)
|
var audiobookData = getAudiobookDataFromDir(folder.fullPath, audiobookDir, parseSubtitle)
|
||||||
var audiobook = {
|
var audiobook = {
|
||||||
|
ino: await getIno(audiobookData.fullPath),
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
...audiobookData,
|
...audiobookData,
|
||||||
|
Loading…
Reference in New Issue
Block a user