Scan for covers now saves covers, server settings to save covers in audiobook folder

This commit is contained in:
Mark Cooper 2021-10-01 20:29:00 -05:00
parent 8d9d5a8d1b
commit 3dd8dc6dd4
9 changed files with 83 additions and 31 deletions

View File

@ -30,7 +30,7 @@ export default {
}
},
className() {
if (this.disabled) return 'bg-bg cursor-not-allowed'
if (this.disabled) return this.toggleValue ? `bg-${this.onColor} cursor-not-allowed` : `bg-${this.offColor} cursor-not-allowed`
return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}`
},
switchClassName() {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.3.3",
"version": "1.3.4",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -42,9 +42,9 @@
<div class="flex items-start py-2">
<div class="py-2">
<div class="flex items-center">
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" @input="updateScannerParseSubtitle" />
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="updateScannerParseSubtitle" />
<ui-tooltip :text="parseSubtitleTooltip">
<p class="pl-4 text-lg">Parse Subtitles <span class="material-icons icon-text">info_outlined</span></p>
<p class="pl-4 text-lg">Parse subtitles <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
</div>
@ -53,12 +53,30 @@
<ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn>
<div class="w-full mb-4">
<ui-tooltip direction="bottom" text="Only scans audiobooks without a cover. Covers will be applied if a close match is found." class="w-full">
<ui-tooltip direction="bottom" text="(Warning: Long running task!) Attempts to lookup and match a cover with all audiobooks that don't have one." class="w-full">
<ui-btn color="primary" class="w-full" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn>
</ui-tooltip>
</div>
</div>
</div>
</div>
<!-- <ui-btn color="primary" small @click="saveMetadataFiles">Save Metadata</ui-btn> -->
<div class="py-4 mb-4">
<p class="text-2xl">Metadata</p>
<div class="flex items-start py-2">
<div class="py-2">
<div class="flex items-center">
<ui-toggle-switch v-model="storeCoversInAudiobookDir" :disabled="updatingServerSettings" @input="updateCoverStorageDestination" />
<ui-tooltip :text="coverDestinationTooltip">
<p class="pl-4 text-lg">Store covers with audiobook <span class="material-icons icon-text">info_outlined</span></p>
</ui-tooltip>
</div>
</div>
<div class="flex-grow" />
<div class="w-40 flex flex-col">
<ui-tooltip :text="saveMetadataTooltip" direction="bottom" class="w-full">
<ui-btn color="primary" small class="w-full" @click="saveMetadataFiles">Save Metadata</ui-btn>
</ui-tooltip>
</div>
</div>
</div>
@ -101,18 +119,21 @@ export default {
},
data() {
return {
storeCoversInAudiobookDir: false,
isResettingAudiobooks: false,
users: [],
selectedAccount: null,
showAccountModal: false,
isDeletingUser: false,
newServerSettings: {}
newServerSettings: {},
updatingServerSettings: false
}
},
watch: {
serverSettings(newVal, oldVal) {
if (newVal && !oldVal) {
this.newServerSettings = { ...this.serverSettings }
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
}
}
},
@ -120,6 +141,12 @@ export default {
parseSubtitleTooltip() {
return 'Extract subtitles from audiobook directory names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"'
},
coverDestinationTooltip() {
return 'By default covers are stored in /metadata/books, enabling this setting will store covers inside your audiobooks directory. Only one file named "cover" will be kept.'
},
saveMetadataTooltip() {
return 'This will write a "metadata.nfo" file in all of your audiobook directories.'
},
serverSettings() {
return this.$store.state.serverSettings
},
@ -134,6 +161,12 @@ export default {
}
},
methods: {
updateCoverStorageDestination(val) {
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
this.updateServerSettings({
coverDestination: this.newServerSettings.coverDestination
})
},
updateScannerParseSubtitle(val) {
var payload = {
scannerParseSubtitle: val
@ -141,13 +174,16 @@ export default {
this.updateServerSettings(payload)
},
updateServerSettings(payload) {
this.updatingServerSettings = true
this.$store
.dispatch('updateServerSettings', payload)
.then((success) => {
console.log('Updated Server Settings', success)
this.updatingServerSettings = false
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.updatingServerSettings = false
})
},
setDeveloperMode() {
@ -161,7 +197,14 @@ export default {
scanCovers() {
this.$root.socket.emit('scan_covers')
},
saveMetadataComplete(result) {
this.savingMetadata = false
if (!result) return
this.$toast.success(`Metadata saved for ${result.success} audiobooks`)
},
saveMetadataFiles() {
this.savingMetadata = true
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
this.$root.socket.emit('save_metadata')
},
loadUsers() {
@ -247,6 +290,7 @@ export default {
this.$root.socket.on('user_removed', this.userRemoved)
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
}
},
mounted() {

View File

@ -5,8 +5,14 @@ const DownloadStatus = {
FAILED: 3
}
const CoverDestination = {
METADATA: 0,
AUDIOBOOK: 1
}
const Constants = {
DownloadStatus
DownloadStatus,
CoverDestination
}
export default ({ app }, inject) => {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.3.3",
"version": "1.3.4",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {

View File

@ -8,7 +8,6 @@ const imageType = require('image-type')
const globals = require('./utils/globals')
const { CoverDestination } = require('./utils/constants')
class CoverController {
constructor(db, MetadataPath, AudiobookPath) {
this.db = db
@ -52,8 +51,8 @@ class CoverController {
}
}
// Remove covers in metadata/books/{ID} that dont have the same filename as the new cover
async checkBookMetadataCovers(dirpath, newCoverExt) {
// Remove covers that dont have the same filename as the new cover
async removeOldCovers(dirpath, newCoverExt) {
var filesInDir = await this.getFilesInDirectory(dirpath)
for (let i = 0; i < filesInDir.length; i++) {
@ -97,17 +96,11 @@ class CoverController {
var { fullPath, relPath } = this.getCoverDirectory(audiobook)
await fs.ensureDir(fullPath)
var isStoringInMetadata = relPath.slice(1).startsWith('metadata')
var coverFilename = `cover${extname}`
var coverFullPath = Path.join(fullPath, coverFilename)
var coverPath = Path.join(relPath, coverFilename)
if (isStoringInMetadata) {
await this.checkBookMetadataCovers(fullPath, extname)
}
// Move cover from temp upload dir to destination
var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
Logger.error('[CoverController] Failed to move cover file', path, error)
@ -115,12 +108,13 @@ class CoverController {
})
if (!success) {
// return res.status(500).send('Failed to move cover into destination')
return {
error: 'Failed to move cover into destination'
}
}
await this.removeOldCovers(fullPath, extname)
Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`)
audiobook.updateBookCover(coverPath)
@ -171,10 +165,7 @@ class CoverController {
var coverFullPath = Path.join(fullPath, coverFilename)
await fs.rename(temppath, coverFullPath)
var isStoringInMetadata = relPath.slice(1).startsWith('metadata')
if (isStoringInMetadata) {
await this.checkBookMetadataCovers(fullPath, '.' + imgtype.ext)
}
await this.removeOldCovers(fullPath, '.' + imgtype.ext)
Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`)

View File

@ -10,12 +10,13 @@ const { secondsToTimestamp } = require('./utils/fileUtils')
const { ScanResult, CoverDestination } = require('./utils/constants')
class Scanner {
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) {
this.AudiobookPath = AUDIOBOOK_PATH
this.MetadataPath = METADATA_PATH
this.BookMetadataPath = Path.join(this.MetadataPath, 'books')
this.db = db
this.coverController = coverController
this.emitter = emitter
this.cancelScan = false
@ -453,6 +454,8 @@ class Scanner {
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
var found = 0
var notFound = 0
var failed = 0
for (let i = 0; i < audiobooksNeedingCover.length; i++) {
var audiobook = audiobooksNeedingCover[i]
var options = {
@ -462,10 +465,15 @@ class Scanner {
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
if (results.length) {
Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`)
audiobook.book.cover = results[0]
await this.db.updateAudiobook(audiobook)
found++
this.emitter('audiobook_updated', audiobook.toJSONMinified())
var coverUrl = results[0]
var result = await this.coverController.downloadCoverFromUrl(audiobook, coverUrl)
if (result.error) {
failed++
} else {
found++
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONMinified())
}
} else {
notFound++
}

View File

@ -36,10 +36,10 @@ class Server {
this.db = new Db(this.ConfigPath)
this.auth = new Auth(this.db)
this.watcher = new Watcher(this.AudiobookPath)
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this))
this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath)
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
this.streamManager = new StreamManager(this.db, this.MetadataPath)
this.rssFeeds = new RssFeeds(this.Port, this.db)
this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath)
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.emitter.bind(this), this.clientEmitter.bind(this))
this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)

View File

@ -437,7 +437,10 @@ class Audiobook {
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
// Some files are not there anymore and filtered out
if (currOtherFileNum !== this.otherFiles.length) hasUpdates = true
if (currOtherFileNum !== this.otherFiles.length) {
Logger.debug(`[Audiobook] ${currOtherFileNum - this.otherFiles.length} other files were removed for "${this.title}"`)
hasUpdates = true
}
// If desc.txt is new or forcing rescan then read it and update description if empty
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')