aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/transcoding/shared
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/transcoding/shared')
-rw-r--r--server/lib/transcoding/shared/ffmpeg-builder.ts18
-rw-r--r--server/lib/transcoding/shared/index.ts2
-rw-r--r--server/lib/transcoding/shared/job-builders/abstract-job-builder.ts38
-rw-r--r--server/lib/transcoding/shared/job-builders/index.ts2
-rw-r--r--server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts308
-rw-r--r--server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts189
6 files changed, 557 insertions, 0 deletions
diff --git a/server/lib/transcoding/shared/ffmpeg-builder.ts b/server/lib/transcoding/shared/ffmpeg-builder.ts
new file mode 100644
index 000000000..441445ec4
--- /dev/null
+++ b/server/lib/transcoding/shared/ffmpeg-builder.ts
@@ -0,0 +1,18 @@
1import { Job } from 'bullmq'
2import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
3import { logger } from '@server/helpers/logger'
4import { FFmpegVOD } from '@shared/ffmpeg'
5import { VideoTranscodingProfilesManager } from '../default-transcoding-profiles'
6
7export function buildFFmpegVOD (job?: Job) {
8 return new FFmpegVOD({
9 ...getFFmpegCommandWrapperOptions('vod', VideoTranscodingProfilesManager.Instance.getAvailableEncoders()),
10
11 updateJobProgress: progress => {
12 if (!job) return
13
14 job.updateProgress(progress)
15 .catch(err => logger.error('Cannot update ffmpeg job progress', { err }))
16 }
17 })
18}
diff --git a/server/lib/transcoding/shared/index.ts b/server/lib/transcoding/shared/index.ts
new file mode 100644
index 000000000..f0b45bcbb
--- /dev/null
+++ b/server/lib/transcoding/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './job-builders'
2export * from './ffmpeg-builder'
diff --git a/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts
new file mode 100644
index 000000000..f1e9efdcf
--- /dev/null
+++ b/server/lib/transcoding/shared/job-builders/abstract-job-builder.ts
@@ -0,0 +1,38 @@
1
2import { JOB_PRIORITY } from '@server/initializers/constants'
3import { VideoModel } from '@server/models/video/video'
4import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
5
6export abstract class AbstractJobBuilder {
7
8 abstract createOptimizeOrMergeAudioJobs (options: {
9 video: MVideoFullLight
10 videoFile: MVideoFile
11 isNewVideo: boolean
12 user: MUserId
13 }): Promise<any>
14
15 abstract createTranscodingJobs (options: {
16 transcodingType: 'hls' | 'webtorrent'
17 video: MVideoFullLight
18 resolutions: number[]
19 isNewVideo: boolean
20 user: MUserId | null
21 }): Promise<any>
22
23 protected async getTranscodingJobPriority (options: {
24 user: MUserId
25 fallback: number
26 }) {
27 const { user, fallback } = options
28
29 if (!user) return fallback
30
31 const now = new Date()
32 const lastWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
33
34 const videoUploadedByUser = await VideoModel.countVideosUploadedByUserSince(user.id, lastWeek)
35
36 return JOB_PRIORITY.TRANSCODING + videoUploadedByUser
37 }
38}
diff --git a/server/lib/transcoding/shared/job-builders/index.ts b/server/lib/transcoding/shared/job-builders/index.ts
new file mode 100644
index 000000000..9b1c82adf
--- /dev/null
+++ b/server/lib/transcoding/shared/job-builders/index.ts
@@ -0,0 +1,2 @@
1export * from './transcoding-job-queue-builder'
2export * from './transcoding-runner-job-builder'
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts
new file mode 100644
index 000000000..7c892718b
--- /dev/null
+++ b/server/lib/transcoding/shared/job-builders/transcoding-job-queue-builder.ts
@@ -0,0 +1,308 @@
1import Bluebird from 'bluebird'
2import { computeOutputFPS } from '@server/helpers/ffmpeg'
3import { logger } from '@server/helpers/logger'
4import { CONFIG } from '@server/initializers/config'
5import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
6import { CreateJobArgument, JobQueue } from '@server/lib/job-queue'
7import { Hooks } from '@server/lib/plugins/hooks'
8import { VideoPathManager } from '@server/lib/video-path-manager'
9import { VideoJobInfoModel } from '@server/models/video/video-job-info'
10import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
11import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
12import {
13 HLSTranscodingPayload,
14 MergeAudioTranscodingPayload,
15 NewWebTorrentResolutionTranscodingPayload,
16 OptimizeTranscodingPayload,
17 VideoTranscodingPayload
18} from '@shared/models'
19import { canDoQuickTranscode } from '../../transcoding-quick-transcode'
20import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
21import { AbstractJobBuilder } from './abstract-job-builder'
22
23export class TranscodingJobQueueBuilder extends AbstractJobBuilder {
24
25 async createOptimizeOrMergeAudioJobs (options: {
26 video: MVideoFullLight
27 videoFile: MVideoFile
28 isNewVideo: boolean
29 user: MUserId
30 }) {
31 const { video, videoFile, isNewVideo, user } = options
32
33 let mergeOrOptimizePayload: MergeAudioTranscodingPayload | OptimizeTranscodingPayload
34 let nextTranscodingSequentialJobPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
35
36 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
37
38 try {
39 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
40 const probe = await ffprobePromise(videoFilePath)
41
42 const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
43 const hasAudio = await hasAudioStream(videoFilePath, probe)
44 const quickTranscode = await canDoQuickTranscode(videoFilePath, probe)
45 const inputFPS = videoFile.isAudio()
46 ? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
47 : await getVideoStreamFPS(videoFilePath, probe)
48
49 const maxResolution = await isAudioFile(videoFilePath, probe)
50 ? DEFAULT_AUDIO_RESOLUTION
51 : resolution
52
53 if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
54 nextTranscodingSequentialJobPayloads.push([
55 this.buildHLSJobPayload({
56 deleteWebTorrentFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
57
58 // We had some issues with a web video quick transcoded while producing a HLS version of it
59 copyCodecs: !quickTranscode,
60
61 resolution: maxResolution,
62 fps: computeOutputFPS({ inputFPS, resolution: maxResolution }),
63 videoUUID: video.uuid,
64 isNewVideo
65 })
66 ])
67 }
68
69 const lowerResolutionJobPayloads = await this.buildLowerResolutionJobPayloads({
70 video,
71 inputVideoResolution: maxResolution,
72 inputVideoFPS: inputFPS,
73 hasAudio,
74 isNewVideo
75 })
76
77 nextTranscodingSequentialJobPayloads = [ ...nextTranscodingSequentialJobPayloads, ...lowerResolutionJobPayloads ]
78
79 mergeOrOptimizePayload = videoFile.isAudio()
80 ? this.buildMergeAudioPayload({ videoUUID: video.uuid, isNewVideo })
81 : this.buildOptimizePayload({ videoUUID: video.uuid, isNewVideo, quickTranscode })
82 })
83 } finally {
84 mutexReleaser()
85 }
86
87 const nextTranscodingSequentialJobs = await Bluebird.mapSeries(nextTranscodingSequentialJobPayloads, payloads => {
88 return Bluebird.mapSeries(payloads, payload => {
89 return this.buildTranscodingJob({ payload, user })
90 })
91 })
92
93 const transcodingJobBuilderJob: CreateJobArgument = {
94 type: 'transcoding-job-builder',
95 payload: {
96 videoUUID: video.uuid,
97 sequentialJobs: nextTranscodingSequentialJobs
98 }
99 }
100
101 const mergeOrOptimizeJob = await this.buildTranscodingJob({ payload: mergeOrOptimizePayload, user })
102
103 return JobQueue.Instance.createSequentialJobFlow(...[ mergeOrOptimizeJob, transcodingJobBuilderJob ])
104 }
105
106 // ---------------------------------------------------------------------------
107
108 async createTranscodingJobs (options: {
109 transcodingType: 'hls' | 'webtorrent'
110 video: MVideoFullLight
111 resolutions: number[]
112 isNewVideo: boolean
113 user: MUserId | null
114 }) {
115 const { video, transcodingType, resolutions, isNewVideo } = options
116
117 const maxResolution = Math.max(...resolutions)
118 const childrenResolutions = resolutions.filter(r => r !== maxResolution)
119
120 logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
121
122 const { fps: inputFPS } = await video.probeMaxQualityFile()
123
124 const children = childrenResolutions.map(resolution => {
125 const fps = computeOutputFPS({ inputFPS, resolution })
126
127 if (transcodingType === 'hls') {
128 return this.buildHLSJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
129 }
130
131 if (transcodingType === 'webtorrent') {
132 return this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution, fps, isNewVideo })
133 }
134
135 throw new Error('Unknown transcoding type')
136 })
137
138 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
139
140 const parent = transcodingType === 'hls'
141 ? this.buildHLSJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
142 : this.buildWebTorrentJobPayload({ videoUUID: video.uuid, resolution: maxResolution, fps, isNewVideo })
143
144 // Process the last resolution after the other ones to prevent concurrency issue
145 // Because low resolutions use the biggest one as ffmpeg input
146 await this.createTranscodingJobsWithChildren({ videoUUID: video.uuid, parent, children, user: null })
147 }
148
149 // ---------------------------------------------------------------------------
150
151 private async createTranscodingJobsWithChildren (options: {
152 videoUUID: string
153 parent: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)
154 children: (HLSTranscodingPayload | NewWebTorrentResolutionTranscodingPayload)[]
155 user: MUserId | null
156 }) {
157 const { videoUUID, parent, children, user } = options
158
159 const parentJob = await this.buildTranscodingJob({ payload: parent, user })
160 const childrenJobs = await Bluebird.mapSeries(children, c => this.buildTranscodingJob({ payload: c, user }))
161
162 await JobQueue.Instance.createJobWithChildren(parentJob, childrenJobs)
163
164 await VideoJobInfoModel.increaseOrCreate(videoUUID, 'pendingTranscode', 1 + children.length)
165 }
166
167 private async buildTranscodingJob (options: {
168 payload: VideoTranscodingPayload
169 user: MUserId | null // null means we don't want priority
170 }) {
171 const { user, payload } = options
172
173 return {
174 type: 'video-transcoding' as 'video-transcoding',
175 priority: await this.getTranscodingJobPriority({ user, fallback: undefined }),
176 payload
177 }
178 }
179
180 private async buildLowerResolutionJobPayloads (options: {
181 video: MVideoWithFileThumbnail
182 inputVideoResolution: number
183 inputVideoFPS: number
184 hasAudio: boolean
185 isNewVideo: boolean
186 }) {
187 const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio } = options
188
189 // Create transcoding jobs if there are enabled resolutions
190 const resolutionsEnabled = await Hooks.wrapObject(
191 computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
192 'filter:transcoding.auto.resolutions-to-transcode.result',
193 options
194 )
195
196 const sequentialPayloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[][] = []
197
198 for (const resolution of resolutionsEnabled) {
199 const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
200
201 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
202 const payloads: (NewWebTorrentResolutionTranscodingPayload | HLSTranscodingPayload)[] = [
203 this.buildWebTorrentJobPayload({
204 videoUUID: video.uuid,
205 resolution,
206 fps,
207 isNewVideo
208 })
209 ]
210
211 // Create a subsequent job to create HLS resolution that will just copy web video codecs
212 if (CONFIG.TRANSCODING.HLS.ENABLED) {
213 payloads.push(
214 this.buildHLSJobPayload({
215 videoUUID: video.uuid,
216 resolution,
217 fps,
218 isNewVideo,
219 copyCodecs: true
220 })
221 )
222 }
223
224 sequentialPayloads.push(payloads)
225 } else if (CONFIG.TRANSCODING.HLS.ENABLED) {
226 sequentialPayloads.push([
227 this.buildHLSJobPayload({
228 videoUUID: video.uuid,
229 resolution,
230 fps,
231 copyCodecs: false,
232 isNewVideo
233 })
234 ])
235 }
236 }
237
238 return sequentialPayloads
239 }
240
241 private buildHLSJobPayload (options: {
242 videoUUID: string
243 resolution: number
244 fps: number
245 isNewVideo: boolean
246 deleteWebTorrentFiles?: boolean // default false
247 copyCodecs?: boolean // default false
248 }): HLSTranscodingPayload {
249 const { videoUUID, resolution, fps, isNewVideo, deleteWebTorrentFiles = false, copyCodecs = false } = options
250
251 return {
252 type: 'new-resolution-to-hls',
253 videoUUID,
254 resolution,
255 fps,
256 copyCodecs,
257 isNewVideo,
258 deleteWebTorrentFiles
259 }
260 }
261
262 private buildWebTorrentJobPayload (options: {
263 videoUUID: string
264 resolution: number
265 fps: number
266 isNewVideo: boolean
267 }): NewWebTorrentResolutionTranscodingPayload {
268 const { videoUUID, resolution, fps, isNewVideo } = options
269
270 return {
271 type: 'new-resolution-to-webtorrent',
272 videoUUID,
273 isNewVideo,
274 resolution,
275 fps
276 }
277 }
278
279 private buildMergeAudioPayload (options: {
280 videoUUID: string
281 isNewVideo: boolean
282 }): MergeAudioTranscodingPayload {
283 const { videoUUID, isNewVideo } = options
284
285 return {
286 type: 'merge-audio-to-webtorrent',
287 resolution: DEFAULT_AUDIO_RESOLUTION,
288 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
289 videoUUID,
290 isNewVideo
291 }
292 }
293
294 private buildOptimizePayload (options: {
295 videoUUID: string
296 quickTranscode: boolean
297 isNewVideo: boolean
298 }): OptimizeTranscodingPayload {
299 const { videoUUID, quickTranscode, isNewVideo } = options
300
301 return {
302 type: 'optimize-to-webtorrent',
303 videoUUID,
304 isNewVideo,
305 quickTranscode
306 }
307 }
308}
diff --git a/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts
new file mode 100644
index 000000000..c7a63d2e2
--- /dev/null
+++ b/server/lib/transcoding/shared/job-builders/transcoding-runner-job-builder.ts
@@ -0,0 +1,189 @@
1import { computeOutputFPS } from '@server/helpers/ffmpeg'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config'
4import { DEFAULT_AUDIO_RESOLUTION, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { VODAudioMergeTranscodingJobHandler, VODHLSTranscodingJobHandler, VODWebVideoTranscodingJobHandler } from '@server/lib/runners'
7import { VideoPathManager } from '@server/lib/video-path-manager'
8import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@server/types/models'
9import { MRunnerJob } from '@server/types/models/runners'
10import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@shared/ffmpeg'
11import { computeResolutionsToTranscode } from '../../transcoding-resolutions'
12import { AbstractJobBuilder } from './abstract-job-builder'
13
14/**
15 *
16 * Class to build transcoding job in the local job queue
17 *
18 */
19
20const lTags = loggerTagsFactory('transcoding')
21
22export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
23
24 async createOptimizeOrMergeAudioJobs (options: {
25 video: MVideoFullLight
26 videoFile: MVideoFile
27 isNewVideo: boolean
28 user: MUserId
29 }) {
30 const { video, videoFile, isNewVideo, user } = options
31
32 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
33
34 try {
35 await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), async videoFilePath => {
36 const probe = await ffprobePromise(videoFilePath)
37
38 const { resolution } = await getVideoStreamDimensionsInfo(videoFilePath, probe)
39 const hasAudio = await hasAudioStream(videoFilePath, probe)
40 const inputFPS = videoFile.isAudio()
41 ? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
42 : await getVideoStreamFPS(videoFilePath, probe)
43
44 const maxResolution = await isAudioFile(videoFilePath, probe)
45 ? DEFAULT_AUDIO_RESOLUTION
46 : resolution
47
48 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
49 const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
50
51 const mainRunnerJob = videoFile.isAudio()
52 ? await new VODAudioMergeTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
53 : await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
54
55 if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
56 await new VODHLSTranscodingJobHandler().create({
57 video,
58 deleteWebVideoFiles: CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false,
59 resolution: maxResolution,
60 fps,
61 isNewVideo,
62 dependsOnRunnerJob: mainRunnerJob,
63 priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
64 })
65 }
66
67 await this.buildLowerResolutionJobPayloads({
68 video,
69 inputVideoResolution: maxResolution,
70 inputVideoFPS: inputFPS,
71 hasAudio,
72 isNewVideo,
73 mainRunnerJob,
74 user
75 })
76 })
77 } finally {
78 mutexReleaser()
79 }
80 }
81
82 // ---------------------------------------------------------------------------
83
84 async createTranscodingJobs (options: {
85 transcodingType: 'hls' | 'webtorrent'
86 video: MVideoFullLight
87 resolutions: number[]
88 isNewVideo: boolean
89 user: MUserId | null
90 }) {
91 const { video, transcodingType, resolutions, isNewVideo, user } = options
92
93 const maxResolution = Math.max(...resolutions)
94 const { fps: inputFPS } = await video.probeMaxQualityFile()
95 const maxFPS = computeOutputFPS({ inputFPS, resolution: maxResolution })
96 const priority = await this.getTranscodingJobPriority({ user, fallback: 0 })
97
98 const childrenResolutions = resolutions.filter(r => r !== maxResolution)
99
100 logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
101
102 // Process the last resolution before the other ones to prevent concurrency issue
103 // Because low resolutions use the biggest one as ffmpeg input
104 const mainJob = transcodingType === 'hls'
105 // eslint-disable-next-line max-len
106 ? await new VODHLSTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, deleteWebVideoFiles: false, priority })
107 : await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, priority })
108
109 for (const resolution of childrenResolutions) {
110 const dependsOnRunnerJob = mainJob
111 const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
112
113 if (transcodingType === 'hls') {
114 await new VODHLSTranscodingJobHandler().create({
115 video,
116 resolution,
117 fps,
118 isNewVideo,
119 deleteWebVideoFiles: false,
120 dependsOnRunnerJob,
121 priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
122 })
123 continue
124 }
125
126 if (transcodingType === 'webtorrent') {
127 await new VODWebVideoTranscodingJobHandler().create({
128 video,
129 resolution,
130 fps,
131 isNewVideo,
132 dependsOnRunnerJob,
133 priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
134 })
135 continue
136 }
137
138 throw new Error('Unknown transcoding type')
139 }
140 }
141
142 private async buildLowerResolutionJobPayloads (options: {
143 mainRunnerJob: MRunnerJob
144 video: MVideoWithFileThumbnail
145 inputVideoResolution: number
146 inputVideoFPS: number
147 hasAudio: boolean
148 isNewVideo: boolean
149 user: MUserId
150 }) {
151 const { video, inputVideoResolution, inputVideoFPS, isNewVideo, hasAudio, mainRunnerJob, user } = options
152
153 // Create transcoding jobs if there are enabled resolutions
154 const resolutionsEnabled = await Hooks.wrapObject(
155 computeResolutionsToTranscode({ input: inputVideoResolution, type: 'vod', includeInput: false, strictLower: true, hasAudio }),
156 'filter:transcoding.auto.resolutions-to-transcode.result',
157 options
158 )
159
160 logger.debug('Lower resolutions build for %s.', video.uuid, { resolutionsEnabled, ...lTags(video.uuid) })
161
162 for (const resolution of resolutionsEnabled) {
163 const fps = computeOutputFPS({ inputFPS: inputVideoFPS, resolution })
164
165 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED) {
166 await new VODWebVideoTranscodingJobHandler().create({
167 video,
168 resolution,
169 fps,
170 isNewVideo,
171 dependsOnRunnerJob: mainRunnerJob,
172 priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
173 })
174 }
175
176 if (CONFIG.TRANSCODING.HLS.ENABLED) {
177 await new VODHLSTranscodingJobHandler().create({
178 video,
179 resolution,
180 fps,
181 isNewVideo,
182 deleteWebVideoFiles: false,
183 dependsOnRunnerJob: mainRunnerJob,
184 priority: await this.getTranscodingJobPriority({ user, fallback: 0 })
185 })
186 }
187 }
188 }
189}