mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Missing audiobooks flagged not deleted, fix close progress loop on stream errors, clickable download toast, consolidate duplicate track error log, improved scanner to ignore non-audio files
This commit is contained in:
		
							parent
							
								
									0851a1e71e
								
							
						
					
					
						commit
						db01db3a2b
					
				| @ -14,7 +14,7 @@ | ||||
|           <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" /> | ||||
| 
 | ||||
|           <div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist"> | ||||
|             <div v-show="!isSelectionMode" class="h-full flex items-center justify-center"> | ||||
|             <div v-show="!isSelectionMode && !isMissing" class="h-full flex items-center justify-center"> | ||||
|               <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play"> | ||||
|                 <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span> | ||||
|               </div> | ||||
| @ -132,7 +132,10 @@ export default { | ||||
|       return this.userProgress ? !!this.userProgress.isRead : false | ||||
|     }, | ||||
|     showError() { | ||||
|       return this.hasMissingParts || this.hasInvalidParts | ||||
|       return this.hasMissingParts || this.hasInvalidParts || this.isMissing | ||||
|     }, | ||||
|     isMissing() { | ||||
|       return this.audiobook.isMissing | ||||
|     }, | ||||
|     hasMissingParts() { | ||||
|       return this.audiobook.hasMissingParts | ||||
| @ -141,6 +144,7 @@ export default { | ||||
|       return this.audiobook.hasInvalidParts | ||||
|     }, | ||||
|     errorText() { | ||||
|       if (this.isMissing) return 'Audiobook directory is missing!' | ||||
|       var txt = '' | ||||
|       if (this.hasMissingParts) { | ||||
|         txt = `${this.hasMissingParts} missing parts.` | ||||
|  | ||||
| @ -109,6 +109,7 @@ export default { | ||||
|     availableTabs() { | ||||
|       if (!this.userCanUpdate && !this.userCanDownload) return [] | ||||
|       return this.tabs.filter((tab) => { | ||||
|         if (tab.id === 'download' && this.isMissing) return false | ||||
|         if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true | ||||
|         if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true | ||||
|         return false | ||||
| @ -122,6 +123,9 @@ export default { | ||||
|       var _tab = this.tabs.find((t) => t.id === this.selectedTab) | ||||
|       return _tab ? _tab.component : '' | ||||
|     }, | ||||
|     isMissing() { | ||||
|       return this.selectedAudiobook.isMissing | ||||
|     }, | ||||
|     selectedAudiobook() { | ||||
|       return this.$store.state.selectedAudiobook || {} | ||||
|     }, | ||||
|  | ||||
| @ -11,7 +11,7 @@ | ||||
|         <th class="text-left">Filename</th> | ||||
|         <th class="text-left">Size</th> | ||||
|         <th class="text-left">Duration</th> | ||||
|         <th v-if="userCanDownload" class="text-center">Download</th> | ||||
|         <th v-if="showDownload" class="text-center">Download</th> | ||||
|       </tr> | ||||
|       <template v-for="track in tracks"> | ||||
|         <tr :key="track.index"> | ||||
| @ -27,7 +27,7 @@ | ||||
|           <td class="font-mono"> | ||||
|             {{ $secondsToTimestamp(track.duration) }} | ||||
|           </td> | ||||
|           <td v-if="userCanDownload" class="font-mono text-center"> | ||||
|           <td v-if="showDownload" class="font-mono text-center"> | ||||
|             <a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a> | ||||
|           </td> | ||||
|         </tr> | ||||
| @ -64,6 +64,12 @@ export default { | ||||
|     }, | ||||
|     userCanDownload() { | ||||
|       return this.$store.getters['user/getUserCanDownload'] | ||||
|     }, | ||||
|     isMissing() { | ||||
|       return this.audiobook.isMissing | ||||
|     }, | ||||
|     showDownload() { | ||||
|       return this.userCanDownload && !this.isMissing | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :class="className" @click="clickBtn"> | ||||
|   <button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :disabled="disabled" :class="className" @click="clickBtn"> | ||||
|     <span class="material-icons icon-text">{{ icon }}</span> | ||||
|   </button> | ||||
| </template> | ||||
| @ -39,6 +39,9 @@ export default { | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| button.icon-btn:disabled { | ||||
|   cursor: not-allowed; | ||||
| } | ||||
| button.icon-btn::before { | ||||
|   content: ''; | ||||
|   position: absolute; | ||||
|  | ||||
| @ -14,7 +14,8 @@ export default { | ||||
|     direction: { | ||||
|       type: String, | ||||
|       default: 'right' | ||||
|     } | ||||
|     }, | ||||
|     disabled: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @ -25,6 +26,11 @@ export default { | ||||
|   watch: { | ||||
|     text() { | ||||
|       this.updateText() | ||||
|     }, | ||||
|     disabled(newVal) { | ||||
|       if (newVal && this.isShowing) { | ||||
|         this.hideTooltip() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -81,6 +87,7 @@ export default { | ||||
|       tooltip.style.left = left + 'px' | ||||
|     }, | ||||
|     showTooltip() { | ||||
|       if (this.disabled) return | ||||
|       if (!this.tooltip) { | ||||
|         this.createTooltip() | ||||
|       } | ||||
|  | ||||
| @ -102,6 +102,7 @@ export default { | ||||
|           if (results.added) scanResultMsgs.push(`${results.added} added`) | ||||
|           if (results.updated) scanResultMsgs.push(`${results.updated} updated`) | ||||
|           if (results.removed) scanResultMsgs.push(`${results.removed} removed`) | ||||
|           if (results.missing) scanResultMsgs.push(`${results.missing} missing`) | ||||
|           if (!scanResultMsgs.length) this.$toast.success('Scan Finished\nEverything was up to date') | ||||
|           else this.$toast.success('Scan Finished\n' + scanResultMsgs.join('\n')) | ||||
|         } | ||||
| @ -128,19 +129,18 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     downloadToastClick(download) { | ||||
|       console.log('Downlaod ready toast click', download) | ||||
|       // if (!download || !download.audiobookId) { | ||||
|       //   return console.error('Invalid download object', download) | ||||
|       // } | ||||
|       // var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId) | ||||
|       // if (!audiobook) { | ||||
|       //   return console.error('Audiobook not found for download', download) | ||||
|       // } | ||||
|       // this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' }) | ||||
|       if (!download || !download.audiobookId) { | ||||
|         return console.error('Invalid download object', download) | ||||
|       } | ||||
|       var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId) | ||||
|       if (!audiobook) { | ||||
|         return console.error('Audiobook not found for download', download) | ||||
|       } | ||||
|       this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' }) | ||||
|     }, | ||||
|     downloadStarted(download) { | ||||
|       download.status = this.$constants.DownloadStatus.PENDING | ||||
|       download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: this.downloadToastClick }) | ||||
|       download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false, onClick: () => this.downloadToastClick(download) }) | ||||
|       this.$store.commit('downloads/addUpdateDownload', download) | ||||
|     }, | ||||
|     downloadReady(download) { | ||||
| @ -149,7 +149,7 @@ export default { | ||||
| 
 | ||||
|       if (existingDownload && existingDownload.toastId !== undefined) { | ||||
|         download.toastId = existingDownload.toastId | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: this.downloadToastClick } }, true) | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" is ready!`, options: { timeout: 5000, type: 'success', onClick: () => this.downloadToastClick(download) } }, true) | ||||
|       } else { | ||||
|         this.$toast.success(`Download "${download.filename}" is ready!`) | ||||
|       } | ||||
| @ -163,7 +163,7 @@ export default { | ||||
| 
 | ||||
|       if (existingDownload && existingDownload.toastId !== undefined) { | ||||
|         download.toastId = existingDownload.toastId | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true) | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error', onClick: () => this.downloadToastClick(download) } }, true) | ||||
|       } else { | ||||
|         console.warn('Download failed no existing download', existingDownload) | ||||
|         this.$toast.error(`Download "${download.filename}" ${failedMsg}`) | ||||
| @ -174,7 +174,7 @@ export default { | ||||
|       var existingDownload = this.$store.getters['downloads/getDownload'](download.id) | ||||
|       if (existingDownload && existingDownload.toastId !== undefined) { | ||||
|         download.toastId = existingDownload.toastId | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: this.downloadToastClick } }, true) | ||||
|         this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" was terminated`, options: { timeout: 5000, type: 'error', onClick: () => this.downloadToastClick(download) } }, true) | ||||
|       } else { | ||||
|         console.warn('Download killed no existing download found', existingDownload) | ||||
|         this.$toast.error(`Download "${download.filename}" was terminated`) | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.1.11", | ||||
|   "version": "1.1.12", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -31,17 +31,21 @@ | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="flex items-center pt-4"> | ||||
|             <ui-btn :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream"> | ||||
|             <ui-btn v-if="!isMissing" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream"> | ||||
|               <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> | ||||
|               {{ streaming ? 'Streaming' : 'Play' }} | ||||
|             </ui-btn> | ||||
|             <ui-btn v-else color="error" :padding-x="4" small class="flex items-center h-9 mr-2"> | ||||
|               <span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span> | ||||
|               Missing | ||||
|             </ui-btn> | ||||
| 
 | ||||
|             <ui-tooltip v-if="userCanUpdate" text="Edit" direction="top"> | ||||
|               <ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" /> | ||||
|             </ui-tooltip> | ||||
| 
 | ||||
|             <ui-tooltip v-if="userCanDownload" text="Download" direction="top"> | ||||
|               <ui-icon-btn icon="download" class="mx-0.5" @click="downloadClick" /> | ||||
|             <ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top"> | ||||
|               <ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" /> | ||||
|             </ui-tooltip> | ||||
| 
 | ||||
|             <ui-tooltip :text="isRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top"> | ||||
| @ -152,6 +156,9 @@ export default { | ||||
|       }) | ||||
|       return chunks | ||||
|     }, | ||||
|     isMissing() { | ||||
|       return this.audiobook.isMissing | ||||
|     }, | ||||
|     missingParts() { | ||||
|       return this.audiobook.missingParts || [] | ||||
|     }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.1.11", | ||||
|   "version": "1.1.12", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -9,7 +9,6 @@ const { comparePaths, getIno } = require('./utils/index') | ||||
| const { secondsToTimestamp } = require('./utils/fileUtils') | ||||
| const { ScanResult } = require('./utils/constants') | ||||
| 
 | ||||
| 
 | ||||
| class Scanner { | ||||
|   constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) { | ||||
|     this.AudiobookPath = AUDIOBOOK_PATH | ||||
| @ -71,6 +70,7 @@ class Scanner { | ||||
|     if (existingAudiobook) { | ||||
| 
 | ||||
|       // REMOVE: No valid audio files
 | ||||
|       // TODO: Label as incomplete, do not actually delete
 | ||||
|       if (!audiobookData.audioFiles.length) { | ||||
|         Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`) | ||||
| 
 | ||||
| @ -109,8 +109,8 @@ class Scanner { | ||||
|         await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
|       // REMOVE: No valid audio tracks
 | ||||
|       // TODO: Label as incomplete, do not actually delete
 | ||||
|       if (!existingAudiobook.tracks.length) { | ||||
|         Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`) | ||||
| 
 | ||||
| @ -135,6 +135,12 @@ class Scanner { | ||||
|         hasUpdates = true | ||||
|       } | ||||
| 
 | ||||
|       if (existingAudiobook.isMissing) { | ||||
|         existingAudiobook.isMissing = false | ||||
|         hasUpdates = true | ||||
|         Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`) | ||||
|       } | ||||
| 
 | ||||
|       if (hasUpdates) { | ||||
|         existingAudiobook.setChapters() | ||||
| 
 | ||||
| @ -173,23 +179,24 @@ class Scanner { | ||||
|   } | ||||
| 
 | ||||
|   async scan() { | ||||
|     // TODO: This temporary fix from pre-release should be removed soon, including the "fixRelativePath" and "checkUpdateInos"
 | ||||
|     // TEMP - fix relative file paths
 | ||||
|     // TEMP - update ino for each audiobook
 | ||||
|     if (this.audiobooks.length) { | ||||
|       for (let i = 0; i < this.audiobooks.length; i++) { | ||||
|         var ab = this.audiobooks[i] | ||||
|         var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino | ||||
|     // if (this.audiobooks.length) {
 | ||||
|     //   for (let i = 0; i < this.audiobooks.length; i++) {
 | ||||
|     //     var ab = this.audiobooks[i]
 | ||||
|     //     var shouldUpdate = ab.fixRelativePath(this.AudiobookPath) || !ab.ino
 | ||||
| 
 | ||||
|         // Update ino if an audio file has the same ino as the audiobook
 | ||||
|         var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino) | ||||
|         if (shouldUpdateIno) { | ||||
|           await ab.checkUpdateInos() | ||||
|         } | ||||
|         if (shouldUpdate) { | ||||
|           await this.db.updateAudiobook(ab) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     //     // Update ino if an audio file has the same ino as the audiobook
 | ||||
|     //     var shouldUpdateIno = !ab.ino || (ab.audioFiles || []).find(abf => abf.ino === ab.ino)
 | ||||
|     //     if (shouldUpdateIno) {
 | ||||
|     //       await ab.checkUpdateInos()
 | ||||
|     //     }
 | ||||
|     //     if (shouldUpdate) {
 | ||||
|     //       await this.db.updateAudiobook(ab)
 | ||||
|     //     }
 | ||||
|     //   }
 | ||||
|     // }
 | ||||
| 
 | ||||
|     const scanStart = Date.now() | ||||
|     var audiobookDataFound = await scanRootDir(this.AudiobookPath, this.db.serverSettings) | ||||
| @ -205,18 +212,21 @@ class Scanner { | ||||
|     var scanResults = { | ||||
|       removed: 0, | ||||
|       updated: 0, | ||||
|       added: 0 | ||||
|       added: 0, | ||||
|       missing: 0 | ||||
|     } | ||||
| 
 | ||||
|     // Check for removed audiobooks
 | ||||
|     for (let i = 0; i < this.audiobooks.length; i++) { | ||||
|       var dataFound = audiobookDataFound.find(abd => abd.ino === this.audiobooks[i].ino) | ||||
|       var audiobook = this.audiobooks[i] | ||||
|       var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino) | ||||
|       if (!dataFound) { | ||||
|         Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`) | ||||
|         var audiobookJSON = this.audiobooks[i].toJSONMinified() | ||||
|         await this.db.removeEntity('audiobook', this.audiobooks[i].id) | ||||
|         scanResults.removed++ | ||||
|         this.emitter('audiobook_removed', audiobookJSON) | ||||
|         Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) | ||||
|         audiobook.isMissing = true | ||||
|         audiobook.lastUpdate = Date.now() | ||||
|         scanResults.missing++ | ||||
|         await this.db.updateAudiobook(audiobook) | ||||
|         this.emitter('audiobook_updated', audiobook.toJSONMinified()) | ||||
|       } | ||||
|       if (this.cancelScan) { | ||||
|         this.cancelScan = false | ||||
| @ -247,7 +257,7 @@ class Scanner { | ||||
|       } | ||||
|     } | ||||
|     const scanElapsed = Math.floor((Date.now() - scanStart) / 1000) | ||||
|     Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | elapsed: ${secondsToTimestamp(scanElapsed)}`) | ||||
|     Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`) | ||||
|     return scanResults | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -197,7 +197,6 @@ class Server { | ||||
|       res.json({ success: true }) | ||||
|     }) | ||||
| 
 | ||||
| 
 | ||||
|     // Used in development to set-up streams without authentication
 | ||||
|     if (process.env.NODE_ENV !== 'production') { | ||||
|       app.use('/test-hls', this.hlsController.router) | ||||
|  | ||||
| @ -28,6 +28,9 @@ class Audiobook { | ||||
|     this.book = null | ||||
|     this.chapters = [] | ||||
| 
 | ||||
|     // Audiobook was scanned and not found
 | ||||
|     this.isMissing = false | ||||
| 
 | ||||
|     if (audiobook) { | ||||
|       this.construct(audiobook) | ||||
|     } | ||||
| @ -55,6 +58,8 @@ class Audiobook { | ||||
|     if (audiobook.chapters) { | ||||
|       this.chapters = audiobook.chapters.map(c => ({ ...c })) | ||||
|     } | ||||
| 
 | ||||
|     this.isMissing = !!audiobook.isMissing | ||||
|   } | ||||
| 
 | ||||
|   get title() { | ||||
| @ -127,7 +132,8 @@ class Audiobook { | ||||
|       tracks: this.tracksToJSON(), | ||||
|       audioFiles: (this.audioFiles || []).map(audioFile => audioFile.toJSON()), | ||||
|       otherFiles: (this.otherFiles || []).map(otherFile => otherFile.toJSON()), | ||||
|       chapters: this.chapters || [] | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -147,7 +153,8 @@ class Audiobook { | ||||
|       hasMissingParts: this.missingParts ? this.missingParts.length : 0, | ||||
|       hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0, | ||||
|       numTracks: this.tracks.length, | ||||
|       chapters: this.chapters || [] | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -169,7 +176,8 @@ class Audiobook { | ||||
|       tags: this.tags, | ||||
|       book: this.bookToJSON(), | ||||
|       tracks: this.tracksToJSON(), | ||||
|       chapters: this.chapters || [] | ||||
|       chapters: this.chapters || [], | ||||
|       isMissing: !!this.isMissing | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -288,6 +288,7 @@ class Stream extends EventEmitter { | ||||
|       } else { | ||||
|         Logger.error('Ffmpeg Err', err.message) | ||||
|       } | ||||
|       clearInterval(this.loop) | ||||
|     }) | ||||
| 
 | ||||
|     this.ffmpeg.on('end', (stdout, stderr) => { | ||||
| @ -300,6 +301,7 @@ class Stream extends EventEmitter { | ||||
|       } | ||||
|       this.isTranscodeComplete = true | ||||
|       this.ffmpeg = null | ||||
|       clearInterval(this.loop) | ||||
|     }) | ||||
| 
 | ||||
|     this.ffmpeg.run() | ||||
|  | ||||
| @ -89,6 +89,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) { | ||||
|     return | ||||
|   } | ||||
|   var tracks = [] | ||||
|   var numDuplicateTracks = 0 | ||||
|   var numInvalidTracks = 0 | ||||
|   for (let i = 0; i < newAudioFiles.length; i++) { | ||||
|     var audioFile = newAudioFiles[i] | ||||
|     var scanData = await scan(audioFile.fullPath) | ||||
| @ -118,17 +120,19 @@ async function scanAudioFiles(audiobook, newAudioFiles) { | ||||
|     if (newAudioFiles.length > 1) { | ||||
|       trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename | ||||
|       if (trackNumber === null) { | ||||
|         Logger.error('[AudioFileScanner] Invalid track number for', audioFile.filename) | ||||
|         Logger.debug('[AudioFileScanner] Invalid track number for', audioFile.filename) | ||||
|         audioFile.invalid = true | ||||
|         audioFile.error = 'Failed to get track number' | ||||
|         numInvalidTracks++ | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (tracks.find(t => t.index === trackNumber)) { | ||||
|       Logger.error('[AudioFileScanner] Duplicate track number for', audioFile.filename) | ||||
|       Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename) | ||||
|       audioFile.invalid = true | ||||
|       audioFile.error = 'Duplicate track number' | ||||
|       numDuplicateTracks++ | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
| @ -141,6 +145,13 @@ async function scanAudioFiles(audiobook, newAudioFiles) { | ||||
|     return | ||||
|   } | ||||
| 
 | ||||
|   if (numDuplicateTracks > 0) { | ||||
|     Logger.warn(`[AudioFileScanner] ${numDuplicateTracks} Duplicate tracks for "${audiobook.title}"`) | ||||
|   } | ||||
|   if (numInvalidTracks > 0) { | ||||
|     Logger.error(`[AudioFileScanner] ${numDuplicateTracks} Invalid tracks for "${audiobook.title}"`) | ||||
|   } | ||||
| 
 | ||||
|   tracks.sort((a, b) => a.index - b.index) | ||||
|   audiobook.audioFiles.sort((a, b) => { | ||||
|     var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0 | ||||
|  | ||||
| @ -19,11 +19,17 @@ function getPaths(path) { | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| function isAudioFile(path) { | ||||
|   if (!path) return false | ||||
|   var ext = Path.extname(path) | ||||
|   if (!ext) return false | ||||
|   return AUDIO_FORMATS.includes(ext.slice(1).toLowerCase()) | ||||
| } | ||||
| 
 | ||||
| function groupFilesIntoAudiobookPaths(paths) { | ||||
|   // Step 1: Normalize path, Remove leading "/", Filter out files in root dir
 | ||||
|   var pathsFiltered = paths.map(path => Path.normalize(path.slice(1))).filter(path => Path.parse(path).dir) | ||||
| 
 | ||||
| 
 | ||||
|   // Step 2: Sort by least number of directories
 | ||||
|   pathsFiltered.sort((a, b) => { | ||||
|     var pathsA = Path.dirname(a).split(Path.sep).length | ||||
| @ -31,25 +37,55 @@ function groupFilesIntoAudiobookPaths(paths) { | ||||
|     return pathsA - pathsB | ||||
|   }) | ||||
| 
 | ||||
|   // Step 3: Group into audiobooks
 | ||||
|   // Step 2.5: Seperate audio files and other files
 | ||||
|   var audioFilePaths = [] | ||||
|   var otherFilePaths = [] | ||||
|   pathsFiltered.forEach(path => { | ||||
|     if (isAudioFile(path)) audioFilePaths.push(path) | ||||
|     else otherFilePaths.push(path) | ||||
|   }) | ||||
| 
 | ||||
|   // Step 3: Group audio files in audiobooks
 | ||||
|   var audiobookGroup = {} | ||||
|   pathsFiltered.forEach((path) => { | ||||
|   audioFilePaths.forEach((path) => { | ||||
|     var dirparts = Path.dirname(path).split(Path.sep) | ||||
|     var numparts = dirparts.length | ||||
|     var _path = '' | ||||
| 
 | ||||
|     // Iterate over directories in path
 | ||||
|     for (let i = 0; i < numparts; i++) { | ||||
|       var dirpart = dirparts.shift() | ||||
|       _path = Path.join(_path, dirpart) | ||||
|       if (audiobookGroup[_path]) { | ||||
| 
 | ||||
| 
 | ||||
|       if (audiobookGroup[_path]) { // Directory already has files, add file
 | ||||
|         var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path)) | ||||
|         audiobookGroup[_path].push(relpath) | ||||
|         return | ||||
|       } else if (!dirparts.length) { | ||||
|       } else if (!dirparts.length) { // This is the last directory, create group
 | ||||
|         audiobookGroup[_path] = [Path.basename(path)] | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|   // Step 4: Add other files into audiobook groups
 | ||||
|   otherFilePaths.forEach((path) => { | ||||
|     var dirparts = Path.dirname(path).split(Path.sep) | ||||
|     var numparts = dirparts.length | ||||
|     var _path = '' | ||||
| 
 | ||||
|     // Iterate over directories in path
 | ||||
|     for (let i = 0; i < numparts; i++) { | ||||
|       var dirpart = dirparts.shift() | ||||
|       _path = Path.join(_path, dirpart) | ||||
|       if (audiobookGroup[_path]) { // Directory is audiobook group
 | ||||
|         var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path)) | ||||
|         audiobookGroup[_path].push(relpath) | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|   return audiobookGroup | ||||
| } | ||||
| module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user