mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Improve track order detection, allow for excluding audio files from tracklist
This commit is contained in:
		
							parent
							
								
									45ee42bddd
								
							
						
					
					
						commit
						af0365c81f
					
				| @ -9,7 +9,15 @@ | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: Boolean | ||||
|     value: Boolean, | ||||
|     onColor: { | ||||
|       type: String, | ||||
|       default: 'success' | ||||
|     }, | ||||
|     offColor: { | ||||
|       type: String, | ||||
|       default: 'primary' | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     toggleValue: { | ||||
| @ -21,12 +29,11 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     toggleColor() { | ||||
|       return this.toggleValue ? 'bg-success' : 'bg-primary' | ||||
|       return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}` | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickToggle() { | ||||
|       console.log('click toggle', this.toggleValue) | ||||
|       this.toggleValue = !this.toggleValue | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "1.0.0", | ||||
|   "version": "1.0.1", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -19,16 +19,15 @@ | ||||
|         <div class="font-mono w-20 text-center">Duration</div> | ||||
|         <div class="font-mono text-center w-20">Status</div> | ||||
|         <div class="font-mono w-56">Notes</div> | ||||
|         <div class="font-book w-40">Include in Tracklist</div> | ||||
|       </div> | ||||
|       <draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false"> | ||||
|         <transition-group type="transition" :name="!drag ? 'flip-list' : null"> | ||||
|           <li v-for="(audio, index) in files" :key="audio.path" class="w-full list-group-item item flex items-center"> | ||||
|           <li v-for="(audio, index) in files" :key="audio.path" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center"> | ||||
|             <div class="font-book text-center px-4 py-1 w-12"> | ||||
|               {{ index + 1 }} | ||||
|             </div> | ||||
|             <div class="font-book text-center px-4 w-12"> | ||||
|               {{ audio.index }} | ||||
|               {{ audio.include ? index - numExcluded + 1 : -1 }} | ||||
|             </div> | ||||
|             <div class="font-book text-center px-4 w-12">{{ audio.index }}</div> | ||||
|             <div class="font-book text-center px-2 w-32"> | ||||
|               {{ audio.trackNumFromFilename }} | ||||
|             </div> | ||||
| @ -51,6 +50,9 @@ | ||||
|             <div class="font-sans text-xs font-normal w-56"> | ||||
|               {{ audio.error }} | ||||
|             </div> | ||||
|             <div class="font-sans text-xs font-normal w-40 flex justify-center"> | ||||
|               <ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" /> | ||||
|             </div> | ||||
|           </li> | ||||
|         </transition-group> | ||||
|       </draggable> | ||||
| @ -77,10 +79,10 @@ export default { | ||||
|       console.error('No audiobook...', params.id) | ||||
|       return redirect('/') | ||||
|     } | ||||
|     let index = 0 | ||||
|     return { | ||||
|       audiobook, | ||||
|       files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, index: ++index })) : [] | ||||
|       files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : [] | ||||
|       // files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, index: ++index })) : [] | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
| @ -98,6 +100,13 @@ export default { | ||||
|     audioFiles() { | ||||
|       return this.audiobook.audioFiles || [] | ||||
|     }, | ||||
|     numExcluded() { | ||||
|       var count = 0 | ||||
|       this.files.forEach((file) => { | ||||
|         if (!file.include) count++ | ||||
|       }) | ||||
|       return count | ||||
|     }, | ||||
|     missingPartChunks() { | ||||
|       if (this.missingParts === 1) return this.missingParts[0] | ||||
|       var chunks = [] | ||||
| @ -164,15 +173,36 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     includeToggled(audio) { | ||||
|       var new_index = 0 | ||||
|       if (audio.include) { | ||||
|         new_index = this.numExcluded | ||||
|       } | ||||
|       var old_index = this.files.findIndex((f) => f.ino === audio.ino) | ||||
|       if (new_index >= this.files.length) { | ||||
|         var k = new_index - this.files.length + 1 | ||||
|         while (k--) { | ||||
|           this.files.push(undefined) | ||||
|         } | ||||
|       } | ||||
|       this.files.splice(new_index, 0, this.files.splice(old_index, 1)[0]) | ||||
|     }, | ||||
|     saveTracklist() { | ||||
|       console.log('Tracklist', this.files) | ||||
|       var orderedFileData = this.files.map((file) => { | ||||
|         return { | ||||
|           index: file.index, | ||||
|           filename: file.filename, | ||||
|           ino: file.ino, | ||||
|           exclude: !file.include | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       this.saving = true | ||||
|       this.$axios | ||||
|         .$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { files: this.files }) | ||||
|         .$patch(`/api/audiobook/${this.audiobook.id}/tracks`, { orderedFileData }) | ||||
|         .then((data) => { | ||||
|           console.log('Finished patching files', data) | ||||
|           this.saving = false | ||||
|           // this.$router.go() | ||||
|           this.$toast.success('Tracks Updated') | ||||
|           this.$router.push(`/audiobook/${this.audiobookId}`) | ||||
|         }) | ||||
| @ -207,16 +237,26 @@ export default { | ||||
| .list-group { | ||||
|   min-height: 30px; | ||||
| } | ||||
| .list-group-item { | ||||
| .list-group-item:not(.exclude) { | ||||
|   cursor: n-resize; | ||||
| } | ||||
| .list-group-item:not(.ghost):hover { | ||||
| .list-group-item.exclude { | ||||
|   cursor: not-allowed; | ||||
| } | ||||
| .list-group-item:not(.ghost):not(.exclude):hover { | ||||
|   background-color: rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| .list-group-item:nth-child(even):not(.ghost) { | ||||
| .list-group-item:nth-child(even):not(.ghost):not(.exclude) { | ||||
|   background-color: rgba(0, 0, 0, 0.25); | ||||
| } | ||||
| .list-group-item:nth-child(even):not(.ghost):hover { | ||||
| .list-group-item:nth-child(even):not(.ghost):not(.exclude):hover { | ||||
|   background-color: rgba(0, 0, 0, 0.1); | ||||
| } | ||||
| 
 | ||||
| .list-group-item.exclude:not(.ghost) { | ||||
|   background-color: rgba(255, 0, 0, 0.25); | ||||
| } | ||||
| .list-group-item.exclude:not(.ghost):hover { | ||||
|   background-color: rgba(223, 0, 0, 0.25); | ||||
| } | ||||
| </style> | ||||
| @ -209,7 +209,7 @@ export default { | ||||
|     streamAudiobook() { | ||||
|       return this.$store.state.streamAudiobook | ||||
|     }, | ||||
|     isStreaming() { | ||||
|     streaming() { | ||||
|       return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "1.0.0", | ||||
|   "version": "1.0.1", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -182,9 +182,9 @@ class ApiController { | ||||
|   async updateAudiobookTracks(req, res) { | ||||
|     var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) | ||||
|     if (!audiobook) return res.sendStatus(404) | ||||
|     var files = req.body.files | ||||
|     var orderedFileData = req.body.orderedFileData | ||||
|     Logger.info(`Updating audiobook tracks called ${audiobook.id}`) | ||||
|     audiobook.updateAudioTracks(files) | ||||
|     audiobook.updateAudioTracks(orderedFileData) | ||||
|     await this.db.updateAudiobook(audiobook) | ||||
|     this.emitter('audiobook_updated', audiobook.toJSONMinified()) | ||||
|     res.json(audiobook.toJSON()) | ||||
|  | ||||
| @ -25,7 +25,8 @@ class FolderWatcher extends EventEmitter { | ||||
|         .on('add', (path) => { | ||||
|           this.onNewFile(path) | ||||
|         }).on('change', (path) => { | ||||
|           this.onFileUpdated(path) | ||||
|           // This is triggered from metadata changes, not what we want
 | ||||
|           // this.onFileUpdated(path)
 | ||||
|         }).on('unlink', path => { | ||||
|           this.onFileRemoved(path) | ||||
|         }).on('rename', (path, pathNext) => { | ||||
|  | ||||
| @ -29,6 +29,7 @@ class AudioFile { | ||||
| 
 | ||||
|     this.manuallyVerified = false | ||||
|     this.invalid = false | ||||
|     this.exclude = false | ||||
|     this.error = null | ||||
| 
 | ||||
|     if (data) { | ||||
| @ -49,6 +50,7 @@ class AudioFile { | ||||
|       trackNumFromFilename: this.trackNumFromFilename, | ||||
|       manuallyVerified: !!this.manuallyVerified, | ||||
|       invalid: !!this.invalid, | ||||
|       exclude: !!this.exclude, | ||||
|       error: this.error || null, | ||||
|       format: this.format, | ||||
|       duration: this.duration, | ||||
| @ -76,6 +78,7 @@ class AudioFile { | ||||
|     this.addedAt = data.addedAt | ||||
|     this.manuallyVerified = !!data.manuallyVerified | ||||
|     this.invalid = !!data.invalid | ||||
|     this.exclude = !!data.exclude | ||||
|     this.error = data.error || null | ||||
| 
 | ||||
|     this.trackNumFromMeta = data.trackNumFromMeta || null | ||||
| @ -112,6 +115,7 @@ class AudioFile { | ||||
| 
 | ||||
|     this.manuallyVerified = !!data.manuallyVerified | ||||
|     this.invalid = !!data.invalid | ||||
|     this.exclude = !!data.exclude | ||||
|     this.error = data.error || null | ||||
| 
 | ||||
|     this.format = data.format | ||||
| @ -131,6 +135,10 @@ class AudioFile { | ||||
|     this.tagTrack = data.file_tag_track || null | ||||
|   } | ||||
| 
 | ||||
|   clone() { | ||||
|     return new AudioFile(this.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   syncFile(newFile) { | ||||
|     var hasUpdates = false | ||||
|     var keysToSync = ['path', 'fullPath', 'ext', 'filename'] | ||||
|  | ||||
| @ -57,14 +57,14 @@ class Audiobook { | ||||
|     return this.book ? this.book.title : 'No Title' | ||||
|   } | ||||
| 
 | ||||
|   get cover() { | ||||
|     return this.book ? this.book.cover : '' | ||||
|   } | ||||
| 
 | ||||
|   get author() { | ||||
|     return this.book ? this.book.author : 'Unknown' | ||||
|   } | ||||
| 
 | ||||
|   get cover() { | ||||
|     return this.book ? this.book.cover : '' | ||||
|   } | ||||
| 
 | ||||
|   get authorLF() { | ||||
|     return this.book ? this.book.authorLF : null | ||||
|   } | ||||
| @ -273,19 +273,32 @@ class Audiobook { | ||||
|     return hasUpdates | ||||
|   } | ||||
| 
 | ||||
|   updateAudioTracks(files) { | ||||
|   updateAudioTracks(orderedFileData) { | ||||
|     var index = 1 | ||||
|     this.audioFiles = files.map((file) => { | ||||
|       file.manuallyVerified = true | ||||
|       file.invalid = false | ||||
|       file.error = null | ||||
|       file.index = index++ | ||||
|       return new AudioFile(file) | ||||
|     this.audioFiles = orderedFileData.map((fileData) => { | ||||
|       var audioFile = this.audioFiles.find(af => af.ino === fileData.ino) | ||||
|       audioFile.manuallyVerified = true | ||||
|       audioFile.invalid = false | ||||
|       audioFile.error = null | ||||
|       if (fileData.exclude !== undefined) { | ||||
|         audioFile.exclude = !!fileData.exclude | ||||
|       } | ||||
|       if (audioFile.exclude) { | ||||
|         audioFile.index = -1 | ||||
|       } else { | ||||
|         audioFile.index = index++ | ||||
|       } | ||||
|       return audioFile | ||||
|     }) | ||||
| 
 | ||||
|     this.audioFiles.sort((a, b) => a.index - b.index) | ||||
| 
 | ||||
|     this.tracks = [] | ||||
|     this.missingParts = [] | ||||
|     this.audioFiles.forEach((file) => { | ||||
|       this.addTrack(file) | ||||
|       if (!file.exclude) { | ||||
|         this.addTrack(file) | ||||
|       } | ||||
|     }) | ||||
|     this.lastUpdate = Date.now() | ||||
|   } | ||||
|  | ||||
| @ -66,8 +66,15 @@ function getTrackNumberFromMeta(scanData) { | ||||
|   return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Number(scanData.trackNumber) : null | ||||
| } | ||||
| 
 | ||||
| function getTrackNumberFromFilename(filename) { | ||||
| function getTrackNumberFromFilename(title, author, series, publishYear, filename) { | ||||
|   var partbasename = Path.basename(filename, Path.extname(filename)) | ||||
| 
 | ||||
|   // Remove title, author, series, and publishYear from filename if there
 | ||||
|   if (title) partbasename = partbasename.replace(title, '') | ||||
|   if (author) partbasename = partbasename.replace(author, '') | ||||
|   if (series) partbasename = partbasename.replace(series, '') | ||||
|   if (publishYear) partbasename = partbasename.replace(publishYear) | ||||
| 
 | ||||
|   var numbersinpath = partbasename.match(/\d+/g) | ||||
|   if (!numbersinpath) return null | ||||
| 
 | ||||
| @ -92,7 +99,8 @@ async function scanAudioFiles(audiobook, newAudioFiles) { | ||||
|     } | ||||
| 
 | ||||
|     var trackNumFromMeta = getTrackNumberFromMeta(scanData) | ||||
|     var trackNumFromFilename = getTrackNumberFromFilename(audioFile.filename) | ||||
|     var book = audiobook.book || {} | ||||
|     var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename) | ||||
| 
 | ||||
|     var audioFileObj = { | ||||
|       ino: audioFile.ino, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user