mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Tasks widget in appbar for merging m4bs & remove old m4b merge routes
This commit is contained in:
		
							parent
							
								
									441b8c5bb7
								
							
						
					
					
						commit
						39979ff8a3
					
				@ -7,7 +7,7 @@
 | 
				
			|||||||
        </nuxt-link>
 | 
					        </nuxt-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <nuxt-link to="/">
 | 
					        <nuxt-link to="/">
 | 
				
			||||||
          <h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
 | 
					          <h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
 | 
				
			||||||
        </nuxt-link>
 | 
					        </nuxt-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-libraries-dropdown class="mr-2" />
 | 
					        <ui-libraries-dropdown class="mr-2" />
 | 
				
			||||||
@ -15,7 +15,7 @@
 | 
				
			|||||||
        <controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
 | 
					        <controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
 | 
				
			||||||
        <div class="flex-grow" />
 | 
					        <div class="flex-grow" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <span v-if="showExperimentalFeatures" class="material-icons text-2xl md:text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
 | 
					        <widgets-notification-widget class="hidden md:block" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
 | 
					        <ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
 | 
				
			||||||
          <span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
 | 
					          <span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="flex-grow" />
 | 
					        <div class="flex-grow" />
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <ui-btn :to="`/audiobook/${libraryItemId}/manage`" class="flex items-center"
 | 
					          <ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
 | 
				
			||||||
            >Open Manager
 | 
					            >Open Manager
 | 
				
			||||||
            <span class="material-icons text-lg ml-2">launch</span>
 | 
					            <span class="material-icons text-lg ml-2">launch</span>
 | 
				
			||||||
          </ui-btn>
 | 
					          </ui-btn>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										25
									
								
								client/components/widgets/NotificationWidget.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								client/components/widgets/NotificationWidget.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div v-if="tasksRunning" class="w-4 h-4 mx-3 relative">
 | 
				
			||||||
 | 
					    <div class="flex h-full items-center justify-center">
 | 
				
			||||||
 | 
					      <widgets-loading-spinner />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    tasks() {
 | 
				
			||||||
 | 
					      return this.$store.state.tasks.tasks
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    tasksRunning() {
 | 
				
			||||||
 | 
					      return this.tasks.some((t) => !t.isFinished)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {},
 | 
				
			||||||
 | 
					  mounted() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -270,6 +270,14 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      this.$store.commit('scanners/addUpdate', data)
 | 
					      this.$store.commit('scanners/addUpdate', data)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    taskStarted(task) {
 | 
				
			||||||
 | 
					      console.log('Task started', task)
 | 
				
			||||||
 | 
					      this.$store.commit('tasks/addUpdateTask', task)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    taskFinished(task) {
 | 
				
			||||||
 | 
					      console.log('Task finished', task)
 | 
				
			||||||
 | 
					      this.$store.commit('tasks/addUpdateTask', task)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    userUpdated(user) {
 | 
					    userUpdated(user) {
 | 
				
			||||||
      if (this.$store.state.user.user.id === user.id) {
 | 
					      if (this.$store.state.user.user.id === user.id) {
 | 
				
			||||||
        this.$store.commit('user/setUser', user)
 | 
					        this.$store.commit('user/setUser', user)
 | 
				
			||||||
@ -302,53 +310,6 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      this.$store.commit('user/removeCollection', collection)
 | 
					      this.$store.commit('user/removeCollection', collection)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    abmergeStarted(download) {
 | 
					 | 
				
			||||||
      download.status = this.$constants.DownloadStatus.PENDING
 | 
					 | 
				
			||||||
      download.toastId = this.$toast(`Preparing download "${download.filename}"`, { timeout: false, draggable: false, closeOnClick: false })
 | 
					 | 
				
			||||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    abmergeReady(download) {
 | 
					 | 
				
			||||||
      download.status = this.$constants.DownloadStatus.READY
 | 
					 | 
				
			||||||
      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}" is ready!`, options: { timeout: 5000, type: 'success' } }, true)
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        this.$toast.success(`Download "${download.filename}" is ready!`)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    abmergeFailed(download) {
 | 
					 | 
				
			||||||
      download.status = this.$constants.DownloadStatus.FAILED
 | 
					 | 
				
			||||||
      var existingDownload = this.$store.getters['downloads/getDownload'](download.id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var failedMsg = download.isTimedOut ? 'timed out' : 'failed'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (existingDownload && existingDownload.toastId !== undefined) {
 | 
					 | 
				
			||||||
        download.toastId = existingDownload.toastId
 | 
					 | 
				
			||||||
        this.$toast.update(existingDownload.toastId, { content: `Download "${download.filename}" ${failedMsg}`, options: { timeout: 5000, type: 'error' } }, true)
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        console.warn('Download failed no existing download', existingDownload)
 | 
					 | 
				
			||||||
        this.$toast.error(`Download "${download.filename}" ${failedMsg}`)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    abmergeKilled(download) {
 | 
					 | 
				
			||||||
      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' } }, true)
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        console.warn('Download killed no existing download found', existingDownload)
 | 
					 | 
				
			||||||
        this.$toast.error(`Download "${download.filename}" was terminated`)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.$store.commit('downloads/removeDownload', download)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    abmergeExpired(download) {
 | 
					 | 
				
			||||||
      download.status = this.$constants.DownloadStatus.EXPIRED
 | 
					 | 
				
			||||||
      this.$store.commit('downloads/addUpdateDownload', download)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    rssFeedOpen(data) {
 | 
					    rssFeedOpen(data) {
 | 
				
			||||||
      this.$store.commit('feeds/addFeed', data)
 | 
					      this.$store.commit('feeds/addFeed', data)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -362,7 +323,7 @@ export default {
 | 
				
			|||||||
    batchQuickMatchComplete(result) {
 | 
					    batchQuickMatchComplete(result) {
 | 
				
			||||||
      var success = result.success || false
 | 
					      var success = result.success || false
 | 
				
			||||||
      var toast = 'Batch quick match complete!\n' + result.updates + ' Updated'
 | 
					      var toast = 'Batch quick match complete!\n' + result.updates + ' Updated'
 | 
				
			||||||
      if (result.unmatched && (result.unmatched > 0)) {
 | 
					      if (result.unmatched && result.unmatched > 0) {
 | 
				
			||||||
        toast += '\n' + result.unmatched + ' with no matches'
 | 
					        toast += '\n' + result.unmatched + ' with no matches'
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (success) {
 | 
					      if (success) {
 | 
				
			||||||
@ -430,19 +391,16 @@ export default {
 | 
				
			|||||||
      this.socket.on('scan_complete', this.scanComplete)
 | 
					      this.socket.on('scan_complete', this.scanComplete)
 | 
				
			||||||
      this.socket.on('scan_progress', this.scanProgress)
 | 
					      this.socket.on('scan_progress', this.scanProgress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Download Listeners
 | 
					      // Task Listeners
 | 
				
			||||||
      this.socket.on('abmerge_started', this.abmergeStarted)
 | 
					      this.socket.on('task_started', this.taskStarted)
 | 
				
			||||||
      this.socket.on('abmerge_ready', this.abmergeReady)
 | 
					      this.socket.on('task_finished', this.taskFinished)
 | 
				
			||||||
      this.socket.on('abmerge_failed', this.abmergeFailed)
 | 
					 | 
				
			||||||
      this.socket.on('abmerge_killed', this.abmergeKilled)
 | 
					 | 
				
			||||||
      this.socket.on('abmerge_expired', this.abmergeExpired)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Feed Listeners
 | 
					      // Feed Listeners
 | 
				
			||||||
      this.socket.on('rss_feed_open', this.rssFeedOpen)
 | 
					      this.socket.on('rss_feed_open', this.rssFeedOpen)
 | 
				
			||||||
      this.socket.on('rss_feed_closed', this.rssFeedClosed)
 | 
					      this.socket.on('rss_feed_closed', this.rssFeedClosed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.socket.on('backup_applied', this.backupApplied)
 | 
					      this.socket.on('backup_applied', this.backupApplied)
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
 | 
					      this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showUpdateToast(versionData) {
 | 
					    showUpdateToast(versionData) {
 | 
				
			||||||
@ -548,6 +506,19 @@ export default {
 | 
				
			|||||||
      // Queue auto play
 | 
					      // Queue auto play
 | 
				
			||||||
      var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
 | 
					      var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
 | 
				
			||||||
      this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
 | 
					      this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    loadTasks() {
 | 
				
			||||||
 | 
					      this.$axios
 | 
				
			||||||
 | 
					        .$get('/api/tasks')
 | 
				
			||||||
 | 
					        .then((payload) => {
 | 
				
			||||||
 | 
					          console.log('Fetched tasks', payload)
 | 
				
			||||||
 | 
					          if (payload.tasks) {
 | 
				
			||||||
 | 
					            this.$store.commit('tasks/setTasks', payload.tasks)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed to load tasks', error)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeMount() {
 | 
					  beforeMount() {
 | 
				
			||||||
@ -565,6 +536,8 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.checkVersionUpdate()
 | 
					    this.checkVersionUpdate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.loadTasks()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.$route.query.error) {
 | 
					    if (this.$route.query.error) {
 | 
				
			||||||
      this.$toast.error(this.$route.query.error)
 | 
					      this.$toast.error(this.$route.query.error)
 | 
				
			||||||
      this.$router.replace(this.$route.path)
 | 
					      this.$router.replace(this.$route.path)
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,7 @@
 | 
				
			|||||||
        <div class="w-full max-h-72 overflow-auto">
 | 
					        <div class="w-full max-h-72 overflow-auto">
 | 
				
			||||||
          <p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">No chapters</p>
 | 
					          <p v-if="!metadataChapters.length" class="py-5 text-center text-gray-200">No chapters</p>
 | 
				
			||||||
          <template v-for="(chapter, index) in metadataChapters">
 | 
					          <template v-for="(chapter, index) in metadataChapters">
 | 
				
			||||||
            <div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
 | 
					            <div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 1 ? 'bg-primary bg-opacity-25' : ''">
 | 
				
			||||||
              <div class="flex-grow font-semibold">{{ chapter.title }}</div>
 | 
					              <div class="flex-grow font-semibold">{{ chapter.title }}</div>
 | 
				
			||||||
              <div class="w-24">
 | 
					              <div class="w-24">
 | 
				
			||||||
                {{ $secondsToTimestamp(chapter.start) }}
 | 
					                {{ $secondsToTimestamp(chapter.start) }}
 | 
				
			||||||
@ -67,7 +67,8 @@
 | 
				
			|||||||
        <p v-else class="text-success text-lg font-semibold">Embed Finished!</p>
 | 
					        <p v-else class="text-success text-lg font-semibold">Embed Finished!</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div v-else class="w-full flex justify-end items-center mb-4">
 | 
					      <div v-else class="w-full flex justify-end items-center mb-4">
 | 
				
			||||||
        <ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">Start M4B Encode</ui-btn>
 | 
					        <ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="encodeM4bClick">Start M4B Encode</ui-btn>
 | 
				
			||||||
 | 
					        <p v-else-if="taskFailed" class="text-error text-lg font-semibold">M4B Failed! {{ taskError }}</p>
 | 
				
			||||||
        <p v-else class="text-success text-lg font-semibold">M4B Finished!</p>
 | 
					        <p v-else class="text-success text-lg font-semibold">M4B Finished!</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -160,14 +161,23 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					      processing: false,
 | 
				
			||||||
      audiofilesEncoding: {},
 | 
					      audiofilesEncoding: {},
 | 
				
			||||||
      audiofilesFinished: {},
 | 
					      audiofilesFinished: {},
 | 
				
			||||||
      processing: false,
 | 
					 | 
				
			||||||
      isFinished: false,
 | 
					      isFinished: false,
 | 
				
			||||||
      toneObject: null,
 | 
					      toneObject: null,
 | 
				
			||||||
      selectedTool: 'embed'
 | 
					      selectedTool: 'embed'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    task: {
 | 
				
			||||||
 | 
					      handler(newVal) {
 | 
				
			||||||
 | 
					        if (newVal) {
 | 
				
			||||||
 | 
					          this.taskUpdated(newVal)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    libraryItemId() {
 | 
					    libraryItemId() {
 | 
				
			||||||
      return this.libraryItem.id
 | 
					      return this.libraryItem.id
 | 
				
			||||||
@ -202,6 +212,21 @@ export default {
 | 
				
			|||||||
          { value: 'm4b', text: 'M4B Encoder' }
 | 
					          { value: 'm4b', text: 'M4B Encoder' }
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    taskFailed() {
 | 
				
			||||||
 | 
					      return this.isTaskFinished && this.task.isFailed
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    taskError() {
 | 
				
			||||||
 | 
					      return this.taskFailed ? this.task.error || 'Unknown Error' : null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isTaskFinished() {
 | 
				
			||||||
 | 
					      return this.task && this.task.isFinished
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    task() {
 | 
				
			||||||
 | 
					      return this.$store.getters['tasks/getTaskByLibraryItemId'](this.libraryItemId)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    taskRunning() {
 | 
				
			||||||
 | 
					      return this.task && !this.task.isFinished
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@ -210,12 +235,12 @@ export default {
 | 
				
			|||||||
      this.$axios
 | 
					      this.$axios
 | 
				
			||||||
        .$get(`/api/audiobook-merge/${this.libraryItemId}`)
 | 
					        .$get(`/api/audiobook-merge/${this.libraryItemId}`)
 | 
				
			||||||
        .then(() => {
 | 
					        .then(() => {
 | 
				
			||||||
          this.processing = false
 | 
					          console.log('Ab m4b merge started')
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .catch((error) => {
 | 
					        .catch((error) => {
 | 
				
			||||||
          var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
 | 
					          var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
 | 
				
			||||||
          this.$toast.error(errorMsg)
 | 
					          this.$toast.error(errorMsg)
 | 
				
			||||||
          this.processing = false
 | 
					          this.processing = true
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    embedClick() {
 | 
					    embedClick() {
 | 
				
			||||||
@ -246,7 +271,6 @@ export default {
 | 
				
			|||||||
      console.log('audio metadata started', data)
 | 
					      console.log('audio metadata started', data)
 | 
				
			||||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
					      if (data.libraryItemId !== this.libraryItemId) return
 | 
				
			||||||
      this.audiofilesFinished = {}
 | 
					      this.audiofilesFinished = {}
 | 
				
			||||||
      this.processing = true
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    audioMetadataFinished(data) {
 | 
					    audioMetadataFinished(data) {
 | 
				
			||||||
      console.log('audio metadata finished', data)
 | 
					      console.log('audio metadata finished', data)
 | 
				
			||||||
@ -278,6 +302,8 @@ export default {
 | 
				
			|||||||
          this.selectedToolUpdated()
 | 
					          this.selectedToolUpdated()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.task) this.taskUpdated(this.task)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    fetchToneObject() {
 | 
					    fetchToneObject() {
 | 
				
			||||||
      this.$axios
 | 
					      this.$axios
 | 
				
			||||||
@ -289,6 +315,9 @@ export default {
 | 
				
			|||||||
        .catch((error) => {
 | 
					        .catch((error) => {
 | 
				
			||||||
          console.error('Failed to fetch tone object', error)
 | 
					          console.error('Failed to fetch tone object', error)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    taskUpdated(task) {
 | 
				
			||||||
 | 
					      this.processing = !task.isFinished
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,34 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
export const state = () => ({
 | 
					 | 
				
			||||||
  downloads: []
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getters = {
 | 
					 | 
				
			||||||
  getDownloads: (state) => (libraryItemId) => {
 | 
					 | 
				
			||||||
    return state.downloads.filter(d => d.libraryItemId === libraryItemId)
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  getDownload: (state) => (id) => {
 | 
					 | 
				
			||||||
    return state.downloads.find(d => d.id === id)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const actions = {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const mutations = {
 | 
					 | 
				
			||||||
  setDownloads(state, downloads) {
 | 
					 | 
				
			||||||
    state.downloads = downloads
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  addUpdateDownload(state, download) {
 | 
					 | 
				
			||||||
    var index = state.downloads.findIndex(d => d.id === download.id)
 | 
					 | 
				
			||||||
    if (index >= 0) {
 | 
					 | 
				
			||||||
      state.downloads.splice(index, 1, download)
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      state.downloads.push(download)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  removeDownload(state, download) {
 | 
					 | 
				
			||||||
    state.downloads = state.downloads.filter(d => d.id !== download.id)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										31
									
								
								client/store/tasks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								client/store/tasks.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					export const state = () => ({
 | 
				
			||||||
 | 
					  tasks: []
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getters = {
 | 
				
			||||||
 | 
					  getTaskByLibraryItemId: (state) => (libraryItemId) => {
 | 
				
			||||||
 | 
					    return state.tasks.find(t => t.data && t.data.libraryItemId === libraryItemId)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const actions = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const mutations = {
 | 
				
			||||||
 | 
					  setTasks(state, tasks) {
 | 
				
			||||||
 | 
					    state.tasks = tasks
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  addUpdateTask(state, task) {
 | 
				
			||||||
 | 
					    var index = state.tasks.findIndex(d => d.id === task.id)
 | 
				
			||||||
 | 
					    if (index >= 0) {
 | 
				
			||||||
 | 
					      state.tasks.splice(index, 1, task)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      state.tasks.push(task)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  removeTask(state, task) {
 | 
				
			||||||
 | 
					    state.tasks = state.tasks.filter(d => d.id !== task.id)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -34,6 +34,7 @@ const PodcastManager = require('./managers/PodcastManager')
 | 
				
			|||||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
 | 
					const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
 | 
				
			||||||
const RssFeedManager = require('./managers/RssFeedManager')
 | 
					const RssFeedManager = require('./managers/RssFeedManager')
 | 
				
			||||||
const CronManager = require('./managers/CronManager')
 | 
					const CronManager = require('./managers/CronManager')
 | 
				
			||||||
 | 
					const TaskManager = require('./managers/TaskManager')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Server {
 | 
					class Server {
 | 
				
			||||||
  constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
 | 
					  constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
 | 
				
			||||||
@ -66,22 +67,23 @@ class Server {
 | 
				
			|||||||
    this.auth = new Auth(this.db)
 | 
					    this.auth = new Auth(this.db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Managers
 | 
					    // Managers
 | 
				
			||||||
 | 
					    this.taskManager = new TaskManager(this.emitter.bind(this))
 | 
				
			||||||
    this.notificationManager = new NotificationManager(this.db, this.emitter.bind(this))
 | 
					    this.notificationManager = new NotificationManager(this.db, this.emitter.bind(this))
 | 
				
			||||||
    this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
 | 
					    this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
 | 
				
			||||||
    this.logManager = new LogManager(this.db)
 | 
					    this.logManager = new LogManager(this.db)
 | 
				
			||||||
    this.cacheManager = new CacheManager()
 | 
					    this.cacheManager = new CacheManager()
 | 
				
			||||||
    this.abMergeManager = new AbMergeManager(this.db, this.clientEmitter.bind(this))
 | 
					    this.abMergeManager = new AbMergeManager(this.db, this.taskManager, this.clientEmitter.bind(this))
 | 
				
			||||||
    this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
					    this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
				
			||||||
    this.coverManager = new CoverManager(this.db, this.cacheManager)
 | 
					    this.coverManager = new CoverManager(this.db, this.cacheManager)
 | 
				
			||||||
    this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this), this.notificationManager)
 | 
					    this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this), this.notificationManager)
 | 
				
			||||||
    this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
					    this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
				
			||||||
    this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
 | 
					    this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
 | 
					    this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
 | 
				
			||||||
    this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
 | 
					    this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Routers
 | 
					    // Routers
 | 
				
			||||||
    this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
					    this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
				
			||||||
    this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
 | 
					    this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
 | 
				
			||||||
    this.staticRouter = new StaticRouter(this.db)
 | 
					    this.staticRouter = new StaticRouter(this.db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -127,7 +129,6 @@ class Server {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
    Logger.info('[Server] Init v' + version)
 | 
					    Logger.info('[Server] Init v' + version)
 | 
				
			||||||
    await this.abMergeManager.removeOrphanDownloads()
 | 
					 | 
				
			||||||
    await this.playbackSessionManager.removeOrphanStreams()
 | 
					    await this.playbackSessionManager.removeOrphanStreams()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
 | 
					    var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
 | 
				
			||||||
 | 
				
			|||||||
@ -110,55 +110,13 @@ class MiscController {
 | 
				
			|||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // GET: api/ab-manager-tasks/:id
 | 
					  // GET: api/tasks
 | 
				
			||||||
  async getAbManagerTask(req, res) {
 | 
					  getTasks(req, res) {
 | 
				
			||||||
    if (!req.user.canDownload) {
 | 
					    res.json({
 | 
				
			||||||
      Logger.error('User attempting to download without permission', req.user)
 | 
					      tasks: this.taskManager.tasks.map(t => t.toJSON())
 | 
				
			||||||
      return res.sendStatus(403)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var taskId = req.params.id
 | 
					 | 
				
			||||||
    Logger.info('Download Request', taskId)
 | 
					 | 
				
			||||||
    var task = this.abMergeManager.getTask(taskId)
 | 
					 | 
				
			||||||
    if (!task) {
 | 
					 | 
				
			||||||
      Logger.error('Ab manager task request not found', taskId)
 | 
					 | 
				
			||||||
      return res.sendStatus(404)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var options = {
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        'Content-Type': task.mimeType
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    res.download(task.path, task.filename, options, (err) => {
 | 
					 | 
				
			||||||
      if (err) {
 | 
					 | 
				
			||||||
        Logger.error('Download Error', err)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // DELETE: api/ab-manager-tasks/:id
 | 
					 | 
				
			||||||
  async removeAbManagerTask(req, res) {
 | 
					 | 
				
			||||||
    if (!req.user.canDownload || !req.user.canDelete) {
 | 
					 | 
				
			||||||
      Logger.error('User attempting to remove ab manager task without permission', req.user.username)
 | 
					 | 
				
			||||||
      return res.sendStatus(403)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.abMergeManager.removeTaskById(req.params.id)
 | 
					 | 
				
			||||||
    res.sendStatus(200)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // GET: api/ab-manager-tasks
 | 
					 | 
				
			||||||
  async getAbManagerTasks(req, res) {
 | 
					 | 
				
			||||||
    if (!req.user.canDownload) {
 | 
					 | 
				
			||||||
      Logger.error('User attempting to get ab manager tasks without permission', req.user.username)
 | 
					 | 
				
			||||||
      return res.sendStatus(403)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var taskData = {
 | 
					 | 
				
			||||||
      tasks: this.abMergeManager.tasks,
 | 
					 | 
				
			||||||
      pendingTasks: this.abMergeManager.pendingTasks
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    res.json(taskData)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // PATCH: api/settings (admin)
 | 
					  // PATCH: api/settings (admin)
 | 
				
			||||||
  async updateServerSettings(req, res) {
 | 
					  async updateServerSettings(req, res) {
 | 
				
			||||||
    if (!req.user.isAdminOrUp) {
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,23 +4,22 @@ const fs = require('../libs/fsExtra')
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const workerThreads = require('worker_threads')
 | 
					const workerThreads = require('worker_threads')
 | 
				
			||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
const AbManagerTask = require('../objects/AbManagerTask')
 | 
					const Task = require('../objects/Task')
 | 
				
			||||||
const filePerms = require('../utils/filePerms')
 | 
					const filePerms = require('../utils/filePerms')
 | 
				
			||||||
const { getId } = require('../utils/index')
 | 
					 | 
				
			||||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
 | 
					const { writeConcatFile } = require('../utils/ffmpegHelpers')
 | 
				
			||||||
const toneHelpers = require('../utils/toneHelpers')
 | 
					const toneHelpers = require('../utils/toneHelpers')
 | 
				
			||||||
const { getFileSize } = require('../utils/fileUtils')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AbMergeManager {
 | 
					class AbMergeManager {
 | 
				
			||||||
  constructor(db, clientEmitter) {
 | 
					  constructor(db, taskManager, clientEmitter) {
 | 
				
			||||||
    this.db = db
 | 
					    this.db = db
 | 
				
			||||||
 | 
					    this.taskManager = taskManager
 | 
				
			||||||
    this.clientEmitter = clientEmitter
 | 
					    this.clientEmitter = clientEmitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
 | 
				
			||||||
    this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
 | 
					    this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
 | 
				
			||||||
    this.downloadDirPathExist = false
 | 
					    this.downloadDirPathExist = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.pendingTasks = []
 | 
					    this.pendingTasks = []
 | 
				
			||||||
    this.tasks = []
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
 | 
					  async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
 | 
				
			||||||
@ -39,85 +38,47 @@ class AbMergeManager {
 | 
				
			|||||||
    this.downloadDirPathExist = true
 | 
					    this.downloadDirPathExist = true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getTask(taskId) {
 | 
					 | 
				
			||||||
    return this.tasks.find(d => d.id === taskId)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  removeTaskById(taskId) {
 | 
					 | 
				
			||||||
    var task = this.getTask(taskId)
 | 
					 | 
				
			||||||
    if (task) {
 | 
					 | 
				
			||||||
      this.removeTask(task)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async removeOrphanDownloads() {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      var dirs = await fs.readdir(this.downloadDirPath)
 | 
					 | 
				
			||||||
      if (!dirs || !dirs.length) return true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      dirs = dirs.filter(d => d.startsWith('abmerge'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await Promise.all(dirs.map(async (dirname) => {
 | 
					 | 
				
			||||||
        var fullPath = Path.join(this.downloadDirPath, dirname)
 | 
					 | 
				
			||||||
        Logger.info(`Removing Orphan Download ${dirname}`)
 | 
					 | 
				
			||||||
        return fs.remove(fullPath)
 | 
					 | 
				
			||||||
      }))
 | 
					 | 
				
			||||||
      return true
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      return false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async startAudiobookMerge(user, libraryItem) {
 | 
					  async startAudiobookMerge(user, libraryItem) {
 | 
				
			||||||
    var taskId = getId('abmerge')
 | 
					    const task = new Task()
 | 
				
			||||||
    var dlpath = Path.join(this.downloadDirPath, taskId)
 | 
					 | 
				
			||||||
    Logger.info(`Start audiobook merge for ${libraryItem.id} - TaskId: ${taskId} - ${dlpath}`)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var audiobookDirname = Path.basename(libraryItem.path)
 | 
					    const audiobookDirname = Path.basename(libraryItem.path)
 | 
				
			||||||
    var filename = audiobookDirname + '.m4b'
 | 
					    const targetFilename = audiobookDirname + '.m4b'
 | 
				
			||||||
    var taskData = {
 | 
					    const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
 | 
				
			||||||
      id: taskId,
 | 
					    const tempFilepath = Path.join(itemCachePath, targetFilename)
 | 
				
			||||||
 | 
					    const taskData = {
 | 
				
			||||||
      libraryItemId: libraryItem.id,
 | 
					      libraryItemId: libraryItem.id,
 | 
				
			||||||
      type: 'abmerge',
 | 
					 | 
				
			||||||
      dirpath: dlpath,
 | 
					 | 
				
			||||||
      path: Path.join(dlpath, filename),
 | 
					 | 
				
			||||||
      filename,
 | 
					 | 
				
			||||||
      ext: '.m4b',
 | 
					 | 
				
			||||||
      userId: user.id,
 | 
					 | 
				
			||||||
      libraryItemPath: libraryItem.path,
 | 
					      libraryItemPath: libraryItem.path,
 | 
				
			||||||
      originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path)
 | 
					      userId: user.id,
 | 
				
			||||||
 | 
					      originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path),
 | 
				
			||||||
 | 
					      tempFilepath,
 | 
				
			||||||
 | 
					      targetFilename,
 | 
				
			||||||
 | 
					      targetFilepath: Path.join(libraryItem.path, targetFilename),
 | 
				
			||||||
 | 
					      itemCachePath,
 | 
				
			||||||
 | 
					      toneMetadataObject: null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    var abManagerTask = new AbManagerTask()
 | 
					    const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
 | 
				
			||||||
    abManagerTask.setData(taskData)
 | 
					    task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData)
 | 
				
			||||||
    abManagerTask.setTimeoutTimer(this.downloadTimedOut.bind(this))
 | 
					    this.taskManager.addTask(task)
 | 
				
			||||||
 | 
					    Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    if (!await fs.pathExists(taskData.itemCachePath)) {
 | 
				
			||||||
      await fs.mkdir(abManagerTask.dirpath)
 | 
					      await fs.mkdir(taskData.itemCachePath)
 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      Logger.error(`[AbMergeManager] Failed to make directory ${abManagerTask.dirpath}`)
 | 
					 | 
				
			||||||
      Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
 | 
					 | 
				
			||||||
      var taskJson = abManagerTask.toJSON()
 | 
					 | 
				
			||||||
      this.clientEmitter(user.id, 'abmerge_failed', taskJson)
 | 
					 | 
				
			||||||
      return
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.clientEmitter(user.id, 'abmerge_started', abManagerTask.toJSON())
 | 
					    this.runAudiobookMerge(libraryItem, task)
 | 
				
			||||||
    this.runAudiobookMerge(libraryItem, abManagerTask)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async runAudiobookMerge(libraryItem, abManagerTask) {
 | 
					  async runAudiobookMerge(libraryItem, task) {
 | 
				
			||||||
    // If changing audio file type then encoding is needed
 | 
					    // If changing audio file type then encoding is needed
 | 
				
			||||||
    var audioTracks = libraryItem.media.tracks
 | 
					    var audioTracks = libraryItem.media.tracks
 | 
				
			||||||
    var audioRequiresEncode = audioTracks[0].metadata.ext !== abManagerTask.ext
 | 
					    var audioRequiresEncode = audioTracks[0].metadata.ext !== '.m4b'
 | 
				
			||||||
    var shouldIncludeCover = libraryItem.media.coverPath
 | 
					 | 
				
			||||||
    var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b'
 | 
					    var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b'
 | 
				
			||||||
    var isOneTrack = audioTracks.length === 1
 | 
					    var isOneTrack = audioTracks.length === 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ffmpegInputs = []
 | 
					    const ffmpegInputs = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!isOneTrack) {
 | 
					    if (!isOneTrack) {
 | 
				
			||||||
      var concatFilePath = Path.join(abManagerTask.dirpath, 'files.txt')
 | 
					      var concatFilePath = Path.join(task.data.itemCachePath, 'files.txt')
 | 
				
			||||||
      console.log('Write files.txt', concatFilePath)
 | 
					 | 
				
			||||||
      await writeConcatFile(audioTracks, concatFilePath)
 | 
					      await writeConcatFile(audioTracks, concatFilePath)
 | 
				
			||||||
      ffmpegInputs.push({
 | 
					      ffmpegInputs.push({
 | 
				
			||||||
        input: concatFilePath,
 | 
					        input: concatFilePath,
 | 
				
			||||||
@ -132,7 +93,7 @@ class AbMergeManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
 | 
					    const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
 | 
				
			||||||
    var ffmpegOptions = [`-loglevel ${logLevel}`]
 | 
					    var ffmpegOptions = [`-loglevel ${logLevel}`]
 | 
				
			||||||
    var ffmpegOutputOptions = []
 | 
					    var ffmpegOutputOptions = ['-f mp4']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (audioRequiresEncode) {
 | 
					    if (audioRequiresEncode) {
 | 
				
			||||||
      ffmpegOptions = ffmpegOptions.concat([
 | 
					      ffmpegOptions = ffmpegOptions.concat([
 | 
				
			||||||
@ -140,25 +101,20 @@ class AbMergeManager {
 | 
				
			|||||||
        '-acodec aac',
 | 
					        '-acodec aac',
 | 
				
			||||||
        '-ac 2',
 | 
					        '-ac 2',
 | 
				
			||||||
        '-b:a 64k'
 | 
					        '-b:a 64k'
 | 
				
			||||||
        // '-movflags use_metadata_tags'
 | 
					 | 
				
			||||||
      ])
 | 
					      ])
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      ffmpegOptions.push('-max_muxing_queue_size 1000')
 | 
					      ffmpegOptions.push('-max_muxing_queue_size 1000')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isOneTrack && firstTrackIsM4b && !shouldIncludeCover) {
 | 
					      if (isOneTrack && firstTrackIsM4b) {
 | 
				
			||||||
        ffmpegOptions.push('-c copy')
 | 
					        ffmpegOptions.push('-c copy')
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        ffmpegOptions.push('-c:a copy')
 | 
					        ffmpegOptions.push('-c:a copy')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (abManagerTask.ext === '.m4b') {
 | 
					 | 
				
			||||||
      ffmpegOutputOptions.push('-f mp4')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var chaptersFilePath = null
 | 
					    var chaptersFilePath = null
 | 
				
			||||||
    if (libraryItem.media.chapters.length) {
 | 
					    if (libraryItem.media.chapters.length) {
 | 
				
			||||||
      chaptersFilePath = Path.join(abManagerTask.dirpath, 'chapters.txt')
 | 
					      chaptersFilePath = Path.join(task.data.itemCachePath, 'chapters.txt')
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
 | 
					        await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
@ -169,7 +125,7 @@ class AbMergeManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
 | 
					    const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
 | 
				
			||||||
    toneMetadataObject.TrackNumber = 1
 | 
					    toneMetadataObject.TrackNumber = 1
 | 
				
			||||||
    abManagerTask.toneMetadataObject = toneMetadataObject
 | 
					    task.data.toneMetadataObject = toneMetadataObject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
 | 
					    Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -177,7 +133,7 @@ class AbMergeManager {
 | 
				
			|||||||
      inputs: ffmpegInputs,
 | 
					      inputs: ffmpegInputs,
 | 
				
			||||||
      options: ffmpegOptions,
 | 
					      options: ffmpegOptions,
 | 
				
			||||||
      outputOptions: ffmpegOutputOptions,
 | 
					      outputOptions: ffmpegOutputOptions,
 | 
				
			||||||
      output: abManagerTask.path,
 | 
					      output: task.data.tempFilepath
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var worker = null
 | 
					    var worker = null
 | 
				
			||||||
@ -186,137 +142,83 @@ class AbMergeManager {
 | 
				
			|||||||
      worker = new workerThreads.Worker(workerPath, { workerData })
 | 
					      worker = new workerThreads.Worker(workerPath, { workerData })
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      Logger.error(`[AbMergeManager] Start worker thread failed`, error)
 | 
					      Logger.error(`[AbMergeManager] Start worker thread failed`, error)
 | 
				
			||||||
      if (abManagerTask.userId) {
 | 
					      task.setFailed('Failed to start worker thread')
 | 
				
			||||||
        var taskJson = abManagerTask.toJSON()
 | 
					      this.removeTask(task, true)
 | 
				
			||||||
        this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worker.on('message', (message) => {
 | 
					    worker.on('message', (message) => {
 | 
				
			||||||
      if (message != null && typeof message === 'object') {
 | 
					      if (message != null && typeof message === 'object') {
 | 
				
			||||||
        if (message.type === 'RESULT') {
 | 
					        if (message.type === 'RESULT') {
 | 
				
			||||||
          if (!abManagerTask.isTimedOut) {
 | 
					          this.sendResult(task, message)
 | 
				
			||||||
            this.sendResult(abManagerTask, message)
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        } else if (message.type === 'FFMPEG') {
 | 
					        } else if (message.type === 'FFMPEG') {
 | 
				
			||||||
          if (Logger[message.level]) {
 | 
					          if (Logger[message.level]) {
 | 
				
			||||||
            Logger[message.level](message.log)
 | 
					            Logger[message.level](message.log)
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        Logger.error('Invalid worker message', message)
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    this.pendingTasks.push({
 | 
					    this.pendingTasks.push({
 | 
				
			||||||
      id: abManagerTask.id,
 | 
					      id: task.id,
 | 
				
			||||||
      abManagerTask,
 | 
					      task,
 | 
				
			||||||
      worker
 | 
					      worker
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async sendResult(abManagerTask, result) {
 | 
					  async sendResult(task, result) {
 | 
				
			||||||
    abManagerTask.clearTimeoutTimer()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Remove pending task
 | 
					    // Remove pending task
 | 
				
			||||||
    this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
 | 
					    this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (result.isKilled) {
 | 
					    if (result.isKilled) {
 | 
				
			||||||
      if (abManagerTask.userId) {
 | 
					      task.setFailed('Ffmpeg task killed')
 | 
				
			||||||
        this.clientEmitter(abManagerTask.userId, 'abmerge_killed', abManagerTask.toJSON())
 | 
					      this.removeTask(task, true)
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!result.success) {
 | 
					    if (!result.success) {
 | 
				
			||||||
      if (abManagerTask.userId) {
 | 
					      task.setFailed('Encoding failed')
 | 
				
			||||||
        this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON())
 | 
					      this.removeTask(task, true)
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Write metadata to merged file
 | 
					    // Write metadata to merged file
 | 
				
			||||||
    const success = await toneHelpers.tagAudioFile(abManagerTask.path, abManagerTask.toneMetadataObject)
 | 
					    const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneMetadataObject)
 | 
				
			||||||
    if (!success) {
 | 
					    if (!success) {
 | 
				
			||||||
      Logger.error(`[AbMergeManager] Failed to write metadata to file "${abManagerTask.path}"`)
 | 
					      Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`)
 | 
				
			||||||
      if (abManagerTask.userId) {
 | 
					      task.setFailed('Failed to write metadata to m4b file')
 | 
				
			||||||
        this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON())
 | 
					      this.removeTask(task, true)
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Move library item tracks to cache
 | 
					    // Move library item tracks to cache
 | 
				
			||||||
    const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${abManagerTask.libraryItemId}`)
 | 
					    for (const trackPath of task.data.originalTrackPaths) {
 | 
				
			||||||
    await fs.ensureDir(itemCacheDir)
 | 
					 | 
				
			||||||
    for (const trackPath of abManagerTask.originalTrackPaths) {
 | 
					 | 
				
			||||||
      const trackFilename = Path.basename(trackPath)
 | 
					      const trackFilename = Path.basename(trackPath)
 | 
				
			||||||
      const moveToPath = Path.join(itemCacheDir, trackFilename)
 | 
					      const moveToPath = Path.join(task.data.itemCachePath, trackFilename)
 | 
				
			||||||
      Logger.debug(`[AbMergeManager] Backing up original track "${trackPath}" to ${moveToPath}`)
 | 
					      Logger.debug(`[AbMergeManager] Backing up original track "${trackPath}" to ${moveToPath}`)
 | 
				
			||||||
      await fs.move(trackPath, moveToPath, { overwrite: true }).catch((err) => {
 | 
					      await fs.move(trackPath, moveToPath, { overwrite: true }).catch((err) => {
 | 
				
			||||||
        Logger.error(`[AbMergeManager] Failed to move track "${trackPath}" to "${moveToPath}"`, err)
 | 
					        Logger.error(`[AbMergeManager] Failed to move track "${trackPath}" to "${moveToPath}"`, err)
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Move m4b to target
 | 
				
			||||||
 | 
					    Logger.debug(`[AbMergeManager] Moving m4b from ${task.data.tempFilepath} to ${task.data.targetFilepath}`)
 | 
				
			||||||
 | 
					    await fs.move(task.data.tempFilepath, task.data.targetFilepath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set file permissions and ownership
 | 
					    // Set file permissions and ownership
 | 
				
			||||||
    await filePerms.setDefault(abManagerTask.path)
 | 
					    await filePerms.setDefault(task.data.targetFilepath)
 | 
				
			||||||
    await filePerms.setDefault(itemCacheDir)
 | 
					    await filePerms.setDefault(task.data.itemCachePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Move merged file to library item
 | 
					    task.setFinished()
 | 
				
			||||||
    const moveToPath = Path.join(abManagerTask.libraryItemPath, abManagerTask.filename)
 | 
					    await this.removeTask(task, false)
 | 
				
			||||||
    Logger.debug(`[AbMergeManager] Moving merged audiobook to library item at "${moveToPath}"`)
 | 
					    Logger.info(`[AbMergeManager] Ab task finished ${task.id}`)
 | 
				
			||||||
    const moveSuccess = await fs.move(abManagerTask.path, moveToPath, { overwrite: true }).then(() => true).catch((err) => {
 | 
					 | 
				
			||||||
      Logger.error(`[AbMergeManager] Failed to move merged audiobook from "${abManagerTask.path}" to "${moveToPath}"`, err)
 | 
					 | 
				
			||||||
      return false
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    if (!moveSuccess) {
 | 
					 | 
				
			||||||
      // TODO: Revert cached og files?
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var filesize = await getFileSize(abManagerTask.path)
 | 
					 | 
				
			||||||
    abManagerTask.setComplete(filesize)
 | 
					 | 
				
			||||||
    if (abManagerTask.userId) {
 | 
					 | 
				
			||||||
      this.clientEmitter(abManagerTask.userId, 'abmerge_ready', abManagerTask.toJSON())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // abManagerTask.setExpirationTimer(this.downloadExpired.bind(this))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // this.tasks.push(abManagerTask)
 | 
					 | 
				
			||||||
    await this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
    Logger.info(`[AbMergeManager] Ab task finished ${abManagerTask.id}`)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // async downloadExpired(abManagerTask) {
 | 
					  async removeTask(task, removeTempFilepath = false) {
 | 
				
			||||||
  //   Logger.info(`[AbMergeManager] Download ${abManagerTask.id} expired`)
 | 
					    Logger.info('[AbMergeManager] Removing task ' + task.id)
 | 
				
			||||||
 | 
					 | 
				
			||||||
  //   if (abManagerTask.userId) {
 | 
					 | 
				
			||||||
  //     this.clientEmitter(abManagerTask.userId, 'abmerge_expired', abManagerTask.toJSON())
 | 
					 | 
				
			||||||
  //   }
 | 
					 | 
				
			||||||
  //   this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
  // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async downloadTimedOut(abManagerTask) {
 | 
					 | 
				
			||||||
    Logger.info(`[AbMergeManager] Download ${abManagerTask.id} timed out (${abManagerTask.timeoutTimeMs}ms)`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (abManagerTask.userId) {
 | 
					 | 
				
			||||||
      var taskJson = abManagerTask.toJSON()
 | 
					 | 
				
			||||||
      taskJson.isTimedOut = true
 | 
					 | 
				
			||||||
      this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this.removeTask(abManagerTask)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async removeTask(abManagerTask) {
 | 
					 | 
				
			||||||
    Logger.info('[AbMergeManager] Removing task ' + abManagerTask.id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    abManagerTask.clearTimeoutTimer()
 | 
					 | 
				
			||||||
    // abManagerTask.clearExpirationTimer()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var pendingDl = this.pendingTasks.find(d => d.id === abManagerTask.id)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const pendingDl = this.pendingTasks.find(d => d.id === task.id)
 | 
				
			||||||
    if (pendingDl) {
 | 
					    if (pendingDl) {
 | 
				
			||||||
      this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
 | 
					      this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id)
 | 
				
			||||||
      Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`)
 | 
					      Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`)
 | 
				
			||||||
      if (pendingDl.worker) {
 | 
					      if (pendingDl.worker) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@ -327,12 +229,17 @@ class AbMergeManager {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await fs.remove(abManagerTask.dirpath).then(() => {
 | 
					    if (removeTempFilepath) { // On failed tasks remove the bad file if it exists
 | 
				
			||||||
      Logger.info('[AbMergeManager] Deleted download', abManagerTask.dirpath)
 | 
					      if (await fs.pathExists(task.data.tempFilepath)) {
 | 
				
			||||||
    }).catch((err) => {
 | 
					        await fs.remove(task.data.tempFilepath).then(() => {
 | 
				
			||||||
      Logger.error('[AbMergeManager] Failed to delete download', err)
 | 
					          Logger.info('[AbMergeManager] Deleted target file', task.data.tempFilepath)
 | 
				
			||||||
    })
 | 
					        }).catch((err) => {
 | 
				
			||||||
    this.tasks = this.tasks.filter(d => d.id !== abManagerTask.id)
 | 
					          Logger.error('[AbMergeManager] Failed to delete target file', err)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.taskManager.taskFinished(task)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = AbMergeManager
 | 
					module.exports = AbMergeManager
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,9 @@ const { writeMetadataFile } = require('../utils/ffmpegHelpers')
 | 
				
			|||||||
const toneHelpers = require('../utils/toneHelpers')
 | 
					const toneHelpers = require('../utils/toneHelpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AudioMetadataMangaer {
 | 
					class AudioMetadataMangaer {
 | 
				
			||||||
  constructor(db, emitter, clientEmitter) {
 | 
					  constructor(db, taskManager, emitter, clientEmitter) {
 | 
				
			||||||
    this.db = db
 | 
					    this.db = db
 | 
				
			||||||
 | 
					    this.taskManager = taskManager
 | 
				
			||||||
    this.emitter = emitter
 | 
					    this.emitter = emitter
 | 
				
			||||||
    this.clientEmitter = clientEmitter
 | 
					    this.clientEmitter = clientEmitter
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -25,22 +25,6 @@ class DownloadManager {
 | 
				
			|||||||
    return this.downloads.find(d => d.id === downloadId)
 | 
					    return this.downloads.find(d => d.id === downloadId)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async removeOrphanDownloads() {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      var dirs = await fs.readdir(this.downloadDirPath)
 | 
					 | 
				
			||||||
      if (!dirs || !dirs.length) return true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await Promise.all(dirs.map(async (dirname) => {
 | 
					 | 
				
			||||||
        var fullPath = Path.join(this.downloadDirPath, dirname)
 | 
					 | 
				
			||||||
        Logger.info(`Removing Orphan Download ${dirname}`)
 | 
					 | 
				
			||||||
        return fs.remove(fullPath)
 | 
					 | 
				
			||||||
      }))
 | 
					 | 
				
			||||||
      return true
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      return false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  downloadSocketRequest(socket, payload) {
 | 
					  downloadSocketRequest(socket, payload) {
 | 
				
			||||||
    var client = socket.sheepClient
 | 
					    var client = socket.sheepClient
 | 
				
			||||||
    var audiobook = this.db.audiobooks.find(a => a.id === payload.audiobookId)
 | 
					    var audiobook = this.db.audiobooks.find(a => a.id === payload.audiobookId)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								server/managers/TaskManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								server/managers/TaskManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					class TaskManager {
 | 
				
			||||||
 | 
					  constructor(emitter) {
 | 
				
			||||||
 | 
					    this.emitter = emitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.tasks = []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  addTask(task) {
 | 
				
			||||||
 | 
					    this.tasks.push(task)
 | 
				
			||||||
 | 
					    this.emitter('task_started', task.toJSON())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  taskFinished(task) {
 | 
				
			||||||
 | 
					    if (this.tasks.some(t => t.id === task.id)) {
 | 
				
			||||||
 | 
					      this.tasks = this.tasks.filter(t => t !== task.id)
 | 
				
			||||||
 | 
					      this.emitter('task_finished', task.toJSON())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = TaskManager
 | 
				
			||||||
@ -1,122 +0,0 @@
 | 
				
			|||||||
const { AudioMimeType } = require('../utils/constants')
 | 
					 | 
				
			||||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
 | 
					 | 
				
			||||||
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
 | 
					 | 
				
			||||||
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AbManagerTask {
 | 
					 | 
				
			||||||
  constructor() {
 | 
					 | 
				
			||||||
    this.id = null
 | 
					 | 
				
			||||||
    this.libraryItemId = null
 | 
					 | 
				
			||||||
    this.libraryItemPath = null
 | 
					 | 
				
			||||||
    this.type = null
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.dirpath = null
 | 
					 | 
				
			||||||
    this.path = null
 | 
					 | 
				
			||||||
    this.ext = null
 | 
					 | 
				
			||||||
    this.filename = null
 | 
					 | 
				
			||||||
    this.size = 0
 | 
					 | 
				
			||||||
    this.toneMetadataObject = null
 | 
					 | 
				
			||||||
    this.originalTrackPaths = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.userId = null
 | 
					 | 
				
			||||||
    this.isReady = false
 | 
					 | 
				
			||||||
    this.isTimedOut = false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.startedAt = null
 | 
					 | 
				
			||||||
    this.finishedAt = null
 | 
					 | 
				
			||||||
    this.expiresAt = null
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.expirationTimeMs = 0
 | 
					 | 
				
			||||||
    this.timeoutTimeMs = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.timeoutTimer = null
 | 
					 | 
				
			||||||
    this.expirationTimer = null
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get mimeType() {
 | 
					 | 
				
			||||||
    return getAudioMimeTypeFromExtname(this.ext) || AudioMimeType.MP3
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  toJSON() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      id: this.id,
 | 
					 | 
				
			||||||
      libraryItemId: this.libraryItemId,
 | 
					 | 
				
			||||||
      type: this.type,
 | 
					 | 
				
			||||||
      dirpath: this.dirpath,
 | 
					 | 
				
			||||||
      path: this.path,
 | 
					 | 
				
			||||||
      ext: this.ext,
 | 
					 | 
				
			||||||
      filename: this.filename,
 | 
					 | 
				
			||||||
      size: this.size,
 | 
					 | 
				
			||||||
      userId: this.userId,
 | 
					 | 
				
			||||||
      isReady: this.isReady,
 | 
					 | 
				
			||||||
      startedAt: this.startedAt,
 | 
					 | 
				
			||||||
      finishedAt: this.finishedAt,
 | 
					 | 
				
			||||||
      expirationSeconds: this.expirationSeconds,
 | 
					 | 
				
			||||||
      toneMetadataObject: this.toneMetadataObject
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setData(downloadData) {
 | 
					 | 
				
			||||||
    downloadData.startedAt = Date.now()
 | 
					 | 
				
			||||||
    downloadData.isProcessing = true
 | 
					 | 
				
			||||||
    this.construct(downloadData)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  construct(download) {
 | 
					 | 
				
			||||||
    this.id = download.id
 | 
					 | 
				
			||||||
    this.libraryItemId = download.libraryItemId
 | 
					 | 
				
			||||||
    this.libraryItemPath = download.libraryItemPath
 | 
					 | 
				
			||||||
    this.type = download.type
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.dirpath = download.dirpath
 | 
					 | 
				
			||||||
    this.path = download.path
 | 
					 | 
				
			||||||
    this.ext = download.ext
 | 
					 | 
				
			||||||
    this.filename = download.filename
 | 
					 | 
				
			||||||
    this.size = download.size || 0
 | 
					 | 
				
			||||||
    this.originalTrackPaths = download.originalTrackPaths
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.userId = download.userId
 | 
					 | 
				
			||||||
    this.isReady = !!download.isReady
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.startedAt = download.startedAt
 | 
					 | 
				
			||||||
    this.finishedAt = download.finishedAt || null
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION
 | 
					 | 
				
			||||||
    this.timeoutTimeMs = download.timeoutTimeMs || DEFAULT_TIMEOUT
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.expiresAt = download.expiresAt || null
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setComplete(fileSize) {
 | 
					 | 
				
			||||||
    this.finishedAt = Date.now()
 | 
					 | 
				
			||||||
    this.size = fileSize
 | 
					 | 
				
			||||||
    this.isReady = true
 | 
					 | 
				
			||||||
    this.expiresAt = this.finishedAt + this.expirationTimeMs
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setExpirationTimer(callback) {
 | 
					 | 
				
			||||||
    this.expirationTimer = setTimeout(() => {
 | 
					 | 
				
			||||||
      if (callback) {
 | 
					 | 
				
			||||||
        callback(this)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }, this.expirationTimeMs)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setTimeoutTimer(callback) {
 | 
					 | 
				
			||||||
    this.timeoutTimer = setTimeout(() => {
 | 
					 | 
				
			||||||
      if (callback) {
 | 
					 | 
				
			||||||
        this.isTimedOut = true
 | 
					 | 
				
			||||||
        callback(this)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }, this.timeoutTimeMs)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  clearTimeoutTimer() {
 | 
					 | 
				
			||||||
    clearTimeout(this.timeoutTimer)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  clearExpirationTimer() {
 | 
					 | 
				
			||||||
    clearTimeout(this.expirationTimer)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
module.exports = AbManagerTask
 | 
					 | 
				
			||||||
							
								
								
									
										56
									
								
								server/objects/Task.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								server/objects/Task.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					const { getId } = require('../utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Task {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    this.id = null
 | 
				
			||||||
 | 
					    this.action = null // e.g. embed-metadata, encode-m4b, etc
 | 
				
			||||||
 | 
					    this.data = null // additional info for the action like libraryItemId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.title = null
 | 
				
			||||||
 | 
					    this.description = null
 | 
				
			||||||
 | 
					    this.error = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.isFailed = false
 | 
				
			||||||
 | 
					    this.isFinished = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.startedAt = null
 | 
				
			||||||
 | 
					    this.finishedAt = null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toJSON() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id: this.id,
 | 
				
			||||||
 | 
					      action: this.action,
 | 
				
			||||||
 | 
					      data: this.data ? { ...this.data } : {},
 | 
				
			||||||
 | 
					      title: this.title,
 | 
				
			||||||
 | 
					      description: this.description,
 | 
				
			||||||
 | 
					      error: this.error,
 | 
				
			||||||
 | 
					      isFailed: this.isFailed,
 | 
				
			||||||
 | 
					      isFinished: this.isFinished,
 | 
				
			||||||
 | 
					      startedAt: this.startedAt,
 | 
				
			||||||
 | 
					      finishedAt: this.finishedAt
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setData(action, title, description, data = {}) {
 | 
				
			||||||
 | 
					    this.id = getId(action)
 | 
				
			||||||
 | 
					    this.action = action
 | 
				
			||||||
 | 
					    this.data = { ...data }
 | 
				
			||||||
 | 
					    this.title = title
 | 
				
			||||||
 | 
					    this.description = description
 | 
				
			||||||
 | 
					    this.startedAt = Date.now()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setFailed(message) {
 | 
				
			||||||
 | 
					    this.error = message
 | 
				
			||||||
 | 
					    this.isFailed = true
 | 
				
			||||||
 | 
					    this.failedAt = Date.now()
 | 
				
			||||||
 | 
					    this.setFinished()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setFinished() {
 | 
				
			||||||
 | 
					    this.isFinished = true
 | 
				
			||||||
 | 
					    this.finishedAt = Date.now()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = Task
 | 
				
			||||||
@ -26,7 +26,7 @@ const Series = require('../objects/entities/Series')
 | 
				
			|||||||
const FileSystemController = require('../controllers/FileSystemController')
 | 
					const FileSystemController = require('../controllers/FileSystemController')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApiRouter {
 | 
					class ApiRouter {
 | 
				
			||||||
  constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, emitter, clientEmitter) {
 | 
					  constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, taskManager, emitter, clientEmitter) {
 | 
				
			||||||
    this.db = db
 | 
					    this.db = db
 | 
				
			||||||
    this.auth = auth
 | 
					    this.auth = auth
 | 
				
			||||||
    this.scanner = scanner
 | 
					    this.scanner = scanner
 | 
				
			||||||
@ -41,6 +41,7 @@ class ApiRouter {
 | 
				
			|||||||
    this.rssFeedManager = rssFeedManager
 | 
					    this.rssFeedManager = rssFeedManager
 | 
				
			||||||
    this.cronManager = cronManager
 | 
					    this.cronManager = cronManager
 | 
				
			||||||
    this.notificationManager = notificationManager
 | 
					    this.notificationManager = notificationManager
 | 
				
			||||||
 | 
					    this.taskManager = taskManager
 | 
				
			||||||
    this.emitter = emitter
 | 
					    this.emitter = emitter
 | 
				
			||||||
    this.clientEmitter = clientEmitter
 | 
					    this.clientEmitter = clientEmitter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -224,9 +225,7 @@ class ApiRouter {
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    this.router.post('/upload', MiscController.handleUpload.bind(this))
 | 
					    this.router.post('/upload', MiscController.handleUpload.bind(this))
 | 
				
			||||||
    this.router.get('/audiobook-merge/:id', MiscController.mergeAudiobook.bind(this))
 | 
					    this.router.get('/audiobook-merge/:id', MiscController.mergeAudiobook.bind(this))
 | 
				
			||||||
    this.router.get('/ab-manager-tasks/:id', MiscController.getAbManagerTask.bind(this))
 | 
					    this.router.get('/tasks', MiscController.getTasks.bind(this))
 | 
				
			||||||
    this.router.delete('/ab-manager-tasks/:id', MiscController.removeAbManagerTask.bind(this))
 | 
					 | 
				
			||||||
    this.router.get('/ab-manager-tasks', MiscController.getAbManagerTasks.bind(this))
 | 
					 | 
				
			||||||
    this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
 | 
					    this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
 | 
				
			||||||
    this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
 | 
					    this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
 | 
				
			||||||
    this.router.post('/authorize', MiscController.authorize.bind(this))
 | 
					    this.router.post('/authorize', MiscController.authorize.bind(this))
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user