diff --git a/client/components/ui/ToggleSwitch.vue b/client/components/ui/ToggleSwitch.vue
index 73892a51..9292d59d 100644
--- a/client/components/ui/ToggleSwitch.vue
+++ b/client/components/ui/ToggleSwitch.vue
@@ -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() {
diff --git a/client/package.json b/client/package.json
index 91940d35..a9d7b142 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "1.3.3",
+ "version": "1.3.4",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index a0bb7efa..c25ef8b6 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -42,9 +42,9 @@
-
+
- Parse Subtitles info_outlined
+ Parse subtitles info_outlined
@@ -53,12 +53,30 @@
Scan
-
+
Scan for Covers
+
+
+
-
+
+
Metadata
+
+
+
+
+
+ Store covers with audiobook info_outlined
+
+
+
+
+
+
+ Save Metadata
+
@@ -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.
Subtitle must be seperated by " - "
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() {
diff --git a/client/plugins/constants.js b/client/plugins/constants.js
index 35dc5e08..dbd71632 100644
--- a/client/plugins/constants.js
+++ b/client/plugins/constants.js
@@ -5,8 +5,14 @@ const DownloadStatus = {
FAILED: 3
}
+const CoverDestination = {
+ METADATA: 0,
+ AUDIOBOOK: 1
+}
+
const Constants = {
- DownloadStatus
+ DownloadStatus,
+ CoverDestination
}
export default ({ app }, inject) => {
diff --git a/package.json b/package.json
index 592e2f4a..fd4e75d5 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/server/CoverController.js b/server/CoverController.js
index fba0a791..0aa5aedd 100644
--- a/server/CoverController.js
+++ b/server/CoverController.js
@@ -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}"`)
diff --git a/server/Scanner.js b/server/Scanner.js
index 44a7b78a..e3881c9c 100644
--- a/server/Scanner.js
+++ b/server/Scanner.js
@@ -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++
}
diff --git a/server/Server.js b/server/Server.js
index b47e5ab1..c3ee1fe4 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -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)
diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js
index 2fec1e86..a4b777df 100644
--- a/server/objects/Audiobook.js
+++ b/server/objects/Audiobook.js
@@ -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')