mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	Scan for covers now saves covers, server settings to save covers in audiobook folder
This commit is contained in:
		
							parent
							
								
									8d9d5a8d1b
								
							
						
					
					
						commit
						3dd8dc6dd4
					
				@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf-client",
 | 
			
		||||
  "version": "1.3.3",
 | 
			
		||||
  "version": "1.3.4",
 | 
			
		||||
  "description": "Audiobook manager and player",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,14 @@ const DownloadStatus = {
 | 
			
		||||
  FAILED: 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CoverDestination = {
 | 
			
		||||
  METADATA: 0,
 | 
			
		||||
  AUDIOBOOK: 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Constants = {
 | 
			
		||||
  DownloadStatus
 | 
			
		||||
  DownloadStatus,
 | 
			
		||||
  CoverDestination
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ({ app }, inject) => {
 | 
			
		||||
 | 
			
		||||
@ -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": {
 | 
			
		||||
 | 
			
		||||
@ -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}"`)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
        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++
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user