mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-30 18:12:25 -04: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) | ||||
|         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++ | ||||
|       } | ||||
|  | ||||
| @ -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