mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Scan for covers now saves covers, server settings to save covers in audiobook folder
This commit is contained in:
		
							parent
							
								
									8d9d5a8d1b
								
							
						
					
					
						commit
						ef2b9a0415
					
				| @ -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() { | ||||||
|  | |||||||
| @ -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": { | ||||||
|  | |||||||
| @ -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() { | ||||||
|  | |||||||
| @ -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) => { | ||||||
|  | |||||||
| @ -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": { | ||||||
|  | |||||||
| @ -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}"`) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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++ | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -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') | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user