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

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

View File

@ -30,7 +30,7 @@ export default {
} }
}, },
className() { 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}` return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}`
}, },
switchClassName() { switchClassName() {

View File

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

View File

@ -42,9 +42,9 @@
<div class="flex items-start py-2"> <div class="flex items-start py-2">
<div class="py-2"> <div class="py-2">
<div class="flex items-center"> <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"> <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> </ui-tooltip>
</div> </div>
</div> </div>
@ -53,12 +53,30 @@
<ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn> <ui-btn color="success" class="mb-4" :loading="isScanning" :disabled="isScanningCovers" @click="scan">Scan</ui-btn>
<div class="w-full mb-4"> <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-btn color="primary" class="w-full" small :padding-x="2" :loading="isScanningCovers" :disabled="isScanning" @click="scanCovers">Scan for Covers</ui-btn>
</ui-tooltip> </ui-tooltip>
</div> </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> </div>
</div> </div>
@ -101,18 +119,21 @@ export default {
}, },
data() { data() {
return { return {
storeCoversInAudiobookDir: false,
isResettingAudiobooks: false, isResettingAudiobooks: false,
users: [], users: [],
selectedAccount: null, selectedAccount: null,
showAccountModal: false, showAccountModal: false,
isDeletingUser: false, isDeletingUser: false,
newServerSettings: {} newServerSettings: {},
updatingServerSettings: false
} }
}, },
watch: { watch: {
serverSettings(newVal, oldVal) { serverSettings(newVal, oldVal) {
if (newVal && !oldVal) { if (newVal && !oldVal) {
this.newServerSettings = { ...this.serverSettings } this.newServerSettings = { ...this.serverSettings }
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
} }
} }
}, },
@ -120,6 +141,12 @@ export default {
parseSubtitleTooltip() { 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"' 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() { serverSettings() {
return this.$store.state.serverSettings return this.$store.state.serverSettings
}, },
@ -134,6 +161,12 @@ export default {
} }
}, },
methods: { methods: {
updateCoverStorageDestination(val) {
this.newServerSettings.coverDestination = val ? this.$constants.CoverDestination.AUDIOBOOK : this.$constants.CoverDestination.METADATA
this.updateServerSettings({
coverDestination: this.newServerSettings.coverDestination
})
},
updateScannerParseSubtitle(val) { updateScannerParseSubtitle(val) {
var payload = { var payload = {
scannerParseSubtitle: val scannerParseSubtitle: val
@ -141,13 +174,16 @@ export default {
this.updateServerSettings(payload) this.updateServerSettings(payload)
}, },
updateServerSettings(payload) { updateServerSettings(payload) {
this.updatingServerSettings = true
this.$store this.$store
.dispatch('updateServerSettings', payload) .dispatch('updateServerSettings', payload)
.then((success) => { .then((success) => {
console.log('Updated Server Settings', success) console.log('Updated Server Settings', success)
this.updatingServerSettings = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
this.updatingServerSettings = false
}) })
}, },
setDeveloperMode() { setDeveloperMode() {
@ -161,7 +197,14 @@ export default {
scanCovers() { scanCovers() {
this.$root.socket.emit('scan_covers') this.$root.socket.emit('scan_covers')
}, },
saveMetadataComplete(result) {
this.savingMetadata = false
if (!result) return
this.$toast.success(`Metadata saved for ${result.success} audiobooks`)
},
saveMetadataFiles() { saveMetadataFiles() {
this.savingMetadata = true
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
this.$root.socket.emit('save_metadata') this.$root.socket.emit('save_metadata')
}, },
loadUsers() { loadUsers() {
@ -247,6 +290,7 @@ export default {
this.$root.socket.on('user_removed', this.userRemoved) this.$root.socket.on('user_removed', this.userRemoved)
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.storeCoversInAudiobookDir = this.newServerSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK
} }
}, },
mounted() { mounted() {

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.3.3", "version": "1.3.4",
"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": {

View File

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

View File

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

View File

@ -36,10 +36,10 @@ class Server {
this.db = new Db(this.ConfigPath) this.db = new Db(this.ConfigPath)
this.auth = new Auth(this.db) this.auth = new Auth(this.db)
this.watcher = new Watcher(this.AudiobookPath) 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.streamManager = new StreamManager(this.db, this.MetadataPath)
this.rssFeeds = new RssFeeds(this.Port, this.db) 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.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.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) 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)) this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
// Some files are not there anymore and filtered out // 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 // 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') var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')