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 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> | ||||
| 
 | ||||
|         <ui-libraries-dropdown class="mr-2" /> | ||||
| @ -15,7 +15,7 @@ | ||||
|         <controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" /> | ||||
|         <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"> | ||||
|           <span class="material-icons-outlined text-warning text-opacity-50"> cast </span> | ||||
|  | ||||
| @ -42,7 +42,7 @@ | ||||
|         </div> | ||||
|         <div class="flex-grow" /> | ||||
|         <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 | ||||
|             <span class="material-icons text-lg ml-2">launch</span> | ||||
|           </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) | ||||
|     }, | ||||
|     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) { | ||||
|       if (this.$store.state.user.user.id === user.id) { | ||||
|         this.$store.commit('user/setUser', user) | ||||
| @ -302,53 +310,6 @@ export default { | ||||
|       } | ||||
|       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) { | ||||
|       this.$store.commit('feeds/addFeed', data) | ||||
|     }, | ||||
| @ -362,7 +323,7 @@ export default { | ||||
|     batchQuickMatchComplete(result) { | ||||
|       var success = result.success || false | ||||
|       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' | ||||
|       } | ||||
|       if (success) { | ||||
| @ -430,12 +391,9 @@ export default { | ||||
|       this.socket.on('scan_complete', this.scanComplete) | ||||
|       this.socket.on('scan_progress', this.scanProgress) | ||||
| 
 | ||||
|       // Download Listeners | ||||
|       this.socket.on('abmerge_started', this.abmergeStarted) | ||||
|       this.socket.on('abmerge_ready', this.abmergeReady) | ||||
|       this.socket.on('abmerge_failed', this.abmergeFailed) | ||||
|       this.socket.on('abmerge_killed', this.abmergeKilled) | ||||
|       this.socket.on('abmerge_expired', this.abmergeExpired) | ||||
|       // Task Listeners | ||||
|       this.socket.on('task_started', this.taskStarted) | ||||
|       this.socket.on('task_finished', this.taskFinished) | ||||
| 
 | ||||
|       // Feed Listeners | ||||
|       this.socket.on('rss_feed_open', this.rssFeedOpen) | ||||
| @ -548,6 +506,19 @@ export default { | ||||
|       // Queue auto play | ||||
|       var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay') | ||||
|       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() { | ||||
| @ -565,6 +536,8 @@ export default { | ||||
| 
 | ||||
|     this.checkVersionUpdate() | ||||
| 
 | ||||
|     this.loadTasks() | ||||
| 
 | ||||
|     if (this.$route.query.error) { | ||||
|       this.$toast.error(this.$route.query.error) | ||||
|       this.$router.replace(this.$route.path) | ||||
|  | ||||
| @ -45,7 +45,7 @@ | ||||
|         <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> | ||||
|           <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="w-24"> | ||||
|                 {{ $secondsToTimestamp(chapter.start) }} | ||||
| @ -67,7 +67,8 @@ | ||||
|         <p v-else class="text-success text-lg font-semibold">Embed Finished!</p> | ||||
|       </div> | ||||
|       <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> | ||||
|       </div> | ||||
| 
 | ||||
| @ -160,14 +161,23 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       processing: false, | ||||
|       audiofilesEncoding: {}, | ||||
|       audiofilesFinished: {}, | ||||
|       processing: false, | ||||
|       isFinished: false, | ||||
|       toneObject: null, | ||||
|       selectedTool: 'embed' | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     task: { | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.taskUpdated(newVal) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     libraryItemId() { | ||||
|       return this.libraryItem.id | ||||
| @ -202,6 +212,21 @@ export default { | ||||
|           { 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: { | ||||
| @ -210,12 +235,12 @@ export default { | ||||
|       this.$axios | ||||
|         .$get(`/api/audiobook-merge/${this.libraryItemId}`) | ||||
|         .then(() => { | ||||
|           this.processing = false | ||||
|           console.log('Ab m4b merge started') | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' | ||||
|           this.$toast.error(errorMsg) | ||||
|           this.processing = false | ||||
|           this.processing = true | ||||
|         }) | ||||
|     }, | ||||
|     embedClick() { | ||||
| @ -246,7 +271,6 @@ export default { | ||||
|       console.log('audio metadata started', data) | ||||
|       if (data.libraryItemId !== this.libraryItemId) return | ||||
|       this.audiofilesFinished = {} | ||||
|       this.processing = true | ||||
|     }, | ||||
|     audioMetadataFinished(data) { | ||||
|       console.log('audio metadata finished', data) | ||||
| @ -278,6 +302,8 @@ export default { | ||||
|           this.selectedToolUpdated() | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (this.task) this.taskUpdated(this.task) | ||||
|     }, | ||||
|     fetchToneObject() { | ||||
|       this.$axios | ||||
| @ -289,6 +315,9 @@ export default { | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to fetch tone object', error) | ||||
|         }) | ||||
|     }, | ||||
|     taskUpdated(task) { | ||||
|       this.processing = !task.isFinished | ||||
|     } | ||||
|   }, | ||||
|   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 RssFeedManager = require('./managers/RssFeedManager') | ||||
| const CronManager = require('./managers/CronManager') | ||||
| const TaskManager = require('./managers/TaskManager') | ||||
| 
 | ||||
| class Server { | ||||
|   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) | ||||
| 
 | ||||
|     // Managers
 | ||||
|     this.taskManager = new TaskManager(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.logManager = new LogManager(this.db) | ||||
|     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.coverManager = new CoverManager(this.db, this.cacheManager) | ||||
|     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.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this)) | ||||
|     this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager) | ||||
| 
 | ||||
|     // 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.staticRouter = new StaticRouter(this.db) | ||||
| 
 | ||||
| @ -127,7 +129,6 @@ class Server { | ||||
| 
 | ||||
|   async init() { | ||||
|     Logger.info('[Server] Init v' + version) | ||||
|     await this.abMergeManager.removeOrphanDownloads() | ||||
|     await this.playbackSessionManager.removeOrphanStreams() | ||||
| 
 | ||||
|     var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
 | ||||
|  | ||||
| @ -110,55 +110,13 @@ class MiscController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   // GET: api/ab-manager-tasks/:id
 | ||||
|   async getAbManagerTask(req, res) { | ||||
|     if (!req.user.canDownload) { | ||||
|       Logger.error('User attempting to download without permission', req.user) | ||||
|       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) | ||||
|       } | ||||
|   // GET: api/tasks
 | ||||
|   getTasks(req, res) { | ||||
|     res.json({ | ||||
|       tasks: this.taskManager.tasks.map(t => t.toJSON()) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   // 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)
 | ||||
|   async updateServerSettings(req, res) { | ||||
|     if (!req.user.isAdminOrUp) { | ||||
|  | ||||
| @ -4,23 +4,22 @@ const fs = require('../libs/fsExtra') | ||||
| 
 | ||||
| const workerThreads = require('worker_threads') | ||||
| const Logger = require('../Logger') | ||||
| const AbManagerTask = require('../objects/AbManagerTask') | ||||
| const Task = require('../objects/Task') | ||||
| const filePerms = require('../utils/filePerms') | ||||
| const { getId } = require('../utils/index') | ||||
| const { writeConcatFile } = require('../utils/ffmpegHelpers') | ||||
| const toneHelpers = require('../utils/toneHelpers') | ||||
| const { getFileSize } = require('../utils/fileUtils') | ||||
| 
 | ||||
| class AbMergeManager { | ||||
|   constructor(db, clientEmitter) { | ||||
|   constructor(db, taskManager, clientEmitter) { | ||||
|     this.db = db | ||||
|     this.taskManager = taskManager | ||||
|     this.clientEmitter = clientEmitter | ||||
| 
 | ||||
|     this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items') | ||||
|     this.downloadDirPath = Path.join(global.MetadataPath, 'downloads') | ||||
|     this.downloadDirPathExist = false | ||||
| 
 | ||||
|     this.pendingTasks = [] | ||||
|     this.tasks = [] | ||||
|   } | ||||
| 
 | ||||
|   async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
 | ||||
| @ -39,85 +38,47 @@ class AbMergeManager { | ||||
|     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) { | ||||
|     var taskId = getId('abmerge') | ||||
|     var dlpath = Path.join(this.downloadDirPath, taskId) | ||||
|     Logger.info(`Start audiobook merge for ${libraryItem.id} - TaskId: ${taskId} - ${dlpath}`) | ||||
|     const task = new Task() | ||||
| 
 | ||||
|     var audiobookDirname = Path.basename(libraryItem.path) | ||||
|     var filename = audiobookDirname + '.m4b' | ||||
|     var taskData = { | ||||
|       id: taskId, | ||||
|     const audiobookDirname = Path.basename(libraryItem.path) | ||||
|     const targetFilename = audiobookDirname + '.m4b' | ||||
|     const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id) | ||||
|     const tempFilepath = Path.join(itemCachePath, targetFilename) | ||||
|     const taskData = { | ||||
|       libraryItemId: libraryItem.id, | ||||
|       type: 'abmerge', | ||||
|       dirpath: dlpath, | ||||
|       path: Path.join(dlpath, filename), | ||||
|       filename, | ||||
|       ext: '.m4b', | ||||
|       userId: user.id, | ||||
|       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() | ||||
|     abManagerTask.setData(taskData) | ||||
|     abManagerTask.setTimeoutTimer(this.downloadTimedOut.bind(this)) | ||||
|     const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.` | ||||
|     task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData) | ||||
|     this.taskManager.addTask(task) | ||||
|     Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`) | ||||
| 
 | ||||
|     try { | ||||
|       await fs.mkdir(abManagerTask.dirpath) | ||||
|     } 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 | ||||
|     if (!await fs.pathExists(taskData.itemCachePath)) { | ||||
|       await fs.mkdir(taskData.itemCachePath) | ||||
|     } | ||||
| 
 | ||||
|     this.clientEmitter(user.id, 'abmerge_started', abManagerTask.toJSON()) | ||||
|     this.runAudiobookMerge(libraryItem, abManagerTask) | ||||
|     this.runAudiobookMerge(libraryItem, task) | ||||
|   } | ||||
| 
 | ||||
|   async runAudiobookMerge(libraryItem, abManagerTask) { | ||||
|   async runAudiobookMerge(libraryItem, task) { | ||||
|     // If changing audio file type then encoding is needed
 | ||||
|     var audioTracks = libraryItem.media.tracks | ||||
|     var audioRequiresEncode = audioTracks[0].metadata.ext !== abManagerTask.ext | ||||
|     var shouldIncludeCover = libraryItem.media.coverPath | ||||
|     var audioRequiresEncode = audioTracks[0].metadata.ext !== '.m4b' | ||||
|     var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b' | ||||
|     var isOneTrack = audioTracks.length === 1 | ||||
| 
 | ||||
|     const ffmpegInputs = [] | ||||
| 
 | ||||
|     if (!isOneTrack) { | ||||
|       var concatFilePath = Path.join(abManagerTask.dirpath, 'files.txt') | ||||
|       console.log('Write files.txt', concatFilePath) | ||||
|       var concatFilePath = Path.join(task.data.itemCachePath, 'files.txt') | ||||
|       await writeConcatFile(audioTracks, concatFilePath) | ||||
|       ffmpegInputs.push({ | ||||
|         input: concatFilePath, | ||||
| @ -132,7 +93,7 @@ class AbMergeManager { | ||||
| 
 | ||||
|     const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning' | ||||
|     var ffmpegOptions = [`-loglevel ${logLevel}`] | ||||
|     var ffmpegOutputOptions = [] | ||||
|     var ffmpegOutputOptions = ['-f mp4'] | ||||
| 
 | ||||
|     if (audioRequiresEncode) { | ||||
|       ffmpegOptions = ffmpegOptions.concat([ | ||||
| @ -140,25 +101,20 @@ class AbMergeManager { | ||||
|         '-acodec aac', | ||||
|         '-ac 2', | ||||
|         '-b:a 64k' | ||||
|         // '-movflags use_metadata_tags'
 | ||||
|       ]) | ||||
|     } else { | ||||
|       ffmpegOptions.push('-max_muxing_queue_size 1000') | ||||
| 
 | ||||
|       if (isOneTrack && firstTrackIsM4b && !shouldIncludeCover) { | ||||
|       if (isOneTrack && firstTrackIsM4b) { | ||||
|         ffmpegOptions.push('-c copy') | ||||
|       } else { | ||||
|         ffmpegOptions.push('-c:a copy') | ||||
|       } | ||||
|     } | ||||
|     if (abManagerTask.ext === '.m4b') { | ||||
|       ffmpegOutputOptions.push('-f mp4') | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     var chaptersFilePath = null | ||||
|     if (libraryItem.media.chapters.length) { | ||||
|       chaptersFilePath = Path.join(abManagerTask.dirpath, 'chapters.txt') | ||||
|       chaptersFilePath = Path.join(task.data.itemCachePath, 'chapters.txt') | ||||
|       try { | ||||
|         await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath) | ||||
|       } catch (error) { | ||||
| @ -169,7 +125,7 @@ class AbMergeManager { | ||||
| 
 | ||||
|     const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath) | ||||
|     toneMetadataObject.TrackNumber = 1 | ||||
|     abManagerTask.toneMetadataObject = toneMetadataObject | ||||
|     task.data.toneMetadataObject = toneMetadataObject | ||||
| 
 | ||||
|     Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject) | ||||
| 
 | ||||
| @ -177,7 +133,7 @@ class AbMergeManager { | ||||
|       inputs: ffmpegInputs, | ||||
|       options: ffmpegOptions, | ||||
|       outputOptions: ffmpegOutputOptions, | ||||
|       output: abManagerTask.path, | ||||
|       output: task.data.tempFilepath | ||||
|     } | ||||
| 
 | ||||
|     var worker = null | ||||
| @ -186,137 +142,83 @@ class AbMergeManager { | ||||
|       worker = new workerThreads.Worker(workerPath, { workerData }) | ||||
|     } catch (error) { | ||||
|       Logger.error(`[AbMergeManager] Start worker thread failed`, error) | ||||
|       if (abManagerTask.userId) { | ||||
|         var taskJson = abManagerTask.toJSON() | ||||
|         this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson) | ||||
|       } | ||||
|       this.removeTask(abManagerTask) | ||||
|       task.setFailed('Failed to start worker thread') | ||||
|       this.removeTask(task, true) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     worker.on('message', (message) => { | ||||
|       if (message != null && typeof message === 'object') { | ||||
|         if (message.type === 'RESULT') { | ||||
|           if (!abManagerTask.isTimedOut) { | ||||
|             this.sendResult(abManagerTask, message) | ||||
|           } | ||||
|           this.sendResult(task, message) | ||||
|         } else if (message.type === 'FFMPEG') { | ||||
|           if (Logger[message.level]) { | ||||
|             Logger[message.level](message.log) | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         Logger.error('Invalid worker message', message) | ||||
|       } | ||||
|     }) | ||||
|     this.pendingTasks.push({ | ||||
|       id: abManagerTask.id, | ||||
|       abManagerTask, | ||||
|       id: task.id, | ||||
|       task, | ||||
|       worker | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async sendResult(abManagerTask, result) { | ||||
|     abManagerTask.clearTimeoutTimer() | ||||
| 
 | ||||
|   async sendResult(task, result) { | ||||
|     // 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 (abManagerTask.userId) { | ||||
|         this.clientEmitter(abManagerTask.userId, 'abmerge_killed', abManagerTask.toJSON()) | ||||
|       } | ||||
|       task.setFailed('Ffmpeg task killed') | ||||
|       this.removeTask(task, true) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     if (!result.success) { | ||||
|       if (abManagerTask.userId) { | ||||
|         this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON()) | ||||
|       } | ||||
|       this.removeTask(abManagerTask) | ||||
|       task.setFailed('Encoding failed') | ||||
|       this.removeTask(task, true) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // 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) { | ||||
|       Logger.error(`[AbMergeManager] Failed to write metadata to file "${abManagerTask.path}"`) | ||||
|       if (abManagerTask.userId) { | ||||
|         this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON()) | ||||
|       } | ||||
|       this.removeTask(abManagerTask) | ||||
|       Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`) | ||||
|       task.setFailed('Failed to write metadata to m4b file') | ||||
|       this.removeTask(task, true) | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|     // Move library item tracks to cache
 | ||||
|     const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${abManagerTask.libraryItemId}`) | ||||
|     await fs.ensureDir(itemCacheDir) | ||||
|     for (const trackPath of abManagerTask.originalTrackPaths) { | ||||
|     for (const trackPath of task.data.originalTrackPaths) { | ||||
|       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}`) | ||||
|       await fs.move(trackPath, moveToPath, { overwrite: true }).catch((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
 | ||||
|     await filePerms.setDefault(abManagerTask.path) | ||||
|     await filePerms.setDefault(itemCacheDir) | ||||
|     await filePerms.setDefault(task.data.targetFilepath) | ||||
|     await filePerms.setDefault(task.data.itemCachePath) | ||||
| 
 | ||||
|     // Move merged file to library item
 | ||||
|     const moveToPath = Path.join(abManagerTask.libraryItemPath, abManagerTask.filename) | ||||
|     Logger.debug(`[AbMergeManager] Moving merged audiobook to library item at "${moveToPath}"`) | ||||
|     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?
 | ||||
|     task.setFinished() | ||||
|     await this.removeTask(task, false) | ||||
|     Logger.info(`[AbMergeManager] Ab task finished ${task.id}`) | ||||
|   } | ||||
| 
 | ||||
|     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) {
 | ||||
|   //   Logger.info(`[AbMergeManager] Download ${abManagerTask.id} expired`)
 | ||||
| 
 | ||||
|   //   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) | ||||
|   async removeTask(task, removeTempFilepath = false) { | ||||
|     Logger.info('[AbMergeManager] Removing task ' + task.id) | ||||
| 
 | ||||
|     const pendingDl = this.pendingTasks.find(d => d.id === task.id) | ||||
|     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`) | ||||
|       if (pendingDl.worker) { | ||||
|         try { | ||||
| @ -327,12 +229,17 @@ class AbMergeManager { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     await fs.remove(abManagerTask.dirpath).then(() => { | ||||
|       Logger.info('[AbMergeManager] Deleted download', abManagerTask.dirpath) | ||||
|     if (removeTempFilepath) { // On failed tasks remove the bad file if it exists
 | ||||
|       if (await fs.pathExists(task.data.tempFilepath)) { | ||||
|         await fs.remove(task.data.tempFilepath).then(() => { | ||||
|           Logger.info('[AbMergeManager] Deleted target file', task.data.tempFilepath) | ||||
|         }).catch((err) => { | ||||
|       Logger.error('[AbMergeManager] Failed to delete download', err) | ||||
|           Logger.error('[AbMergeManager] Failed to delete target file', err) | ||||
|         }) | ||||
|     this.tasks = this.tasks.filter(d => d.id !== abManagerTask.id) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.taskManager.taskFinished(task) | ||||
|   } | ||||
| } | ||||
| module.exports = AbMergeManager | ||||
|  | ||||
| @ -8,8 +8,9 @@ const { writeMetadataFile } = require('../utils/ffmpegHelpers') | ||||
| const toneHelpers = require('../utils/toneHelpers') | ||||
| 
 | ||||
| class AudioMetadataMangaer { | ||||
|   constructor(db, emitter, clientEmitter) { | ||||
|   constructor(db, taskManager, emitter, clientEmitter) { | ||||
|     this.db = db | ||||
|     this.taskManager = taskManager | ||||
|     this.emitter = emitter | ||||
|     this.clientEmitter = clientEmitter | ||||
|   } | ||||
|  | ||||
| @ -25,22 +25,6 @@ class DownloadManager { | ||||
|     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) { | ||||
|     var client = socket.sheepClient | ||||
|     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') | ||||
| 
 | ||||
| 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.auth = auth | ||||
|     this.scanner = scanner | ||||
| @ -41,6 +41,7 @@ class ApiRouter { | ||||
|     this.rssFeedManager = rssFeedManager | ||||
|     this.cronManager = cronManager | ||||
|     this.notificationManager = notificationManager | ||||
|     this.taskManager = taskManager | ||||
|     this.emitter = emitter | ||||
|     this.clientEmitter = clientEmitter | ||||
| 
 | ||||
| @ -224,9 +225,7 @@ class ApiRouter { | ||||
|     //
 | ||||
|     this.router.post('/upload', MiscController.handleUpload.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.delete('/ab-manager-tasks/:id', MiscController.removeAbManagerTask.bind(this)) | ||||
|     this.router.get('/ab-manager-tasks', MiscController.getAbManagerTasks.bind(this)) | ||||
|     this.router.get('/tasks', MiscController.getTasks.bind(this)) | ||||
|     this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
 | ||||
|     this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
 | ||||
|     this.router.post('/authorize', MiscController.authorize.bind(this)) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user