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