aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--config/default.yaml17
-rw-r--r--config/production.yaml.example5
-rw-r--r--config/test-1.yaml1
-rw-r--r--config/test-2.yaml1
-rw-r--r--config/test-3.yaml1
-rw-r--r--config/test-4.yaml1
-rw-r--r--config/test-5.yaml1
-rw-r--r--config/test-6.yaml1
-rw-r--r--config/test.yaml3
-rw-r--r--package.json4
-rw-r--r--server/controllers/api/videos/import.ts10
-rw-r--r--server/helpers/requests.ts9
-rw-r--r--server/helpers/youtube-dl.ts394
-rw-r--r--server/helpers/youtube-dl/index.ts3
-rw-r--r--server/helpers/youtube-dl/youtube-dl-cli.ts198
-rw-r--r--server/helpers/youtube-dl/youtube-dl-info-builder.ts154
-rw-r--r--server/helpers/youtube-dl/youtube-dl-wrapper.ts135
-rw-r--r--server/initializers/config.ts13
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/lib/job-queue/handlers/video-import.ts6
-rw-r--r--server/lib/schedulers/youtube-dl-update-scheduler.ts4
-rw-r--r--server/tests/api/server/proxy.ts107
-rw-r--r--server/tests/api/videos/video-imports.ts632
-rw-r--r--server/tests/fixtures/video_import_preview_yt_dlp.jpgbin0 -> 15844 bytes
-rw-r--r--server/tests/fixtures/video_import_thumbnail_yt_dlp.jpgbin0 -> 10163 bytes
-rw-r--r--server/tools/peertube-import-videos.ts179
-rw-r--r--shared/extra-utils/miscs/tests.ts2
-rw-r--r--shared/extra-utils/videos/captions.ts8
-rw-r--r--support/docker/production/config/production.yaml1
-rw-r--r--yarn.lock397
30 files changed, 1053 insertions, 1241 deletions
diff --git a/config/default.yaml b/config/default.yaml
index c46d0e883..ec9622477 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -85,6 +85,7 @@ client:
85# From the project root directory 85# From the project root directory
86storage: 86storage:
87 tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... 87 tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
88 bin: 'storage/bin/'
88 avatars: 'storage/avatars/' 89 avatars: 'storage/avatars/'
89 videos: 'storage/videos/' 90 videos: 'storage/videos/'
90 streaming_playlists: 'storage/streaming-playlists/' 91 streaming_playlists: 'storage/streaming-playlists/'
@@ -394,13 +395,21 @@ import:
394 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html 395 http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
395 enabled: false 396 enabled: false
396 397
398 youtube_dl_release:
399 # Direct download URL to youtube-dl binary
400 # Github releases API is also supported
401 # Examples:
402 # * https://api.github.com/repos/ytdl-org/youtube-dl/releases
403 # * https://api.github.com/repos/yt-dlp/yt-dlp/releases
404 url: 'https://yt-dl.org/downloads/latest/youtube-dl'
405
406 # youtube-dl binary name
407 # yt-dlp is also supported
408 name: 'youtube-dl'
409
397 # IPv6 is very strongly rate-limited on most sites supported by youtube-dl 410 # IPv6 is very strongly rate-limited on most sites supported by youtube-dl
398 force_ipv4: false 411 force_ipv4: false
399 412
400 # You can use an HTTP/HTTPS/SOCKS proxy with youtube-dl
401 proxy:
402 enabled: false
403 url: ''
404 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) 413 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
405 enabled: false 414 enabled: false
406 415
diff --git a/config/production.yaml.example b/config/production.yaml.example
index d023070e3..588d6a3a5 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -83,6 +83,7 @@ client:
83# From the project root directory 83# From the project root directory
84storage: 84storage:
85 tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... 85 tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
86 bin: '/var/www/peertube/storage/bin/'
86 avatars: '/var/www/peertube/storage/avatars/' 87 avatars: '/var/www/peertube/storage/avatars/'
87 videos: '/var/www/peertube/storage/videos/' 88 videos: '/var/www/peertube/storage/videos/'
88 streaming_playlists: '/var/www/peertube/storage/streaming-playlists/' 89 streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
@@ -407,10 +408,6 @@ import:
407 # IPv6 is very strongly rate-limited on most sites supported by youtube-dl 408 # IPv6 is very strongly rate-limited on most sites supported by youtube-dl
408 force_ipv4: false 409 force_ipv4: false
409 410
410 # You can use an HTTP/HTTPS/SOCKS proxy with youtube-dl
411 proxy:
412 enabled: false
413 url: ''
414 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) 411 torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
415 enabled: false 412 enabled: false
416 413
diff --git a/config/test-1.yaml b/config/test-1.yaml
index fe5b3cf44..d5f8299e0 100644
--- a/config/test-1.yaml
+++ b/config/test-1.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test1/tmp/' 13 tmp: 'test1/tmp/'
14 bin: 'test1/bin/'
14 avatars: 'test1/avatars/' 15 avatars: 'test1/avatars/'
15 videos: 'test1/videos/' 16 videos: 'test1/videos/'
16 streaming_playlists: 'test1/streaming-playlists/' 17 streaming_playlists: 'test1/streaming-playlists/'
diff --git a/config/test-2.yaml b/config/test-2.yaml
index b559769c3..9da79da16 100644
--- a/config/test-2.yaml
+++ b/config/test-2.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test2/tmp/' 13 tmp: 'test2/tmp/'
14 bin: 'test2/bin/'
14 avatars: 'test2/avatars/' 15 avatars: 'test2/avatars/'
15 videos: 'test2/videos/' 16 videos: 'test2/videos/'
16 streaming_playlists: 'test2/streaming-playlists/' 17 streaming_playlists: 'test2/streaming-playlists/'
diff --git a/config/test-3.yaml b/config/test-3.yaml
index 9a7a944e9..594439b62 100644
--- a/config/test-3.yaml
+++ b/config/test-3.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test3/tmp/' 13 tmp: 'test3/tmp/'
14 bin: 'test3/bin/'
14 avatars: 'test3/avatars/' 15 avatars: 'test3/avatars/'
15 videos: 'test3/videos/' 16 videos: 'test3/videos/'
16 streaming_playlists: 'test3/streaming-playlists/' 17 streaming_playlists: 'test3/streaming-playlists/'
diff --git a/config/test-4.yaml b/config/test-4.yaml
index 1e4bee974..1e6368bf7 100644
--- a/config/test-4.yaml
+++ b/config/test-4.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test4/tmp/' 13 tmp: 'test4/tmp/'
14 bin: 'test4/bin/'
14 avatars: 'test4/avatars/' 15 avatars: 'test4/avatars/'
15 videos: 'test4/videos/' 16 videos: 'test4/videos/'
16 streaming_playlists: 'test4/streaming-playlists/' 17 streaming_playlists: 'test4/streaming-playlists/'
diff --git a/config/test-5.yaml b/config/test-5.yaml
index 9725e84f4..97f18a7a0 100644
--- a/config/test-5.yaml
+++ b/config/test-5.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test5/tmp/' 13 tmp: 'test5/tmp/'
14 bin: 'test5/bin/'
14 avatars: 'test5/avatars/' 15 avatars: 'test5/avatars/'
15 videos: 'test5/videos/' 16 videos: 'test5/videos/'
16 streaming_playlists: 'test5/streaming-playlists/' 17 streaming_playlists: 'test5/streaming-playlists/'
diff --git a/config/test-6.yaml b/config/test-6.yaml
index a04c8a6a9..156da84d2 100644
--- a/config/test-6.yaml
+++ b/config/test-6.yaml
@@ -11,6 +11,7 @@ database:
11# From the project root directory 11# From the project root directory
12storage: 12storage:
13 tmp: 'test6/tmp/' 13 tmp: 'test6/tmp/'
14 bin: 'test6/bin/'
14 avatars: 'test6/avatars/' 15 avatars: 'test6/avatars/'
15 videos: 'test6/videos/' 16 videos: 'test6/videos/'
16 streaming_playlists: 'test6/streaming-playlists/' 17 streaming_playlists: 'test6/streaming-playlists/'
diff --git a/config/test.yaml b/config/test.yaml
index 9a522a983..3eb2f04d8 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -118,9 +118,6 @@ import:
118 concurrency: 2 118 concurrency: 2
119 http: 119 http:
120 enabled: true 120 enabled: true
121 proxy:
122 enabled: false
123 url: ""
124 torrent: 121 torrent:
125 enabled: true 122 enabled: true
126 123
diff --git a/package.json b/package.json
index e1ddf1168..0737df7d5 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
91 "decache": "^4.6.0", 91 "decache": "^4.6.0",
92 "deep-object-diff": "^1.1.0", 92 "deep-object-diff": "^1.1.0",
93 "email-templates": "^8.0.3", 93 "email-templates": "^8.0.3",
94 "execa": "^5.1.1",
94 "express": "^4.12.4", 95 "express": "^4.12.4",
95 "express-rate-limit": "^5.0.0", 96 "express-rate-limit": "^5.0.0",
96 "express-validator": "^6.4.0", 97 "express-validator": "^6.4.0",
@@ -144,8 +145,7 @@
144 "webfinger.js": "^2.6.6", 145 "webfinger.js": "^2.6.6",
145 "webtorrent": "^1.0.0", 146 "webtorrent": "^1.0.0",
146 "winston": "3.3.3", 147 "winston": "3.3.3",
147 "ws": "^8.0.0", 148 "ws": "^8.0.0"
148 "youtube-dl": "^3.0.2"
149 }, 149 },
150 "devDependencies": { 150 "devDependencies": {
151 "@types/async": "^3.0.0", 151 "@types/async": "^3.0.0",
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 4265f3217..eddb9b32d 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -26,7 +26,7 @@ import { isArray } from '../../../helpers/custom-validators/misc'
26import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' 26import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
27import { logger } from '../../../helpers/logger' 27import { logger } from '../../../helpers/logger'
28import { getSecureTorrentName } from '../../../helpers/utils' 28import { getSecureTorrentName } from '../../../helpers/utils'
29import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl' 29import { YoutubeDLWrapper, YoutubeDLInfo } from '../../../helpers/youtube-dl'
30import { CONFIG } from '../../../initializers/config' 30import { CONFIG } from '../../../initializers/config'
31import { MIMETYPES } from '../../../initializers/constants' 31import { MIMETYPES } from '../../../initializers/constants'
32import { sequelizeTypescript } from '../../../initializers/database' 32import { sequelizeTypescript } from '../../../initializers/database'
@@ -134,12 +134,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
134 const targetUrl = body.targetUrl 134 const targetUrl = body.targetUrl
135 const user = res.locals.oauth.token.User 135 const user = res.locals.oauth.token.User
136 136
137 const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) 137 const youtubeDL = new YoutubeDLWrapper(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
138 138
139 // Get video infos 139 // Get video infos
140 let youtubeDLInfo: YoutubeDLInfo 140 let youtubeDLInfo: YoutubeDLInfo
141 try { 141 try {
142 youtubeDLInfo = await youtubeDL.getYoutubeDLInfo() 142 youtubeDLInfo = await youtubeDL.getInfoForDownload()
143 } catch (err) { 143 } catch (err) {
144 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) 144 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
145 145
@@ -373,9 +373,9 @@ function extractNameFromArray (name: string | string[]) {
373 return isArray(name) ? name[0] : name 373 return isArray(name) ? name[0] : name
374} 374}
375 375
376async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) { 376async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl: string, videoId: number) {
377 try { 377 try {
378 const subtitles = await youtubeDL.getYoutubeDLSubs() 378 const subtitles = await youtubeDL.getSubtitles()
379 379
380 logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) 380 logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
381 381
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 991270952..d93f55776 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,9 +1,9 @@
1import { createWriteStream, remove } from 'fs-extra' 1import { createWriteStream, remove } from 'fs-extra'
2import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' 2import got, { CancelableRequest, Options as GotOptions, RequestError, Response } from 'got'
3import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' 3import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'
4import { join } from 'path' 4import { join } from 'path'
5import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
6import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' 6import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants'
7import { pipelinePromise } from './core-utils' 7import { pipelinePromise } from './core-utils'
8import { processImage } from './image-utils' 8import { processImage } from './image-utils'
9import { logger } from './logger' 9import { logger } from './logger'
@@ -180,12 +180,17 @@ function getUserAgent () {
180 return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` 180 return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})`
181} 181}
182 182
183function isBinaryResponse (result: Response<any>) {
184 return BINARY_CONTENT_TYPES.has(result.headers['content-type'])
185}
186
183// --------------------------------------------------------------------------- 187// ---------------------------------------------------------------------------
184 188
185export { 189export {
186 doRequest, 190 doRequest,
187 doJSONRequest, 191 doJSONRequest,
188 doRequestAndSaveToFile, 192 doRequestAndSaveToFile,
193 isBinaryResponse,
189 downloadImage, 194 downloadImage,
190 peertubeGot 195 peertubeGot
191} 196}
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
deleted file mode 100644
index 0392ec4c7..000000000
--- a/server/helpers/youtube-dl.ts
+++ /dev/null
@@ -1,394 +0,0 @@
1import { createWriteStream } from 'fs'
2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
3import { join } from 'path'
4import { CONFIG } from '@server/initializers/config'
5import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
6import { VideoResolution } from '../../shared/models/videos'
7import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
8import { peertubeTruncate, pipelinePromise, root } from './core-utils'
9import { isVideoFileExtnameValid } from './custom-validators/videos'
10import { logger } from './logger'
11import { peertubeGot } from './requests'
12import { generateVideoImportTmpPath } from './utils'
13
14export type YoutubeDLInfo = {
15 name?: string
16 description?: string
17 category?: number
18 language?: string
19 licence?: number
20 nsfw?: boolean
21 tags?: string[]
22 thumbnailUrl?: string
23 ext?: string
24 originallyPublishedAt?: Date
25}
26
27export type YoutubeDLSubs = {
28 language: string
29 filename: string
30 path: string
31}[]
32
33const processOptions = {
34 maxBuffer: 1024 * 1024 * 10 // 10MB
35}
36
37class YoutubeDL {
38
39 constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) {
40
41 }
42
43 getYoutubeDLInfo (opts?: string[]): Promise<YoutubeDLInfo> {
44 return new Promise<YoutubeDLInfo>((res, rej) => {
45 let args = opts || []
46
47 if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
48 args.push('--force-ipv4')
49 }
50
51 args = this.wrapWithProxyOptions(args)
52 args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args)
53
54 YoutubeDL.safeGetYoutubeDL()
55 .then(youtubeDL => {
56 youtubeDL.getInfo(this.url, args, processOptions, (err, info) => {
57 if (err) return rej(err)
58 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
59
60 const obj = this.buildVideoInfo(this.normalizeObject(info))
61 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
62
63 return res(obj)
64 })
65 })
66 .catch(err => rej(err))
67 })
68 }
69
70 getYoutubeDLSubs (opts?: object): Promise<YoutubeDLSubs> {
71 return new Promise<YoutubeDLSubs>((res, rej) => {
72 const cwd = CONFIG.STORAGE.TMP_DIR
73 const options = opts || { all: true, format: 'vtt', cwd }
74
75 YoutubeDL.safeGetYoutubeDL()
76 .then(youtubeDL => {
77 youtubeDL.getSubs(this.url, options, (err, files) => {
78 if (err) return rej(err)
79 if (!files) return []
80
81 logger.debug('Get subtitles from youtube dl.', { url: this.url, files })
82
83 const subtitles = files.reduce((acc, filename) => {
84 const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
85 if (!matched || !matched[1]) return acc
86
87 return [
88 ...acc,
89 {
90 language: matched[1],
91 path: join(cwd, filename),
92 filename
93 }
94 ]
95 }, [])
96
97 return res(subtitles)
98 })
99 })
100 .catch(err => rej(err))
101 })
102 }
103
104 getYoutubeDLVideoFormat () {
105 /**
106 * list of format selectors in order or preference
107 * see https://github.com/ytdl-org/youtube-dl#format-selection
108 *
109 * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
110 * of being able to do a "quick-transcode"
111 * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
112 * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
113 *
114 * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
115 **/
116 const resolution = this.enabledResolutions.length === 0
117 ? VideoResolution.H_720P
118 : Math.max(...this.enabledResolutions)
119
120 return [
121 `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
122 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
123 `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
124 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
125 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
126 'best' // Ultimate fallback
127 ].join('/')
128 }
129
130 downloadYoutubeDLVideo (fileExt: string, timeout: number) {
131 // Leave empty the extension, youtube-dl will add it
132 const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
133
134 let timer
135
136 logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension)
137
138 let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ]
139 options = this.wrapWithProxyOptions(options)
140
141 if (process.env.FFMPEG_PATH) {
142 options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
143 }
144
145 logger.debug('YoutubeDL options for %s.', this.url, { options })
146
147 return new Promise<string>((res, rej) => {
148 YoutubeDL.safeGetYoutubeDL()
149 .then(youtubeDL => {
150 youtubeDL.exec(this.url, options, processOptions, async err => {
151 clearTimeout(timer)
152
153 try {
154 // If youtube-dl did not guess an extension for our file, just use .mp4 as default
155 if (await pathExists(pathWithoutExtension)) {
156 await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
157 }
158
159 const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
160
161 if (err) {
162 remove(path)
163 .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
164
165 return rej(err)
166 }
167
168 return res(path)
169 } catch (err) {
170 return rej(err)
171 }
172 })
173
174 timer = setTimeout(() => {
175 const err = new Error('YoutubeDL download timeout.')
176
177 this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
178 .then(path => remove(path))
179 .finally(() => rej(err))
180 .catch(err => {
181 logger.error('Cannot remove file in youtubeDL timeout.', { err })
182 return rej(err)
183 })
184 }, timeout)
185 })
186 .catch(err => rej(err))
187 })
188 }
189
190 buildOriginallyPublishedAt (obj: any) {
191 let originallyPublishedAt: Date = null
192
193 const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
194 if (uploadDateMatcher) {
195 originallyPublishedAt = new Date()
196 originallyPublishedAt.setHours(0, 0, 0, 0)
197
198 const year = parseInt(uploadDateMatcher[1], 10)
199 // Month starts from 0
200 const month = parseInt(uploadDateMatcher[2], 10) - 1
201 const day = parseInt(uploadDateMatcher[3], 10)
202
203 originallyPublishedAt.setFullYear(year, month, day)
204 }
205
206 return originallyPublishedAt
207 }
208
209 private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
210 if (!isVideoFileExtnameValid(sourceExt)) {
211 throw new Error('Invalid video extension ' + sourceExt)
212 }
213
214 const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
215
216 for (const extension of extensions) {
217 const path = tmpPath + extension
218
219 if (await pathExists(path)) return path
220 }
221
222 throw new Error('Cannot guess path of ' + tmpPath)
223 }
224
225 private normalizeObject (obj: any) {
226 const newObj: any = {}
227
228 for (const key of Object.keys(obj)) {
229 // Deprecated key
230 if (key === 'resolution') continue
231
232 const value = obj[key]
233
234 if (typeof value === 'string') {
235 newObj[key] = value.normalize()
236 } else {
237 newObj[key] = value
238 }
239 }
240
241 return newObj
242 }
243
244 private buildVideoInfo (obj: any): YoutubeDLInfo {
245 return {
246 name: this.titleTruncation(obj.title),
247 description: this.descriptionTruncation(obj.description),
248 category: this.getCategory(obj.categories),
249 licence: this.getLicence(obj.license),
250 language: this.getLanguage(obj.language),
251 nsfw: this.isNSFW(obj),
252 tags: this.getTags(obj.tags),
253 thumbnailUrl: obj.thumbnail || undefined,
254 originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
255 ext: obj.ext
256 }
257 }
258
259 private titleTruncation (title: string) {
260 return peertubeTruncate(title, {
261 length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
262 separator: /,? +/,
263 omission: ' […]'
264 })
265 }
266
267 private descriptionTruncation (description: string) {
268 if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
269
270 return peertubeTruncate(description, {
271 length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
272 separator: /,? +/,
273 omission: ' […]'
274 })
275 }
276
277 private isNSFW (info: any) {
278 return info.age_limit && info.age_limit >= 16
279 }
280
281 private getTags (tags: any) {
282 if (Array.isArray(tags) === false) return []
283
284 return tags
285 .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
286 .map(t => t.normalize())
287 .slice(0, 5)
288 }
289
290 private getLicence (licence: string) {
291 if (!licence) return undefined
292
293 if (licence.includes('Creative Commons Attribution')) return 1
294
295 for (const key of Object.keys(VIDEO_LICENCES)) {
296 const peertubeLicence = VIDEO_LICENCES[key]
297 if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
298 }
299
300 return undefined
301 }
302
303 private getCategory (categories: string[]) {
304 if (!categories) return undefined
305
306 const categoryString = categories[0]
307 if (!categoryString || typeof categoryString !== 'string') return undefined
308
309 if (categoryString === 'News & Politics') return 11
310
311 for (const key of Object.keys(VIDEO_CATEGORIES)) {
312 const category = VIDEO_CATEGORIES[key]
313 if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
314 }
315
316 return undefined
317 }
318
319 private getLanguage (language: string) {
320 return VIDEO_LANGUAGES[language] ? language : undefined
321 }
322
323 private wrapWithProxyOptions (options: string[]) {
324 if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
325 logger.debug('Using proxy for YoutubeDL')
326
327 return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options)
328 }
329
330 return options
331 }
332
333 // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
334 // We rewrote it to avoid sync calls
335 static async updateYoutubeDLBinary () {
336 logger.info('Updating youtubeDL binary.')
337
338 const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
339 const bin = join(binDirectory, 'youtube-dl')
340 const detailsPath = join(binDirectory, 'details')
341 const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl'
342
343 await ensureDir(binDirectory)
344
345 try {
346 const gotContext = { bodyKBLimit: 20_000 }
347
348 const result = await peertubeGot(url, { followRedirect: false, context: gotContext })
349
350 if (result.statusCode !== HttpStatusCode.FOUND_302) {
351 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
352 return
353 }
354
355 const newUrl = result.headers.location
356 const newVersion = /\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl$/.exec(newUrl)[1]
357
358 const downloadFileStream = peertubeGot.stream(newUrl, { context: gotContext })
359 const writeStream = createWriteStream(bin, { mode: 493 })
360
361 await pipelinePromise(
362 downloadFileStream,
363 writeStream
364 )
365
366 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
367 await writeFile(detailsPath, details, { encoding: 'utf8' })
368
369 logger.info('youtube-dl updated to version %s.', newVersion)
370 } catch (err) {
371 logger.error('Cannot update youtube-dl.', { err })
372 }
373 }
374
375 static async safeGetYoutubeDL () {
376 let youtubeDL
377
378 try {
379 youtubeDL = require('youtube-dl')
380 } catch (e) {
381 // Download binary
382 await this.updateYoutubeDLBinary()
383 youtubeDL = require('youtube-dl')
384 }
385
386 return youtubeDL
387 }
388}
389
390// ---------------------------------------------------------------------------
391
392export {
393 YoutubeDL
394}
diff --git a/server/helpers/youtube-dl/index.ts b/server/helpers/youtube-dl/index.ts
new file mode 100644
index 000000000..6afc77dcf
--- /dev/null
+++ b/server/helpers/youtube-dl/index.ts
@@ -0,0 +1,3 @@
1export * from './youtube-dl-cli'
2export * from './youtube-dl-info-builder'
3export * from './youtube-dl-wrapper'
diff --git a/server/helpers/youtube-dl/youtube-dl-cli.ts b/server/helpers/youtube-dl/youtube-dl-cli.ts
new file mode 100644
index 000000000..440869205
--- /dev/null
+++ b/server/helpers/youtube-dl/youtube-dl-cli.ts
@@ -0,0 +1,198 @@
1import execa from 'execa'
2import { pathExists, writeFile } from 'fs-extra'
3import { join } from 'path'
4import { CONFIG } from '@server/initializers/config'
5import { VideoResolution } from '@shared/models'
6import { logger, loggerTagsFactory } from '../logger'
7import { getProxy, isProxyEnabled } from '../proxy'
8import { isBinaryResponse, peertubeGot } from '../requests'
9
10const lTags = loggerTagsFactory('youtube-dl')
11
12const youtubeDLBinaryPath = join(CONFIG.STORAGE.BIN_DIR, CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.NAME)
13
14export class YoutubeDLCLI {
15
16 static async safeGet () {
17 if (!await pathExists(youtubeDLBinaryPath)) {
18 await this.updateYoutubeDLBinary()
19 }
20
21 return new YoutubeDLCLI()
22 }
23
24 static async updateYoutubeDLBinary () {
25 const url = CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.URL
26
27 logger.info('Updating youtubeDL binary from %s.', url, lTags())
28
29 const gotOptions = { context: { bodyKBLimit: 20_000 }, responseType: 'buffer' as 'buffer' }
30
31 try {
32 let gotResult = await peertubeGot(url, gotOptions)
33
34 if (!isBinaryResponse(gotResult)) {
35 const json = JSON.parse(gotResult.body.toString())
36 const latest = json.filter(release => release.prerelease === false)[0]
37 if (!latest) throw new Error('Cannot find latest release')
38
39 const releaseName = CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.NAME
40 const releaseAsset = latest.assets.find(a => a.name === releaseName)
41 if (!releaseAsset) throw new Error(`Cannot find appropriate release with name ${releaseName} in release assets`)
42
43 gotResult = await peertubeGot(releaseAsset.browser_download_url, gotOptions)
44 }
45
46 if (!isBinaryResponse(gotResult)) {
47 throw new Error('Not a binary response')
48 }
49
50 await writeFile(youtubeDLBinaryPath, gotResult.body)
51
52 logger.info('youtube-dl updated %s.', youtubeDLBinaryPath, lTags())
53 } catch (err) {
54 logger.error('Cannot update youtube-dl from %s.', url, { err, ...lTags() })
55 }
56 }
57
58 static getYoutubeDLVideoFormat (enabledResolutions: VideoResolution[]) {
59 /**
60 * list of format selectors in order or preference
61 * see https://github.com/ytdl-org/youtube-dl#format-selection
62 *
63 * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
64 * of being able to do a "quick-transcode"
65 * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
66 * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
67 *
68 * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
69 **/
70 const resolution = enabledResolutions.length === 0
71 ? VideoResolution.H_720P
72 : Math.max(...enabledResolutions)
73
74 return [
75 `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
76 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
77 `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
78 `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
79 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
80 'best' // Ultimate fallback
81 ].join('/')
82 }
83
84 private constructor () {
85
86 }
87
88 download (options: {
89 url: string
90 format: string
91 output: string
92 processOptions: execa.NodeOptions
93 additionalYoutubeDLArgs?: string[]
94 }) {
95 return this.run({
96 url: options.url,
97 processOptions: options.processOptions,
98 args: (options.additionalYoutubeDLArgs || []).concat([ '-f', options.format, '-o', options.output ])
99 })
100 }
101
102 async getInfo (options: {
103 url: string
104 format: string
105 processOptions: execa.NodeOptions
106 additionalYoutubeDLArgs?: string[]
107 }) {
108 const { url, format, additionalYoutubeDLArgs = [], processOptions } = options
109
110 const completeArgs = additionalYoutubeDLArgs.concat([ '--dump-json', '-f', format ])
111
112 const data = await this.run({ url, args: completeArgs, processOptions })
113 const info = data.map(this.parseInfo)
114
115 return info.length === 1
116 ? info[0]
117 : info
118 }
119
120 async getSubs (options: {
121 url: string
122 format: 'vtt'
123 processOptions: execa.NodeOptions
124 }) {
125 const { url, format, processOptions } = options
126
127 const args = [ '--skip-download', '--all-subs', `--sub-format=${format}` ]
128
129 const data = await this.run({ url, args, processOptions })
130 const files: string[] = []
131
132 const skipString = '[info] Writing video subtitles to: '
133
134 for (let i = 0, len = data.length; i < len; i++) {
135 const line = data[i]
136
137 if (line.indexOf(skipString) === 0) {
138 files.push(line.slice(skipString.length))
139 }
140 }
141
142 return files
143 }
144
145 private async run (options: {
146 url: string
147 args: string[]
148 processOptions: execa.NodeOptions
149 }) {
150 const { url, args, processOptions } = options
151
152 let completeArgs = this.wrapWithProxyOptions(args)
153 completeArgs = this.wrapWithIPOptions(completeArgs)
154 completeArgs = this.wrapWithFFmpegOptions(completeArgs)
155
156 const output = await execa('python', [ youtubeDLBinaryPath, ...completeArgs, url ], processOptions)
157
158 logger.debug('Runned youtube-dl command.', { command: output.command, stdout: output.stdout, ...lTags() })
159
160 return output.stdout
161 ? output.stdout.trim().split(/\r?\n/)
162 : undefined
163 }
164
165 private wrapWithProxyOptions (args: string[]) {
166 if (isProxyEnabled()) {
167 logger.debug('Using proxy %s for YoutubeDL', getProxy(), lTags())
168
169 return [ '--proxy', getProxy() ].concat(args)
170 }
171
172 return args
173 }
174
175 private wrapWithIPOptions (args: string[]) {
176 if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
177 logger.debug('Force ipv4 for YoutubeDL')
178
179 return [ '--force-ipv4' ].concat(args)
180 }
181
182 return args
183 }
184
185 private wrapWithFFmpegOptions (args: string[]) {
186 if (process.env.FFMPEG_PATH) {
187 logger.debug('Using ffmpeg location %s for YoutubeDL', process.env.FFMPEG_PATH, lTags())
188
189 return [ '--ffmpeg-location', process.env.FFMPEG_PATH ].concat(args)
190 }
191
192 return args
193 }
194
195 private parseInfo (data: string) {
196 return JSON.parse(data)
197 }
198}
diff --git a/server/helpers/youtube-dl/youtube-dl-info-builder.ts b/server/helpers/youtube-dl/youtube-dl-info-builder.ts
new file mode 100644
index 000000000..9746a7067
--- /dev/null
+++ b/server/helpers/youtube-dl/youtube-dl-info-builder.ts
@@ -0,0 +1,154 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
2import { peertubeTruncate } from '../core-utils'
3
4type YoutubeDLInfo = {
5 name?: string
6 description?: string
7 category?: number
8 language?: string
9 licence?: number
10 nsfw?: boolean
11 tags?: string[]
12 thumbnailUrl?: string
13 ext?: string
14 originallyPublishedAt?: Date
15}
16
17class YoutubeDLInfoBuilder {
18 private readonly info: any
19
20 constructor (info: any) {
21 this.info = { ...info }
22 }
23
24 getInfo () {
25 const obj = this.buildVideoInfo(this.normalizeObject(this.info))
26 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
27
28 return obj
29 }
30
31 private normalizeObject (obj: any) {
32 const newObj: any = {}
33
34 for (const key of Object.keys(obj)) {
35 // Deprecated key
36 if (key === 'resolution') continue
37
38 const value = obj[key]
39
40 if (typeof value === 'string') {
41 newObj[key] = value.normalize()
42 } else {
43 newObj[key] = value
44 }
45 }
46
47 return newObj
48 }
49
50 private buildOriginallyPublishedAt (obj: any) {
51 let originallyPublishedAt: Date = null
52
53 const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
54 if (uploadDateMatcher) {
55 originallyPublishedAt = new Date()
56 originallyPublishedAt.setHours(0, 0, 0, 0)
57
58 const year = parseInt(uploadDateMatcher[1], 10)
59 // Month starts from 0
60 const month = parseInt(uploadDateMatcher[2], 10) - 1
61 const day = parseInt(uploadDateMatcher[3], 10)
62
63 originallyPublishedAt.setFullYear(year, month, day)
64 }
65
66 return originallyPublishedAt
67 }
68
69 private buildVideoInfo (obj: any): YoutubeDLInfo {
70 return {
71 name: this.titleTruncation(obj.title),
72 description: this.descriptionTruncation(obj.description),
73 category: this.getCategory(obj.categories),
74 licence: this.getLicence(obj.license),
75 language: this.getLanguage(obj.language),
76 nsfw: this.isNSFW(obj),
77 tags: this.getTags(obj.tags),
78 thumbnailUrl: obj.thumbnail || undefined,
79 originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
80 ext: obj.ext
81 }
82 }
83
84 private titleTruncation (title: string) {
85 return peertubeTruncate(title, {
86 length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
87 separator: /,? +/,
88 omission: ' […]'
89 })
90 }
91
92 private descriptionTruncation (description: string) {
93 if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
94
95 return peertubeTruncate(description, {
96 length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
97 separator: /,? +/,
98 omission: ' […]'
99 })
100 }
101
102 private isNSFW (info: any) {
103 return info?.age_limit >= 16
104 }
105
106 private getTags (tags: string[]) {
107 if (Array.isArray(tags) === false) return []
108
109 return tags
110 .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
111 .map(t => t.normalize())
112 .slice(0, 5)
113 }
114
115 private getLicence (licence: string) {
116 if (!licence) return undefined
117
118 if (licence.includes('Creative Commons Attribution')) return 1
119
120 for (const key of Object.keys(VIDEO_LICENCES)) {
121 const peertubeLicence = VIDEO_LICENCES[key]
122 if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
123 }
124
125 return undefined
126 }
127
128 private getCategory (categories: string[]) {
129 if (!categories) return undefined
130
131 const categoryString = categories[0]
132 if (!categoryString || typeof categoryString !== 'string') return undefined
133
134 if (categoryString === 'News & Politics') return 11
135
136 for (const key of Object.keys(VIDEO_CATEGORIES)) {
137 const category = VIDEO_CATEGORIES[key]
138 if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
139 }
140
141 return undefined
142 }
143
144 private getLanguage (language: string) {
145 return VIDEO_LANGUAGES[language] ? language : undefined
146 }
147}
148
149// ---------------------------------------------------------------------------
150
151export {
152 YoutubeDLInfo,
153 YoutubeDLInfoBuilder
154}
diff --git a/server/helpers/youtube-dl/youtube-dl-wrapper.ts b/server/helpers/youtube-dl/youtube-dl-wrapper.ts
new file mode 100644
index 000000000..6960fbae4
--- /dev/null
+++ b/server/helpers/youtube-dl/youtube-dl-wrapper.ts
@@ -0,0 +1,135 @@
1import { move, pathExists, readdir, remove } from 'fs-extra'
2import { dirname, join } from 'path'
3import { CONFIG } from '@server/initializers/config'
4import { isVideoFileExtnameValid } from '../custom-validators/videos'
5import { logger, loggerTagsFactory } from '../logger'
6import { generateVideoImportTmpPath } from '../utils'
7import { YoutubeDLCLI } from './youtube-dl-cli'
8import { YoutubeDLInfo, YoutubeDLInfoBuilder } from './youtube-dl-info-builder'
9
10const lTags = loggerTagsFactory('youtube-dl')
11
12export type YoutubeDLSubs = {
13 language: string
14 filename: string
15 path: string
16}[]
17
18const processOptions = {
19 maxBuffer: 1024 * 1024 * 10 // 10MB
20}
21
22class YoutubeDLWrapper {
23
24 constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) {
25
26 }
27
28 async getInfoForDownload (youtubeDLArgs: string[] = []): Promise<YoutubeDLInfo> {
29 const youtubeDL = await YoutubeDLCLI.safeGet()
30
31 const info = await youtubeDL.getInfo({
32 url: this.url,
33 format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions),
34 additionalYoutubeDLArgs: youtubeDLArgs,
35 processOptions
36 })
37
38 if (info.is_live === true) throw new Error('Cannot download a live streaming.')
39
40 const infoBuilder = new YoutubeDLInfoBuilder(info)
41
42 return infoBuilder.getInfo()
43 }
44
45 async getSubtitles (): Promise<YoutubeDLSubs> {
46 const cwd = CONFIG.STORAGE.TMP_DIR
47
48 const youtubeDL = await YoutubeDLCLI.safeGet()
49
50 const files = await youtubeDL.getSubs({ url: this.url, format: 'vtt', processOptions: { cwd } })
51 if (!files) return []
52
53 logger.debug('Get subtitles from youtube dl.', { url: this.url, files, ...lTags() })
54
55 const subtitles = files.reduce((acc, filename) => {
56 const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
57 if (!matched || !matched[1]) return acc
58
59 return [
60 ...acc,
61 {
62 language: matched[1],
63 path: join(cwd, filename),
64 filename
65 }
66 ]
67 }, [])
68
69 return subtitles
70 }
71
72 async downloadVideo (fileExt: string, timeout: number): Promise<string> {
73 // Leave empty the extension, youtube-dl will add it
74 const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
75
76 let timer: NodeJS.Timeout
77
78 logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension, lTags())
79
80 const youtubeDL = await YoutubeDLCLI.safeGet()
81
82 const timeoutPromise = new Promise<string>((_, rej) => {
83 timer = setTimeout(() => rej(new Error('YoutubeDL download timeout.')), timeout)
84 })
85
86 const downloadPromise = youtubeDL.download({
87 url: this.url,
88 format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions),
89 output: pathWithoutExtension,
90 processOptions
91 }).then(() => clearTimeout(timer))
92 .then(async () => {
93 // If youtube-dl did not guess an extension for our file, just use .mp4 as default
94 if (await pathExists(pathWithoutExtension)) {
95 await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
96 }
97
98 return this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
99 })
100
101 return Promise.race([ downloadPromise, timeoutPromise ])
102 .catch(async err => {
103 const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
104
105 remove(path)
106 .catch(err => logger.error('Cannot remove file in youtubeDL timeout.', { err, ...lTags() }))
107
108 throw err
109 })
110 }
111
112 private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
113 if (!isVideoFileExtnameValid(sourceExt)) {
114 throw new Error('Invalid video extension ' + sourceExt)
115 }
116
117 const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
118
119 for (const extension of extensions) {
120 const path = tmpPath + extension
121
122 if (await pathExists(path)) return path
123 }
124
125 const directoryContent = await readdir(dirname(tmpPath))
126
127 throw new Error(`Cannot guess path of ${tmpPath}. Directory content: ${directoryContent.join(', ')}`)
128 }
129}
130
131// ---------------------------------------------------------------------------
132
133export {
134 YoutubeDLWrapper
135}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 3a7c72a1c..e20efe02c 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -69,6 +69,7 @@ const CONFIG = {
69 69
70 STORAGE: { 70 STORAGE: {
71 TMP_DIR: buildPath(config.get<string>('storage.tmp')), 71 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
72 BIN_DIR: buildPath(config.get<string>('storage.bin')),
72 ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), 73 ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')),
73 LOG_DIR: buildPath(config.get<string>('storage.logs')), 74 LOG_DIR: buildPath(config.get<string>('storage.logs')),
74 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), 75 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
@@ -292,11 +293,13 @@ const CONFIG = {
292 293
293 HTTP: { 294 HTTP: {
294 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, 295 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') },
295 get FORCE_IPV4 () { return config.get<boolean>('import.videos.http.force_ipv4') }, 296
296 PROXY: { 297 YOUTUBE_DL_RELEASE: {
297 get ENABLED () { return config.get<boolean>('import.videos.http.proxy.enabled') }, 298 get URL () { return config.get<string>('import.videos.http.youtube_dl_release.url') },
298 get URL () { return config.get<string>('import.videos.http.proxy.url') } 299 get NAME () { return config.get<string>('import.videos.http.youtube_dl_release.name') }
299 } 300 },
301
302 get FORCE_IPV4 () { return config.get<boolean>('import.videos.http.force_ipv4') }
300 }, 303 },
301 TORRENT: { 304 TORRENT: {
302 get ENABLED () { return config.get<boolean>('import.videos.torrent.enabled') } 305 get ENABLED () { return config.get<boolean>('import.videos.torrent.enabled') }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index dcbad9264..1d434d5ab 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -497,6 +497,12 @@ const MIMETYPES = {
497MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT) 497MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
498MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT) 498MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
499 499
500const BINARY_CONTENT_TYPES = new Set([
501 'binary/octet-stream',
502 'application/octet-stream',
503 'application/x-binary'
504])
505
500// --------------------------------------------------------------------------- 506// ---------------------------------------------------------------------------
501 507
502const OVERVIEWS = { 508const OVERVIEWS = {
@@ -903,6 +909,7 @@ export {
903 MIMETYPES, 909 MIMETYPES,
904 CRAWL_REQUEST_CONCURRENCY, 910 CRAWL_REQUEST_CONCURRENCY,
905 DEFAULT_AUDIO_RESOLUTION, 911 DEFAULT_AUDIO_RESOLUTION,
912 BINARY_CONTENT_TYPES,
906 JOB_COMPLETED_LIFETIME, 913 JOB_COMPLETED_LIFETIME,
907 HTTP_SIGNATURE, 914 HTTP_SIGNATURE,
908 VIDEO_IMPORT_STATES, 915 VIDEO_IMPORT_STATES,
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 8313c2561..4ce1a6c30 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -2,7 +2,7 @@ import { Job } from 'bull'
2import { move, remove, stat } from 'fs-extra' 2import { move, remove, stat } from 'fs-extra'
3import { getLowercaseExtension } from '@server/helpers/core-utils' 3import { getLowercaseExtension } from '@server/helpers/core-utils'
4import { retryTransactionWrapper } from '@server/helpers/database-utils' 4import { retryTransactionWrapper } from '@server/helpers/database-utils'
5import { YoutubeDL } from '@server/helpers/youtube-dl' 5import { YoutubeDLWrapper } from '@server/helpers/youtube-dl'
6import { isPostImportVideoAccepted } from '@server/lib/moderation' 6import { isPostImportVideoAccepted } from '@server/lib/moderation'
7import { generateWebTorrentVideoFilename } from '@server/lib/paths' 7import { generateWebTorrentVideoFilename } from '@server/lib/paths'
8import { Hooks } from '@server/lib/plugins/hooks' 8import { Hooks } from '@server/lib/plugins/hooks'
@@ -77,10 +77,10 @@ async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPa
77 videoImportId: videoImport.id 77 videoImportId: videoImport.id
78 } 78 }
79 79
80 const youtubeDL = new YoutubeDL(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) 80 const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
81 81
82 return processFile( 82 return processFile(
83 () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), 83 () => youtubeDL.downloadVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT),
84 videoImport, 84 videoImport,
85 options 85 options
86 ) 86 )
diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts
index 898691c13..93d02f8a9 100644
--- a/server/lib/schedulers/youtube-dl-update-scheduler.ts
+++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts
@@ -1,4 +1,4 @@
1import { YoutubeDL } from '@server/helpers/youtube-dl' 1import { YoutubeDLCLI } from '@server/helpers/youtube-dl'
2import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' 2import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
3import { AbstractScheduler } from './abstract-scheduler' 3import { AbstractScheduler } from './abstract-scheduler'
4 4
@@ -13,7 +13,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler {
13 } 13 }
14 14
15 protected internalExecute () { 15 protected internalExecute () {
16 return YoutubeDL.updateYoutubeDLBinary() 16 return YoutubeDLCLI.updateYoutubeDLBinary()
17 } 17 }
18 18
19 static get Instance () { 19 static get Instance () {
diff --git a/server/tests/api/server/proxy.ts b/server/tests/api/server/proxy.ts
index 72bd49078..29f3e10d8 100644
--- a/server/tests/api/server/proxy.ts
+++ b/server/tests/api/server/proxy.ts
@@ -2,8 +2,18 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' 5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 FIXTURE_URLS,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@shared/extra-utils'
6import { MockProxy } from '@shared/extra-utils/mock-servers/mock-proxy' 15import { MockProxy } from '@shared/extra-utils/mock-servers/mock-proxy'
16import { HttpStatusCode, VideoPrivacy } from '@shared/models'
7 17
8const expect = chai.expect 18const expect = chai.expect
9 19
@@ -25,43 +35,90 @@ describe('Test proxy', function () {
25 goodEnv.HTTP_PROXY = 'http://localhost:' + proxyPort 35 goodEnv.HTTP_PROXY = 'http://localhost:' + proxyPort
26 36
27 await setAccessTokensToServers(servers) 37 await setAccessTokensToServers(servers)
38 await setDefaultVideoChannel(servers)
28 await doubleFollow(servers[0], servers[1]) 39 await doubleFollow(servers[0], servers[1])
29 }) 40 })
30 41
31 it('Should succeed federation with the appropriate proxy config', async function () { 42 describe('Federation', function () {
32 await servers[0].kill()
33 await servers[0].run({}, { env: goodEnv })
34 43
35 await servers[0].videos.quickUpload({ name: 'video 1' }) 44 it('Should succeed federation with the appropriate proxy config', async function () {
45 this.timeout(40000)
36 46
37 await waitJobs(servers) 47 await servers[0].kill()
48 await servers[0].run({}, { env: goodEnv })
38 49
39 for (const server of servers) { 50 await servers[0].videos.quickUpload({ name: 'video 1' })
40 const { total, data } = await server.videos.list() 51
41 expect(total).to.equal(1) 52 await waitJobs(servers)
42 expect(data).to.have.lengthOf(1) 53
43 } 54 for (const server of servers) {
55 const { total, data } = await server.videos.list()
56 expect(total).to.equal(1)
57 expect(data).to.have.lengthOf(1)
58 }
59 })
60
61 it('Should fail federation with a wrong proxy config', async function () {
62 this.timeout(40000)
63
64 await servers[0].kill()
65 await servers[0].run({}, { env: badEnv })
66
67 await servers[0].videos.quickUpload({ name: 'video 2' })
68
69 await waitJobs(servers)
70
71 {
72 const { total, data } = await servers[0].videos.list()
73 expect(total).to.equal(2)
74 expect(data).to.have.lengthOf(2)
75 }
76
77 {
78 const { total, data } = await servers[1].videos.list()
79 expect(total).to.equal(1)
80 expect(data).to.have.lengthOf(1)
81 }
82 })
44 }) 83 })
45 84
46 it('Should fail federation with a wrong proxy config', async function () { 85 describe('Videos import', async function () {
47 await servers[0].kill() 86
48 await servers[0].run({}, { env: badEnv }) 87 function quickImport (expectedStatus: HttpStatusCode = HttpStatusCode.OK_200) {
88 return servers[0].imports.importVideo({
89 attributes: {
90 name: 'video import',
91 channelId: servers[0].store.channel.id,
92 privacy: VideoPrivacy.PUBLIC,
93 targetUrl: FIXTURE_URLS.peertube_long
94 },
95 expectedStatus
96 })
97 }
98
99 it('Should succeed import with the appropriate proxy config', async function () {
100 this.timeout(40000)
101
102 await servers[0].kill()
103 await servers[0].run({}, { env: goodEnv })
49 104
50 await servers[0].videos.quickUpload({ name: 'video 2' }) 105 await quickImport()
51 106
52 await waitJobs(servers) 107 await waitJobs(servers)
53 108
54 {
55 const { total, data } = await servers[0].videos.list() 109 const { total, data } = await servers[0].videos.list()
56 expect(total).to.equal(2) 110 expect(total).to.equal(3)
57 expect(data).to.have.lengthOf(2) 111 expect(data).to.have.lengthOf(3)
58 } 112 })
59 113
60 { 114 it('Should fail import with a wrong proxy config', async function () {
61 const { total, data } = await servers[1].videos.list() 115 this.timeout(40000)
62 expect(total).to.equal(1) 116
63 expect(data).to.have.lengthOf(1) 117 await servers[0].kill()
64 } 118 await servers[0].run({}, { env: badEnv })
119
120 await quickImport(HttpStatusCode.BAD_REQUEST_400)
121 })
65 }) 122 })
66 123
67 after(async function () { 124 after(async function () {
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index 948c779e8..cfb188060 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -1,368 +1,444 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import { expect } from 'chai'
5import { pathExists, remove } from 'fs-extra'
6import { join } from 'path'
5import { 7import {
6 areHttpImportTestsDisabled, 8 areHttpImportTestsDisabled,
7 cleanupTests, 9 cleanupTests,
8 createMultipleServers, 10 createMultipleServers,
11 createSingleServer,
9 doubleFollow, 12 doubleFollow,
10 FIXTURE_URLS, 13 FIXTURE_URLS,
11 PeerTubeServer, 14 PeerTubeServer,
12 setAccessTokensToServers, 15 setAccessTokensToServers,
16 setDefaultVideoChannel,
13 testCaptionFile, 17 testCaptionFile,
14 testImage, 18 testImage,
15 waitJobs 19 waitJobs
16} from '@shared/extra-utils' 20} from '@shared/extra-utils'
17import { VideoPrivacy, VideoResolution } from '@shared/models' 21import { VideoPrivacy, VideoResolution } from '@shared/models'
18 22
19const expect = chai.expect 23async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) {
24 const videoHttp = await server.videos.get({ id: idHttp })
25
26 expect(videoHttp.name).to.equal('small video - youtube')
27 // FIXME: youtube-dl seems broken
28 // expect(videoHttp.category.label).to.equal('News & Politics')
29 // expect(videoHttp.licence.label).to.equal('Attribution')
30 expect(videoHttp.language.label).to.equal('Unknown')
31 expect(videoHttp.nsfw).to.be.false
32 expect(videoHttp.description).to.equal('this is a super description')
33 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ])
34 expect(videoHttp.files).to.have.lengthOf(1)
35
36 const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt)
37 expect(originallyPublishedAt.getDate()).to.equal(14)
38 expect(originallyPublishedAt.getMonth()).to.equal(0)
39 expect(originallyPublishedAt.getFullYear()).to.equal(2019)
40
41 const videoMagnet = await server.videos.get({ id: idMagnet })
42 const videoTorrent = await server.videos.get({ id: idTorrent })
43
44 for (const video of [ videoMagnet, videoTorrent ]) {
45 expect(video.category.label).to.equal('Misc')
46 expect(video.licence.label).to.equal('Unknown')
47 expect(video.language.label).to.equal('Unknown')
48 expect(video.nsfw).to.be.false
49 expect(video.description).to.equal('this is a super torrent description')
50 expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ])
51 expect(video.files).to.have.lengthOf(1)
52 }
53
54 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4')
55 expect(videoMagnet.name).to.contain('super peertube2 video')
56
57 const bodyCaptions = await server.captions.list({ videoId: idHttp })
58 expect(bodyCaptions.total).to.equal(2)
59}
60
61async function checkVideoServer2 (server: PeerTubeServer, id: number | string) {
62 const video = await server.videos.get({ id })
63
64 expect(video.name).to.equal('my super name')
65 expect(video.category.label).to.equal('Entertainment')
66 expect(video.licence.label).to.equal('Public Domain Dedication')
67 expect(video.language.label).to.equal('English')
68 expect(video.nsfw).to.be.false
69 expect(video.description).to.equal('my super description')
70 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ])
71
72 expect(video.files).to.have.lengthOf(1)
73
74 const bodyCaptions = await server.captions.list({ videoId: id })
75 expect(bodyCaptions.total).to.equal(2)
76}
20 77
21describe('Test video imports', function () { 78describe('Test video imports', function () {
22 let servers: PeerTubeServer[] = []
23 let channelIdServer1: number
24 let channelIdServer2: number
25 79
26 if (areHttpImportTestsDisabled()) return 80 if (areHttpImportTestsDisabled()) return
27 81
28 async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) { 82 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
29 const videoHttp = await server.videos.get({ id: idHttp })
30
31 expect(videoHttp.name).to.equal('small video - youtube')
32 // FIXME: youtube-dl seems broken
33 // expect(videoHttp.category.label).to.equal('News & Politics')
34 // expect(videoHttp.licence.label).to.equal('Attribution')
35 expect(videoHttp.language.label).to.equal('Unknown')
36 expect(videoHttp.nsfw).to.be.false
37 expect(videoHttp.description).to.equal('this is a super description')
38 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ])
39 expect(videoHttp.files).to.have.lengthOf(1)
40
41 const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt)
42 expect(originallyPublishedAt.getDate()).to.equal(14)
43 expect(originallyPublishedAt.getMonth()).to.equal(0)
44 expect(originallyPublishedAt.getFullYear()).to.equal(2019)
45
46 const videoMagnet = await server.videos.get({ id: idMagnet })
47 const videoTorrent = await server.videos.get({ id: idTorrent })
48
49 for (const video of [ videoMagnet, videoTorrent ]) {
50 expect(video.category.label).to.equal('Misc')
51 expect(video.licence.label).to.equal('Unknown')
52 expect(video.language.label).to.equal('Unknown')
53 expect(video.nsfw).to.be.false
54 expect(video.description).to.equal('this is a super torrent description')
55 expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ])
56 expect(video.files).to.have.lengthOf(1)
57 }
58 83
59 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4') 84 describe('Import ' + mode, function () {
60 expect(videoMagnet.name).to.contain('super peertube2 video') 85 let servers: PeerTubeServer[] = []
61 86
62 const bodyCaptions = await server.captions.list({ videoId: idHttp }) 87 before(async function () {
63 expect(bodyCaptions.total).to.equal(2) 88 this.timeout(30_000)
64 }
65 89
66 async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { 90 // Run servers
67 const video = await server.videos.get({ id }) 91 servers = await createMultipleServers(2, {
92 import: {
93 videos: {
94 http: {
95 youtube_dl_release: {
96 url: mode === 'youtube-dl'
97 ? 'https://yt-dl.org/downloads/latest/youtube-dl'
98 : 'https://api.github.com/repos/yt-dlp/yt-dlp/releases',
68 99
69 expect(video.name).to.equal('my super name') 100 name: mode
70 expect(video.category.label).to.equal('Entertainment') 101 }
71 expect(video.licence.label).to.equal('Public Domain Dedication') 102 }
72 expect(video.language.label).to.equal('English') 103 }
73 expect(video.nsfw).to.be.false 104 }
74 expect(video.description).to.equal('my super description') 105 })
75 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ])
76 106
77 expect(video.files).to.have.lengthOf(1) 107 await setAccessTokensToServers(servers)
108 await setDefaultVideoChannel(servers)
78 109
79 const bodyCaptions = await server.captions.list({ videoId: id }) 110 await doubleFollow(servers[0], servers[1])
80 expect(bodyCaptions.total).to.equal(2) 111 })
81 }
82 112
83 before(async function () { 113 it('Should import videos on server 1', async function () {
84 this.timeout(30_000) 114 this.timeout(60_000)
85 115
86 // Run servers 116 const baseAttributes = {
87 servers = await createMultipleServers(2) 117 channelId: servers[0].store.channel.id,
118 privacy: VideoPrivacy.PUBLIC
119 }
88 120
89 await setAccessTokensToServers(servers) 121 {
122 const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube }
123 const { video } = await servers[0].imports.importVideo({ attributes })
124 expect(video.name).to.equal('small video - youtube')
90 125
91 { 126 {
92 const { videoChannels } = await servers[0].users.getMyInfo() 127 expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`))
93 channelIdServer1 = videoChannels[0].id 128 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
94 }
95 129
96 { 130 const suffix = mode === 'yt-dlp'
97 const { videoChannels } = await servers[1].users.getMyInfo() 131 ? '_yt_dlp'
98 channelIdServer2 = videoChannels[0].id 132 : ''
99 }
100 133
101 await doubleFollow(servers[0], servers[1]) 134 await testImage(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath)
102 }) 135 await testImage(servers[0].url, 'video_import_preview' + suffix, video.previewPath)
136 }
103 137
104 it('Should import videos on server 1', async function () { 138 const bodyCaptions = await servers[0].captions.list({ videoId: video.id })
105 this.timeout(60_000) 139 const videoCaptions = bodyCaptions.data
140 expect(videoCaptions).to.have.lengthOf(2)
106 141
107 const baseAttributes = { 142 {
108 channelId: channelIdServer1, 143 const enCaption = videoCaptions.find(caption => caption.language.id === 'en')
109 privacy: VideoPrivacy.PUBLIC 144 expect(enCaption).to.exist
110 } 145 expect(enCaption.language.label).to.equal('English')
146 expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`))
111 147
112 { 148 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+Language: en[ \n]+00:00:01.600 --> 00:00:04.200[ \n]+English \\(US\\)[ \n]+` +
113 const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube } 149 `00:00:05.900 --> 00:00:07.999[ \n]+This is a subtitle in American English[ \n]+` +
114 const { video } = await servers[0].imports.importVideo({ attributes }) 150 `00:00:10.000 --> 00:00:14.000[ \n]+Adding subtitles is very easy to do`
115 expect(video.name).to.equal('small video - youtube') 151 await testCaptionFile(servers[0].url, enCaption.captionPath, new RegExp(regex))
152 }
116 153
117 expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) 154 {
118 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) 155 const frCaption = videoCaptions.find(caption => caption.language.id === 'fr')
156 expect(frCaption).to.exist
157 expect(frCaption.language.label).to.equal('French')
158 expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`))
119 159
120 await testImage(servers[0].url, 'video_import_thumbnail', video.thumbnailPath) 160 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+Language: fr[ \n]+00:00:01.600 --> 00:00:04.200[ \n]+` +
121 await testImage(servers[0].url, 'video_import_preview', video.previewPath) 161 `Français \\(FR\\)[ \n]+00:00:05.900 --> 00:00:07.999[ \n]+C'est un sous-titre français[ \n]+` +
162 `00:00:10.000 --> 00:00:14.000[ \n]+Ajouter un sous-titre est vraiment facile`
122 163
123 const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) 164 await testCaptionFile(servers[0].url, frCaption.captionPath, new RegExp(regex))
124 const videoCaptions = bodyCaptions.data 165 }
125 expect(videoCaptions).to.have.lengthOf(2) 166 }
126 167
127 const enCaption = videoCaptions.find(caption => caption.language.id === 'en') 168 {
128 expect(enCaption).to.exist 169 const attributes = {
129 expect(enCaption.language.label).to.equal('English') 170 ...baseAttributes,
130 expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`)) 171 magnetUri: FIXTURE_URLS.magnet,
131 await testCaptionFile(servers[0].url, enCaption.captionPath, `WEBVTT 172 description: 'this is a super torrent description',
132Kind: captions 173 tags: [ 'tag_torrent1', 'tag_torrent2' ]
133Language: en 174 }
175 const { video } = await servers[0].imports.importVideo({ attributes })
176 expect(video.name).to.equal('super peertube2 video')
177 }
134 178
13500:00:01.600 --> 00:00:04.200 179 {
136English (US) 180 const attributes = {
181 ...baseAttributes,
182 torrentfile: 'video-720p.torrent' as any,
183 description: 'this is a super torrent description',
184 tags: [ 'tag_torrent1', 'tag_torrent2' ]
185 }
186 const { video } = await servers[0].imports.importVideo({ attributes })
187 expect(video.name).to.equal('你好 世界 720p.mp4')
188 }
189 })
137 190
13800:00:05.900 --> 00:00:07.999 191 it('Should list the videos to import in my videos on server 1', async function () {
139This is a subtitle in American English 192 const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' })
140 193
14100:00:10.000 --> 00:00:14.000 194 expect(total).to.equal(3)
142Adding subtitles is very easy to do`)
143 195
144 const frCaption = videoCaptions.find(caption => caption.language.id === 'fr') 196 expect(data).to.have.lengthOf(3)
145 expect(frCaption).to.exist 197 expect(data[0].name).to.equal('small video - youtube')
146 expect(frCaption.language.label).to.equal('French') 198 expect(data[1].name).to.equal('super peertube2 video')
147 expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`)) 199 expect(data[2].name).to.equal('你好 世界 720p.mp4')
148 await testCaptionFile(servers[0].url, frCaption.captionPath, `WEBVTT 200 })
149Kind: captions
150Language: fr
151 201
15200:00:01.600 --> 00:00:04.200 202 it('Should list the videos to import in my imports on server 1', async function () {
153Français (FR) 203 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' })
204 expect(total).to.equal(3)
154 205
15500:00:05.900 --> 00:00:07.999 206 expect(videoImports).to.have.lengthOf(3)
156C'est un sous-titre français
157 207
15800:00:10.000 --> 00:00:14.000 208 expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube)
159Ajouter un sous-titre est vraiment facile`) 209 expect(videoImports[2].magnetUri).to.be.null
160 } 210 expect(videoImports[2].torrentName).to.be.null
211 expect(videoImports[2].video.name).to.equal('small video - youtube')
161 212
162 { 213 expect(videoImports[1].targetUrl).to.be.null
163 const attributes = { 214 expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet)
164 ...baseAttributes, 215 expect(videoImports[1].torrentName).to.be.null
165 magnetUri: FIXTURE_URLS.magnet, 216 expect(videoImports[1].video.name).to.equal('super peertube2 video')
166 description: 'this is a super torrent description',
167 tags: [ 'tag_torrent1', 'tag_torrent2' ]
168 }
169 const { video } = await servers[0].imports.importVideo({ attributes })
170 expect(video.name).to.equal('super peertube2 video')
171 }
172 217
173 { 218 expect(videoImports[0].targetUrl).to.be.null
174 const attributes = { 219 expect(videoImports[0].magnetUri).to.be.null
175 ...baseAttributes, 220 expect(videoImports[0].torrentName).to.equal('video-720p.torrent')
176 torrentfile: 'video-720p.torrent' as any, 221 expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4')
177 description: 'this is a super torrent description', 222 })
178 tags: [ 'tag_torrent1', 'tag_torrent2' ]
179 }
180 const { video } = await servers[0].imports.importVideo({ attributes })
181 expect(video.name).to.equal('你好 世界 720p.mp4')
182 }
183 })
184 223
185 it('Should list the videos to import in my videos on server 1', async function () { 224 it('Should have the video listed on the two instances', async function () {
186 const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' }) 225 this.timeout(120_000)
187 226
188 expect(total).to.equal(3) 227 await waitJobs(servers)
189 228
190 expect(data).to.have.lengthOf(3) 229 for (const server of servers) {
191 expect(data[0].name).to.equal('small video - youtube') 230 const { total, data } = await server.videos.list()
192 expect(data[1].name).to.equal('super peertube2 video') 231 expect(total).to.equal(3)
193 expect(data[2].name).to.equal('你好 世界 720p.mp4') 232 expect(data).to.have.lengthOf(3)
194 })
195 233
196 it('Should list the videos to import in my imports on server 1', async function () { 234 const [ videoHttp, videoMagnet, videoTorrent ] = data
197 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' }) 235 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
198 expect(total).to.equal(3) 236 }
237 })
238
239 it('Should import a video on server 2 with some fields', async function () {
240 this.timeout(60_000)
241
242 const attributes = {
243 targetUrl: FIXTURE_URLS.youtube,
244 channelId: servers[1].store.channel.id,
245 privacy: VideoPrivacy.PUBLIC,
246 category: 10,
247 licence: 7,
248 language: 'en',
249 name: 'my super name',
250 description: 'my super description',
251 tags: [ 'supertag1', 'supertag2' ]
252 }
253 const { video } = await servers[1].imports.importVideo({ attributes })
254 expect(video.name).to.equal('my super name')
255 })
199 256
200 expect(videoImports).to.have.lengthOf(3) 257 it('Should have the videos listed on the two instances', async function () {
258 this.timeout(120_000)
201 259
202 expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube) 260 await waitJobs(servers)
203 expect(videoImports[2].magnetUri).to.be.null
204 expect(videoImports[2].torrentName).to.be.null
205 expect(videoImports[2].video.name).to.equal('small video - youtube')
206 261
207 expect(videoImports[1].targetUrl).to.be.null 262 for (const server of servers) {
208 expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet) 263 const { total, data } = await server.videos.list()
209 expect(videoImports[1].torrentName).to.be.null 264 expect(total).to.equal(4)
210 expect(videoImports[1].video.name).to.equal('super peertube2 video') 265 expect(data).to.have.lengthOf(4)
211 266
212 expect(videoImports[0].targetUrl).to.be.null 267 await checkVideoServer2(server, data[0].uuid)
213 expect(videoImports[0].magnetUri).to.be.null
214 expect(videoImports[0].torrentName).to.equal('video-720p.torrent')
215 expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4')
216 })
217 268
218 it('Should have the video listed on the two instances', async function () { 269 const [ , videoHttp, videoMagnet, videoTorrent ] = data
219 this.timeout(120_000) 270 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
271 }
272 })
220 273
221 await waitJobs(servers) 274 it('Should import a video that will be transcoded', async function () {
275 this.timeout(240_000)
222 276
223 for (const server of servers) { 277 const attributes = {
224 const { total, data } = await server.videos.list() 278 name: 'transcoded video',
225 expect(total).to.equal(3) 279 magnetUri: FIXTURE_URLS.magnet,
226 expect(data).to.have.lengthOf(3) 280 channelId: servers[1].store.channel.id,
281 privacy: VideoPrivacy.PUBLIC
282 }
283 const { video } = await servers[1].imports.importVideo({ attributes })
284 const videoUUID = video.uuid
227 285
228 const [ videoHttp, videoMagnet, videoTorrent ] = data 286 await waitJobs(servers)
229 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
230 }
231 })
232 287
233 it('Should import a video on server 2 with some fields', async function () { 288 for (const server of servers) {
234 this.timeout(60_000) 289 const video = await server.videos.get({ id: videoUUID })
235
236 const attributes = {
237 targetUrl: FIXTURE_URLS.youtube,
238 channelId: channelIdServer2,
239 privacy: VideoPrivacy.PUBLIC,
240 category: 10,
241 licence: 7,
242 language: 'en',
243 name: 'my super name',
244 description: 'my super description',
245 tags: [ 'supertag1', 'supertag2' ]
246 }
247 const { video } = await servers[1].imports.importVideo({ attributes })
248 expect(video.name).to.equal('my super name')
249 })
250 290
251 it('Should have the videos listed on the two instances', async function () { 291 expect(video.name).to.equal('transcoded video')
252 this.timeout(120_000) 292 expect(video.files).to.have.lengthOf(4)
293 }
294 })
295
296 it('Should import no HDR version on a HDR video', async function () {
297 this.timeout(300_000)
298
299 const config = {
300 transcoding: {
301 enabled: true,
302 resolutions: {
303 '240p': true,
304 '360p': false,
305 '480p': false,
306 '720p': false,
307 '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01
308 '1440p': false,
309 '2160p': false
310 },
311 webtorrent: { enabled: true },
312 hls: { enabled: false }
313 },
314 import: {
315 videos: {
316 http: {
317 enabled: true
318 },
319 torrent: {
320 enabled: true
321 }
322 }
323 }
324 }
325 await servers[0].config.updateCustomSubConfig({ newConfig: config })
253 326
254 await waitJobs(servers) 327 const attributes = {
328 name: 'hdr video',
329 targetUrl: FIXTURE_URLS.youtubeHDR,
330 channelId: servers[0].store.channel.id,
331 privacy: VideoPrivacy.PUBLIC
332 }
333 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
334 const videoUUID = videoImported.uuid
335
336 await waitJobs(servers)
337
338 // test resolution
339 const video = await servers[0].videos.get({ id: videoUUID })
340 expect(video.name).to.equal('hdr video')
341 const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id }))
342 expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P)
343 })
344
345 it('Should import a peertube video', async function () {
346 this.timeout(120_000)
347
348 // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged
349 for (const targetUrl of [ FIXTURE_URLS.peertube_long ]) {
350 // for (const targetUrl of [ FIXTURE_URLS.peertube_long, FIXTURE_URLS.peertube_short ]) {
351 await servers[0].config.disableTranscoding()
352
353 const attributes = {
354 targetUrl,
355 channelId: servers[0].store.channel.id,
356 privacy: VideoPrivacy.PUBLIC
357 }
358 const { video } = await servers[0].imports.importVideo({ attributes })
359 const videoUUID = video.uuid
255 360
256 for (const server of servers) { 361 await waitJobs(servers)
257 const { total, data } = await server.videos.list()
258 expect(total).to.equal(4)
259 expect(data).to.have.lengthOf(4)
260 362
261 await checkVideoServer2(server, data[0].uuid) 363 for (const server of servers) {
364 const video = await server.videos.get({ id: videoUUID })
262 365
263 const [ , videoHttp, videoMagnet, videoTorrent ] = data 366 expect(video.name).to.equal('E2E tests')
264 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) 367 }
265 } 368 }
266 }) 369 })
267 370
268 it('Should import a video that will be transcoded', async function () { 371 after(async function () {
269 this.timeout(240_000) 372 await cleanupTests(servers)
373 })
374 })
375 }
270 376
271 const attributes = { 377 runSuite('youtube-dl')
272 name: 'transcoded video',
273 magnetUri: FIXTURE_URLS.magnet,
274 channelId: channelIdServer2,
275 privacy: VideoPrivacy.PUBLIC
276 }
277 const { video } = await servers[1].imports.importVideo({ attributes })
278 const videoUUID = video.uuid
279 378
280 await waitJobs(servers) 379 runSuite('yt-dlp')
281 380
282 for (const server of servers) { 381 describe('Auto update', function () {
283 const video = await server.videos.get({ id: videoUUID }) 382 let server: PeerTubeServer
284 383
285 expect(video.name).to.equal('transcoded video') 384 function quickPeerTubeImport () {
286 expect(video.files).to.have.lengthOf(4) 385 const attributes = {
386 targetUrl: FIXTURE_URLS.peertube_long,
387 channelId: server.store.channel.id,
388 privacy: VideoPrivacy.PUBLIC
389 }
390
391 return server.imports.importVideo({ attributes })
287 } 392 }
288 })
289 393
290 it('Should import no HDR version on a HDR video', async function () { 394 async function testBinaryUpdate (releaseUrl: string, releaseName: string) {
291 this.timeout(300_000) 395 await remove(join(server.servers.buildDirectory('bin'), releaseName))
292 396
293 const config = { 397 await server.kill()
294 transcoding: { 398 await server.run({
295 enabled: true, 399 import: {
296 resolutions: { 400 videos: {
297 '240p': true, 401 http: {
298 '360p': false, 402 youtube_dl_release: {
299 '480p': false, 403 url: releaseUrl,
300 '720p': false, 404 name: releaseName
301 '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01 405 }
302 '1440p': false, 406 }
303 '2160p': false
304 },
305 webtorrent: { enabled: true },
306 hls: { enabled: false }
307 },
308 import: {
309 videos: {
310 http: {
311 enabled: true
312 },
313 torrent: {
314 enabled: true
315 } 407 }
316 } 408 }
317 } 409 })
318 } 410
319 await servers[0].config.updateCustomSubConfig({ newConfig: config }) 411 await quickPeerTubeImport()
320 412
321 const attributes = { 413 expect(await pathExists(join(server.servers.buildDirectory('bin'), releaseName))).to.be.true
322 name: 'hdr video',
323 targetUrl: FIXTURE_URLS.youtubeHDR,
324 channelId: channelIdServer1,
325 privacy: VideoPrivacy.PUBLIC
326 } 414 }
327 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
328 const videoUUID = videoImported.uuid
329 415
330 await waitJobs(servers) 416 before(async function () {
417 this.timeout(30_000)
331 418
332 // test resolution 419 // Run servers
333 const video = await servers[0].videos.get({ id: videoUUID }) 420 server = await createSingleServer(1)
334 expect(video.name).to.equal('hdr video')
335 const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id }))
336 expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P)
337 })
338 421
339 it('Should import a peertube video', async function () { 422 await setAccessTokensToServers([ server ])
340 this.timeout(120_000) 423 await setDefaultVideoChannel([ server ])
424 })
341 425
342 // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged 426 it('Should update youtube-dl from github URL', async function () {
343 for (const targetUrl of [ FIXTURE_URLS.peertube_long ]) { 427 this.timeout(120_000)
344 // for (const targetUrl of [ FIXTURE_URLS.peertube_long, FIXTURE_URLS.peertube_short ]) {
345 await servers[0].config.disableTranscoding()
346 428
347 const attributes = { 429 await testBinaryUpdate('https://api.github.com/repos/ytdl-org/youtube-dl/releases', 'youtube-dl')
348 targetUrl, 430 })
349 channelId: channelIdServer1,
350 privacy: VideoPrivacy.PUBLIC
351 }
352 const { video } = await servers[0].imports.importVideo({ attributes })
353 const videoUUID = video.uuid
354 431
355 await waitJobs(servers) 432 it('Should update youtube-dl from raw URL', async function () {
433 this.timeout(120_000)
356 434
357 for (const server of servers) { 435 await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl')
358 const video = await server.videos.get({ id: videoUUID }) 436 })
359 437
360 expect(video.name).to.equal('E2E tests') 438 it('Should update youtube-dl from youtube-dl fork', async function () {
361 } 439 this.timeout(120_000)
362 }
363 })
364 440
365 after(async function () { 441 await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp')
366 await cleanupTests(servers) 442 })
367 }) 443 })
368}) 444})
diff --git a/server/tests/fixtures/video_import_preview_yt_dlp.jpg b/server/tests/fixtures/video_import_preview_yt_dlp.jpg
new file mode 100644
index 000000000..9e8833bf9
--- /dev/null
+++ b/server/tests/fixtures/video_import_preview_yt_dlp.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_import_thumbnail_yt_dlp.jpg b/server/tests/fixtures/video_import_thumbnail_yt_dlp.jpg
new file mode 100644
index 000000000..f672a785a
--- /dev/null
+++ b/server/tests/fixtures/video_import_thumbnail_yt_dlp.jpg
Binary files differ
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 758b561e1..54ac910e6 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -4,13 +4,9 @@ registerTSPaths()
4import { program } from 'commander' 4import { program } from 'commander'
5import { accessSync, constants } from 'fs' 5import { accessSync, constants } from 'fs'
6import { remove } from 'fs-extra' 6import { remove } from 'fs-extra'
7import { truncate } from 'lodash'
8import { join } from 'path' 7import { join } from 'path'
9import { promisify } from 'util'
10import { YoutubeDL } from '@server/helpers/youtube-dl'
11import { sha256 } from '../helpers/core-utils' 8import { sha256 } from '../helpers/core-utils'
12import { doRequestAndSaveToFile } from '../helpers/requests' 9import { doRequestAndSaveToFile } from '../helpers/requests'
13import { CONSTRAINTS_FIELDS } from '../initializers/constants'
14import { 10import {
15 assignToken, 11 assignToken,
16 buildCommonVideoOptions, 12 buildCommonVideoOptions,
@@ -19,8 +15,8 @@ import {
19 getLogger, 15 getLogger,
20 getServerCredentials 16 getServerCredentials
21} from './cli' 17} from './cli'
22import { PeerTubeServer } from '@shared/extra-utils' 18import { wait } from '@shared/extra-utils'
23 19import { YoutubeDLCLI, YoutubeDLInfo, YoutubeDLInfoBuilder } from '@server/helpers/youtube-dl'
24import prompt = require('prompt') 20import prompt = require('prompt')
25 21
26const processOptions = { 22const processOptions = {
@@ -73,7 +69,7 @@ getServerCredentials(command)
73async function run (url: string, username: string, password: string) { 69async function run (url: string, username: string, password: string) {
74 if (!password) password = await promptPassword() 70 if (!password) password = await promptPassword()
75 71
76 const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() 72 const youtubeDLBinary = await YoutubeDLCLI.safeGet()
77 73
78 let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args) 74 let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args)
79 75
@@ -96,8 +92,6 @@ async function run (url: string, username: string, password: string) {
96 } else if (options.last) { 92 } else if (options.last) {
97 infoArray = infoArray.slice(-options.last) 93 infoArray = infoArray.slice(-options.last)
98 } 94 }
99 // Normalize utf8 fields
100 infoArray = infoArray.map(i => normalizeObject(i))
101 95
102 log.info('Will download and upload %d videos.\n', infoArray.length) 96 log.info('Will download and upload %d videos.\n', infoArray.length)
103 97
@@ -105,8 +99,9 @@ async function run (url: string, username: string, password: string) {
105 try { 99 try {
106 if (index > 0 && options.waitInterval) { 100 if (index > 0 && options.waitInterval) {
107 log.info("Wait for %d seconds before continuing.", options.waitInterval / 1000) 101 log.info("Wait for %d seconds before continuing.", options.waitInterval / 1000)
108 await new Promise(res => setTimeout(res, options.waitInterval)) 102 await wait(options.waitInterval)
109 } 103 }
104
110 await processVideo({ 105 await processVideo({
111 cwd: options.tmpdir, 106 cwd: options.tmpdir,
112 url, 107 url,
@@ -131,29 +126,26 @@ async function processVideo (parameters: {
131 youtubeInfo: any 126 youtubeInfo: any
132}) { 127}) {
133 const { youtubeInfo, cwd, url, username, password } = parameters 128 const { youtubeInfo, cwd, url, username, password } = parameters
134 const youtubeDL = new YoutubeDL('', [])
135 129
136 log.debug('Fetching object.', youtubeInfo) 130 log.debug('Fetching object.', youtubeInfo)
137 131
138 const videoInfo = await fetchObject(youtubeInfo) 132 const videoInfo = await fetchObject(youtubeInfo)
139 log.debug('Fetched object.', videoInfo) 133 log.debug('Fetched object.', videoInfo)
140 134
141 const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) 135 if (options.since && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() < options.since.getTime()) {
142 136 log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.since))
143 if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) {
144 log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.since))
145 return 137 return
146 } 138 }
147 139
148 if (options.until && originallyPublishedAt && originallyPublishedAt.getTime() > options.until.getTime()) { 140 if (options.until && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() > options.until.getTime()) {
149 log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.until)) 141 log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.until))
150 return 142 return
151 } 143 }
152 144
153 const server = buildServer(url) 145 const server = buildServer(url)
154 const { data } = await server.search.advancedVideoSearch({ 146 const { data } = await server.search.advancedVideoSearch({
155 search: { 147 search: {
156 search: videoInfo.title, 148 search: videoInfo.name,
157 sort: '-match', 149 sort: '-match',
158 searchTarget: 'local' 150 searchTarget: 'local'
159 } 151 }
@@ -161,28 +153,32 @@ async function processVideo (parameters: {
161 153
162 log.info('############################################################\n') 154 log.info('############################################################\n')
163 155
164 if (data.find(v => v.name === videoInfo.title)) { 156 if (data.find(v => v.name === videoInfo.name)) {
165 log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title) 157 log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.name)
166 return 158 return
167 } 159 }
168 160
169 const path = join(cwd, sha256(videoInfo.url) + '.mp4') 161 const path = join(cwd, sha256(videoInfo.url) + '.mp4')
170 162
171 log.info('Downloading video "%s"...', videoInfo.title) 163 log.info('Downloading video "%s"...', videoInfo.name)
172 164
173 const youtubeDLOptions = [ '-f', youtubeDL.getYoutubeDLVideoFormat(), ...command.args, '-o', path ]
174 try { 165 try {
175 const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() 166 const youtubeDLBinary = await YoutubeDLCLI.safeGet()
176 const youtubeDLExec = promisify(youtubeDLBinary.exec).bind(youtubeDLBinary) 167 const output = await youtubeDLBinary.download({
177 const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions) 168 url: videoInfo.url,
169 format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
170 output: path,
171 additionalYoutubeDLArgs: command.args,
172 processOptions
173 })
174
178 log.info(output.join('\n')) 175 log.info(output.join('\n'))
179 await uploadVideoOnPeerTube({ 176 await uploadVideoOnPeerTube({
180 youtubeDL,
181 cwd, 177 cwd,
182 url, 178 url,
183 username, 179 username,
184 password, 180 password,
185 videoInfo: normalizeObject(videoInfo), 181 videoInfo,
186 videoPath: path 182 videoPath: path
187 }) 183 })
188 } catch (err) { 184 } catch (err) {
@@ -191,57 +187,34 @@ async function processVideo (parameters: {
191} 187}
192 188
193async function uploadVideoOnPeerTube (parameters: { 189async function uploadVideoOnPeerTube (parameters: {
194 youtubeDL: YoutubeDL 190 videoInfo: YoutubeDLInfo
195 videoInfo: any
196 videoPath: string 191 videoPath: string
197 cwd: string 192 cwd: string
198 url: string 193 url: string
199 username: string 194 username: string
200 password: string 195 password: string
201}) { 196}) {
202 const { youtubeDL, videoInfo, videoPath, cwd, url, username, password } = parameters 197 const { videoInfo, videoPath, cwd, url, username, password } = parameters
203 198
204 const server = buildServer(url) 199 const server = buildServer(url)
205 await assignToken(server, username, password) 200 await assignToken(server, username, password)
206 201
207 const category = await getCategory(server, videoInfo.categories) 202 let thumbnailfile: string
208 const licence = getLicence(videoInfo.license) 203 if (videoInfo.thumbnailUrl) {
209 let tags = [] 204 thumbnailfile = join(cwd, sha256(videoInfo.thumbnailUrl) + '.jpg')
210 if (Array.isArray(videoInfo.tags)) {
211 tags = videoInfo.tags
212 .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
213 .map(t => t.normalize())
214 .slice(0, 5)
215 }
216
217 let thumbnailfile
218 if (videoInfo.thumbnail) {
219 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg')
220 205
221 await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile) 206 await doRequestAndSaveToFile(videoInfo.thumbnailUrl, thumbnailfile)
222 } 207 }
223 208
224 const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) 209 const baseAttributes = await buildVideoAttributesFromCommander(server, program, videoInfo)
225
226 const defaultAttributes = {
227 name: truncate(videoInfo.title, {
228 length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
229 separator: /,? +/,
230 omission: ' […]'
231 }),
232 category,
233 licence,
234 nsfw: isNSFW(videoInfo),
235 description: videoInfo.description,
236 tags
237 }
238
239 const baseAttributes = await buildVideoAttributesFromCommander(server, program, defaultAttributes)
240 210
241 const attributes = { 211 const attributes = {
242 ...baseAttributes, 212 ...baseAttributes,
243 213
244 originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null, 214 originallyPublishedAt: videoInfo.originallyPublishedAt
215 ? videoInfo.originallyPublishedAt.toISOString()
216 : null,
217
245 thumbnailfile, 218 thumbnailfile,
246 previewfile: thumbnailfile, 219 previewfile: thumbnailfile,
247 fixture: videoPath 220 fixture: videoPath
@@ -266,67 +239,26 @@ async function uploadVideoOnPeerTube (parameters: {
266 await remove(videoPath) 239 await remove(videoPath)
267 if (thumbnailfile) await remove(thumbnailfile) 240 if (thumbnailfile) await remove(thumbnailfile)
268 241
269 log.warn('Uploaded video "%s"!\n', attributes.name) 242 log.info('Uploaded video "%s"!\n', attributes.name)
270} 243}
271 244
272/* ---------------------------------------------------------- */ 245/* ---------------------------------------------------------- */
273 246
274async function getCategory (server: PeerTubeServer, categories: string[]) { 247async function fetchObject (info: any) {
275 if (!categories) return undefined 248 const url = buildUrl(info)
276
277 const categoryString = categories[0]
278
279 if (categoryString === 'News & Politics') return 11
280
281 const categoriesServer = await server.videos.getCategories()
282
283 for (const key of Object.keys(categoriesServer)) {
284 const categoryServer = categoriesServer[key]
285 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
286 }
287
288 return undefined
289}
290
291function getLicence (licence: string) {
292 if (!licence) return undefined
293
294 if (licence.includes('Creative Commons Attribution licence')) return 1
295
296 return undefined
297}
298
299function normalizeObject (obj: any) {
300 const newObj: any = {}
301
302 for (const key of Object.keys(obj)) {
303 // Deprecated key
304 if (key === 'resolution') continue
305
306 const value = obj[key]
307
308 if (typeof value === 'string') {
309 newObj[key] = value.normalize()
310 } else {
311 newObj[key] = value
312 }
313 }
314 249
315 return newObj 250 const youtubeDLCLI = await YoutubeDLCLI.safeGet()
316} 251 const result = await youtubeDLCLI.getInfo({
252 url,
253 format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
254 processOptions
255 })
317 256
318function fetchObject (info: any) { 257 const builder = new YoutubeDLInfoBuilder(result)
319 const url = buildUrl(info)
320 258
321 return new Promise<any>(async (res, rej) => { 259 const videoInfo = builder.getInfo()
322 const youtubeDL = await YoutubeDL.safeGetYoutubeDL()
323 youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => {
324 if (err) return rej(err)
325 260
326 const videoInfoWithUrl = Object.assign(videoInfo, { url }) 261 return { ...videoInfo, url }
327 return res(normalizeObject(videoInfoWithUrl))
328 })
329 })
330} 262}
331 263
332function buildUrl (info: any) { 264function buildUrl (info: any) {
@@ -340,10 +272,6 @@ function buildUrl (info: any) {
340 return 'https://www.youtube.com/watch?v=' + info.id 272 return 'https://www.youtube.com/watch?v=' + info.id
341} 273}
342 274
343function isNSFW (info: any) {
344 return info.age_limit && info.age_limit >= 16
345}
346
347function normalizeTargetUrl (url: string) { 275function normalizeTargetUrl (url: string) {
348 let normalizedUrl = url.replace(/\/+$/, '') 276 let normalizedUrl = url.replace(/\/+$/, '')
349 277
@@ -404,14 +332,11 @@ function exitError (message: string, ...meta: any[]) {
404 process.exit(-1) 332 process.exit(-1)
405} 333}
406 334
407function getYoutubeDLInfo (youtubeDL: any, url: string, args: string[]) { 335function getYoutubeDLInfo (youtubeDLCLI: YoutubeDLCLI, url: string, args: string[]) {
408 return new Promise<any>((res, rej) => { 336 return youtubeDLCLI.getInfo({
409 const options = [ '-j', '--flat-playlist', '--playlist-reverse', ...args ] 337 url,
410 338 format: YoutubeDLCLI.getYoutubeDLVideoFormat([]),
411 youtubeDL.getInfo(url, options, processOptions, (err, info) => { 339 additionalYoutubeDLArgs: [ '-j', '--flat-playlist', '--playlist-reverse', ...args ],
412 if (err) return rej(err) 340 processOptions
413
414 return res(info)
415 })
416 }) 341 })
417} 342}
diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts
index 6299a48f5..658fe5fd3 100644
--- a/shared/extra-utils/miscs/tests.ts
+++ b/shared/extra-utils/miscs/tests.ts
@@ -20,7 +20,7 @@ const FIXTURE_URLS = {
20 youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4', 20 youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4',
21 21
22 // eslint-disable-next-line max-len 22 // eslint-disable-next-line max-len
23 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', 23 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
24 24
25 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', 25 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
26 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', 26 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
index fc44cd250..35e722408 100644
--- a/shared/extra-utils/videos/captions.ts
+++ b/shared/extra-utils/videos/captions.ts
@@ -2,12 +2,16 @@ import { expect } from 'chai'
2import request from 'supertest' 2import request from 'supertest'
3import { HttpStatusCode } from '@shared/models' 3import { HttpStatusCode } from '@shared/models'
4 4
5async function testCaptionFile (url: string, captionPath: string, containsString: string) { 5async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) {
6 const res = await request(url) 6 const res = await request(url)
7 .get(captionPath) 7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200) 8 .expect(HttpStatusCode.OK_200)
9 9
10 expect(res.text).to.contain(containsString) 10 if (toTest instanceof RegExp) {
11 expect(res.text).to.match(toTest)
12 } else {
13 expect(res.text).to.contain(toTest)
14 }
11} 15}
12 16
13// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
diff --git a/support/docker/production/config/production.yaml b/support/docker/production/config/production.yaml
index 54d619b1a..2a9117242 100644
--- a/support/docker/production/config/production.yaml
+++ b/support/docker/production/config/production.yaml
@@ -44,6 +44,7 @@ redis:
44# From the project root directory 44# From the project root directory
45storage: 45storage:
46 tmp: '../data/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... 46 tmp: '../data/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
47 bin: '../data/bin/'
47 avatars: '../data/avatars/' 48 avatars: '../data/avatars/'
48 videos: '../data/videos/' 49 videos: '../data/videos/'
49 streaming_playlists: '../data/streaming-playlists' 50 streaming_playlists: '../data/streaming-playlists'
diff --git a/yarn.lock b/yarn.lock
index d49f52b43..4e18fbbe1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2064,11 +2064,6 @@ array-differ@^3.0.0:
2064 resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" 2064 resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b"
2065 integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== 2065 integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==
2066 2066
2067array-find-index@^1.0.1:
2068 version "1.0.2"
2069 resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
2070 integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
2071
2072array-flatten@1.1.1: 2067array-flatten@1.1.1:
2073 version "1.1.1" 2068 version "1.1.1"
2074 resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 2069 resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -2104,11 +2099,6 @@ arraybuffer.slice@~0.0.7:
2104 resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" 2099 resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
2105 integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== 2100 integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
2106 2101
2107arrify@^1.0.1:
2108 version "1.0.1"
2109 resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
2110 integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
2111
2112arrify@^2.0.1: 2102arrify@^2.0.1:
2113 version "2.0.1" 2103 version "2.0.1"
2114 resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" 2104 resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@@ -2296,32 +2286,6 @@ better-assert@~1.0.0:
2296 dependencies: 2286 dependencies:
2297 callsite "1.0.0" 2287 callsite "1.0.0"
2298 2288
2299bin-version-check-cli@~2.0.0:
2300 version "2.0.0"
2301 resolved "https://registry.yarnpkg.com/bin-version-check-cli/-/bin-version-check-cli-2.0.0.tgz#7d45a23dc55024bbf741b8e66dc5c0afbac7d738"
2302 integrity sha512-wPASWpdpQuY/qkiT0hOLpTT/siAkmM/GXkuuQ/kgF1HuO4LEoIR6CgjnmuEv6lCbOSh2CWxAqmeyynp2OA1qhQ==
2303 dependencies:
2304 arrify "^1.0.1"
2305 bin-version-check "^4.0.0"
2306 meow "^5.0.0"
2307
2308bin-version-check@^4.0.0:
2309 version "4.0.0"
2310 resolved "https://registry.yarnpkg.com/bin-version-check/-/bin-version-check-4.0.0.tgz#7d819c62496991f80d893e6e02a3032361608f71"
2311 integrity sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==
2312 dependencies:
2313 bin-version "^3.0.0"
2314 semver "^5.6.0"
2315 semver-truncate "^1.1.2"
2316
2317bin-version@^3.0.0:
2318 version "3.1.0"
2319 resolved "https://registry.yarnpkg.com/bin-version/-/bin-version-3.1.0.tgz#5b09eb280752b1bd28f0c9db3f96f2f43b6c0839"
2320 integrity sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==
2321 dependencies:
2322 execa "^1.0.0"
2323 find-versions "^3.0.0"
2324
2325binary-extensions@^2.0.0: 2289binary-extensions@^2.0.0:
2326 version "2.2.0" 2290 version "2.2.0"
2327 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 2291 resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -2666,25 +2630,11 @@ callsites@^3.0.0:
2666 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 2630 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
2667 integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 2631 integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
2668 2632
2669camelcase-keys@^4.0.0:
2670 version "4.2.0"
2671 resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77"
2672 integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=
2673 dependencies:
2674 camelcase "^4.1.0"
2675 map-obj "^2.0.0"
2676 quick-lru "^1.0.0"
2677
2678camelcase@5.0.0: 2633camelcase@5.0.0:
2679 version "5.0.0" 2634 version "5.0.0"
2680 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" 2635 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
2681 integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== 2636 integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
2682 2637
2683camelcase@^4.1.0:
2684 version "4.1.0"
2685 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
2686 integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
2687
2688camelcase@^5.0.0: 2638camelcase@^5.0.0:
2689 version "5.3.1" 2639 version "5.3.1"
2690 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" 2640 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -3215,17 +3165,6 @@ cross-argv@^1.0.0:
3215 resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-1.0.0.tgz#e7221e9ff73092a80496c699c8c45efb20f6486c" 3165 resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-1.0.0.tgz#e7221e9ff73092a80496c699c8c45efb20f6486c"
3216 integrity sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw== 3166 integrity sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw==
3217 3167
3218cross-spawn@^6.0.0:
3219 version "6.0.5"
3220 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
3221 integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
3222 dependencies:
3223 nice-try "^1.0.4"
3224 path-key "^2.0.1"
3225 semver "^5.5.0"
3226 shebang-command "^1.2.0"
3227 which "^1.2.9"
3228
3229cross-spawn@^7.0.2, cross-spawn@^7.0.3: 3168cross-spawn@^7.0.2, cross-spawn@^7.0.3:
3230 version "7.0.3" 3169 version "7.0.3"
3231 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 3170 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -3261,13 +3200,6 @@ css-what@^5.0.0, css-what@^5.0.1:
3261 resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" 3200 resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
3262 integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== 3201 integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
3263 3202
3264currently-unhandled@^0.4.1:
3265 version "0.4.1"
3266 resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
3267 integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
3268 dependencies:
3269 array-find-index "^1.0.1"
3270
3271cycle@1.0.x: 3203cycle@1.0.x:
3272 version "1.0.3" 3204 version "1.0.3"
3273 resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" 3205 resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
@@ -3355,15 +3287,7 @@ decache@^4.6.0:
3355 dependencies: 3287 dependencies:
3356 callsite "^1.0.0" 3288 callsite "^1.0.0"
3357 3289
3358decamelize-keys@^1.0.0: 3290decamelize@^1.2.0:
3359 version "1.1.0"
3360 resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
3361 integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
3362 dependencies:
3363 decamelize "^1.1.0"
3364 map-obj "^1.0.0"
3365
3366decamelize@^1.1.0, decamelize@^1.2.0:
3367 version "1.2.0" 3291 version "1.2.0"
3368 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 3292 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
3369 integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 3293 integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3737,13 +3661,6 @@ err-code@^3.0.1:
3737 resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" 3661 resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920"
3738 integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== 3662 integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==
3739 3663
3740error-ex@^1.3.1:
3741 version "1.3.2"
3742 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
3743 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
3744 dependencies:
3745 is-arrayish "^0.2.1"
3746
3747es-abstract@^1.19.0, es-abstract@^1.19.1: 3664es-abstract@^1.19.0, es-abstract@^1.19.1:
3748 version "1.19.1" 3665 version "1.19.1"
3749 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" 3666 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
@@ -4084,23 +4001,10 @@ event-target-shim@^5.0.0:
4084 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 4001 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
4085 integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 4002 integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
4086 4003
4087execa@^1.0.0: 4004execa@^5.1.1:
4088 version "1.0.0" 4005 version "5.1.1"
4089 resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" 4006 resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
4090 integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== 4007 integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
4091 dependencies:
4092 cross-spawn "^6.0.0"
4093 get-stream "^4.0.0"
4094 is-stream "^1.1.0"
4095 npm-run-path "^2.0.0"
4096 p-finally "^1.0.0"
4097 signal-exit "^3.0.0"
4098 strip-eof "^1.0.0"
4099
4100execa@~5.0.0:
4101 version "5.0.1"
4102 resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.1.tgz#aee63b871c9b2cb56bc9addcd3c70a785c6bf0d1"
4103 integrity sha512-4hFTjFbFzQa3aCLobpbPJR/U+VoL1wdV5ozOWjeet0AWDeYr9UFGM1eUFWHX+VtOWFq4p0xXUXfW1YxUaP4fpw==
4104 dependencies: 4008 dependencies:
4105 cross-spawn "^7.0.3" 4009 cross-spawn "^7.0.3"
4106 get-stream "^6.0.0" 4010 get-stream "^6.0.0"
@@ -4306,7 +4210,7 @@ find-up@5.0.0:
4306 locate-path "^6.0.0" 4210 locate-path "^6.0.0"
4307 path-exists "^4.0.0" 4211 path-exists "^4.0.0"
4308 4212
4309find-up@^2.0.0, find-up@^2.1.0: 4213find-up@^2.1.0:
4310 version "2.1.0" 4214 version "2.1.0"
4311 resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" 4215 resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
4312 integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= 4216 integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
@@ -4321,13 +4225,6 @@ find-up@^4.1.0:
4321 locate-path "^5.0.0" 4225 locate-path "^5.0.0"
4322 path-exists "^4.0.0" 4226 path-exists "^4.0.0"
4323 4227
4324find-versions@^3.0.0:
4325 version "3.2.0"
4326 resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e"
4327 integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==
4328 dependencies:
4329 semver-regex "^2.0.0"
4330
4331flat-cache@^3.0.4: 4228flat-cache@^3.0.4:
4332 version "3.0.4" 4229 version "3.0.4"
4333 resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" 4230 resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -4516,7 +4413,7 @@ get-stdin@^8.0.0:
4516 resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" 4413 resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
4517 integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== 4414 integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
4518 4415
4519get-stream@^4.0.0, get-stream@^4.1.0: 4416get-stream@^4.1.0:
4520 version "4.1.0" 4417 version "4.1.0"
4521 resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 4418 resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
4522 integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== 4419 integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
@@ -4630,7 +4527,7 @@ globby@^11.0.3:
4630 merge2 "^1.3.0" 4527 merge2 "^1.3.0"
4631 slash "^3.0.0" 4528 slash "^3.0.0"
4632 4529
4633got@^11.8.2, got@~11.8.1: 4530got@^11.8.2:
4634 version "11.8.2" 4531 version "11.8.2"
4635 resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" 4532 resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
4636 integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== 4533 integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
@@ -4759,18 +4656,6 @@ helmet@^4.1.0:
4759 resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df" 4656 resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
4760 integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg== 4657 integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==
4761 4658
4762hh-mm-ss@~1.2.0:
4763 version "1.2.0"
4764 resolved "https://registry.yarnpkg.com/hh-mm-ss/-/hh-mm-ss-1.2.0.tgz#6d0f0b8280824a634cb1d1f20e0bc7bc8b689948"
4765 integrity sha512-f4I9Hz1dLpX/3mrEs7yq30+FiuO3tt5NWAqAGeBTaoeoBfB8vhcQ3BphuDc5DjZb/K809agqrAaFlP0jhEU/8w==
4766 dependencies:
4767 zero-fill "^2.2.3"
4768
4769hosted-git-info@^2.1.4:
4770 version "2.8.9"
4771 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
4772 integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
4773
4774hpagent@^0.1.2: 4659hpagent@^0.1.2:
4775 version "0.1.2" 4660 version "0.1.2"
4776 resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" 4661 resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
@@ -5008,11 +4893,6 @@ imurmurhash@^0.1.4:
5008 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 4893 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
5009 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 4894 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
5010 4895
5011indent-string@^3.0.0:
5012 version "3.2.0"
5013 resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
5014 integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=
5015
5016indexof@0.0.1: 4896indexof@0.0.1:
5017 version "0.0.1" 4897 version "0.0.1"
5018 resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 4898 resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
@@ -5114,11 +4994,6 @@ ipv6-normalize@1.0.1:
5114 resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8" 4994 resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8"
5115 integrity sha1-GzJYKQ02X6gyOeiZB93kWS52IKg= 4995 integrity sha1-GzJYKQ02X6gyOeiZB93kWS52IKg=
5116 4996
5117is-arrayish@^0.2.1:
5118 version "0.2.1"
5119 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
5120 integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
5121
5122is-arrayish@^0.3.1: 4997is-arrayish@^0.3.1:
5123 version "0.3.2" 4998 version "0.3.2"
5124 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" 4999 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
@@ -5294,11 +5169,6 @@ is-path-inside@^3.0.2:
5294 resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" 5169 resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
5295 integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 5170 integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
5296 5171
5297is-plain-obj@^1.1.0:
5298 version "1.1.0"
5299 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
5300 integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
5301
5302is-plain-obj@^2.1.0: 5172is-plain-obj@^2.1.0:
5303 version "2.1.0" 5173 version "2.1.0"
5304 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 5174 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
@@ -5327,11 +5197,6 @@ is-shared-array-buffer@^1.0.1:
5327 resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" 5197 resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
5328 integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== 5198 integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
5329 5199
5330is-stream@^1.1.0:
5331 version "1.1.0"
5332 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
5333 integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
5334
5335is-stream@^2.0.0: 5200is-stream@^2.0.0:
5336 version "2.0.1" 5201 version "2.0.1"
5337 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" 5202 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -5466,11 +5331,6 @@ json-buffer@3.0.1:
5466 resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" 5331 resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
5467 integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 5332 integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
5468 5333
5469json-parse-better-errors@^1.0.1:
5470 version "1.0.2"
5471 resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
5472 integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
5473
5474json-schema-traverse@^0.4.1: 5334json-schema-traverse@^0.4.1:
5475 version "0.4.1" 5335 version "0.4.1"
5476 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 5336 resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -5703,16 +5563,6 @@ load-ip-set@^2.2.1:
5703 simple-get "^4.0.0" 5563 simple-get "^4.0.0"
5704 split "^1.0.1" 5564 split "^1.0.1"
5705 5565
5706load-json-file@^4.0.0:
5707 version "4.0.0"
5708 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
5709 integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
5710 dependencies:
5711 graceful-fs "^4.1.2"
5712 parse-json "^4.0.0"
5713 pify "^3.0.0"
5714 strip-bom "^3.0.0"
5715
5716locate-path@^2.0.0: 5566locate-path@^2.0.0:
5717 version "2.0.0" 5567 version "2.0.0"
5718 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" 5568 resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -5794,14 +5644,6 @@ logform@^2.2.0:
5794 safe-stable-stringify "^1.1.0" 5644 safe-stable-stringify "^1.1.0"
5795 triple-beam "^1.3.0" 5645 triple-beam "^1.3.0"
5796 5646
5797loud-rejection@^1.0.0:
5798 version "1.6.0"
5799 resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
5800 integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
5801 dependencies:
5802 currently-unhandled "^0.4.1"
5803 signal-exit "^3.0.0"
5804
5805lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: 5647lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
5806 version "1.0.1" 5648 version "1.0.1"
5807 resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" 5649 resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
@@ -5937,16 +5779,6 @@ manage-path@^2.0.0:
5937 resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" 5779 resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597"
5938 integrity sha1-9M+EV7km7u4qg7FzUBQUvHbrlZc= 5780 integrity sha1-9M+EV7km7u4qg7FzUBQUvHbrlZc=
5939 5781
5940map-obj@^1.0.0:
5941 version "1.0.1"
5942 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
5943 integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
5944
5945map-obj@^2.0.0:
5946 version "2.0.0"
5947 resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
5948 integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
5949
5950markdown-it-emoji@^2.0.0: 5782markdown-it-emoji@^2.0.0:
5951 version "2.0.0" 5783 version "2.0.0"
5952 resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231" 5784 resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231"
@@ -6022,21 +5854,6 @@ mensch@^0.3.4:
6022 resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" 5854 resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd"
6023 integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== 5855 integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==
6024 5856
6025meow@^5.0.0:
6026 version "5.0.0"
6027 resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4"
6028 integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==
6029 dependencies:
6030 camelcase-keys "^4.0.0"
6031 decamelize-keys "^1.0.0"
6032 loud-rejection "^1.0.0"
6033 minimist-options "^3.0.1"
6034 normalize-package-data "^2.3.4"
6035 read-pkg-up "^3.0.0"
6036 redent "^2.0.0"
6037 trim-newlines "^2.0.0"
6038 yargs-parser "^10.0.0"
6039
6040merge-descriptors@1.0.1: 5857merge-descriptors@1.0.1:
6041 version "1.0.1" 5858 version "1.0.1"
6042 resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 5859 resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6135,14 +5952,6 @@ minimatch@3.0.4, minimatch@^3.0.4:
6135 dependencies: 5952 dependencies:
6136 brace-expansion "^1.1.7" 5953 brace-expansion "^1.1.7"
6137 5954
6138minimist-options@^3.0.1:
6139 version "3.0.2"
6140 resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954"
6141 integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==
6142 dependencies:
6143 arrify "^1.0.1"
6144 is-plain-obj "^1.1.0"
6145
6146minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: 5955minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5:
6147 version "1.2.5" 5956 version "1.2.5"
6148 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 5957 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
@@ -6180,7 +5989,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.4:
6180 dependencies: 5989 dependencies:
6181 minimist "^1.2.5" 5990 minimist "^1.2.5"
6182 5991
6183mkdirp@^1.0.3, mkdirp@~1.0.4: 5992mkdirp@^1.0.3:
6184 version "1.0.4" 5993 version "1.0.4"
6185 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 5994 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
6186 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 5995 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@@ -6392,11 +6201,6 @@ next-tick@~1.0.0:
6392 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" 6201 resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
6393 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= 6202 integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
6394 6203
6395nice-try@^1.0.4:
6396 version "1.0.5"
6397 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
6398 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
6399
6400node-addon-api@^3.1.0: 6204node-addon-api@^3.1.0:
6401 version "3.2.1" 6205 version "3.2.1"
6402 resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" 6206 resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@@ -6481,16 +6285,6 @@ nopt@~1.0.10:
6481 dependencies: 6285 dependencies:
6482 abbrev "1" 6286 abbrev "1"
6483 6287
6484normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
6485 version "2.5.0"
6486 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
6487 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
6488 dependencies:
6489 hosted-git-info "^2.1.4"
6490 resolve "^1.10.0"
6491 semver "2 || 3 || 4 || 5"
6492 validate-npm-package-license "^3.0.1"
6493
6494normalize-path@^3.0.0, normalize-path@~3.0.0: 6288normalize-path@^3.0.0, normalize-path@~3.0.0:
6495 version "3.0.0" 6289 version "3.0.0"
6496 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 6290 resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -6506,13 +6300,6 @@ normalize-url@^6.0.1:
6506 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" 6300 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
6507 integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== 6301 integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
6508 6302
6509npm-run-path@^2.0.0:
6510 version "2.0.2"
6511 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
6512 integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
6513 dependencies:
6514 path-key "^2.0.0"
6515
6516npm-run-path@^4.0.1: 6303npm-run-path@^4.0.1:
6517 version "4.0.1" 6304 version "4.0.1"
6518 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" 6305 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -6813,14 +6600,6 @@ parse-headers@^2.0.0:
6813 resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.4.tgz#9eaf2d02bed2d1eff494331ce3df36d7924760bf" 6600 resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.4.tgz#9eaf2d02bed2d1eff494331ce3df36d7924760bf"
6814 integrity sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw== 6601 integrity sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw==
6815 6602
6816parse-json@^4.0.0:
6817 version "4.0.0"
6818 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
6819 integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
6820 dependencies:
6821 error-ex "^1.3.1"
6822 json-parse-better-errors "^1.0.1"
6823
6824parse-srcset@^1.0.2: 6603parse-srcset@^1.0.2:
6825 version "1.0.2" 6604 version "1.0.2"
6826 resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" 6605 resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
@@ -6908,11 +6687,6 @@ path-is-absolute@^1.0.0:
6908 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 6687 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
6909 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 6688 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
6910 6689
6911path-key@^2.0.0, path-key@^2.0.1:
6912 version "2.0.1"
6913 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
6914 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
6915
6916path-key@^3.0.0, path-key@^3.1.0: 6690path-key@^3.0.0, path-key@^3.1.0:
6917 version "3.1.1" 6691 version "3.1.1"
6918 resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 6692 resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -6928,13 +6702,6 @@ path-to-regexp@0.1.7:
6928 resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 6702 resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
6929 integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 6703 integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
6930 6704
6931path-type@^3.0.0:
6932 version "3.0.0"
6933 resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
6934 integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
6935 dependencies:
6936 pify "^3.0.0"
6937
6938path-type@^4.0.0: 6705path-type@^4.0.0:
6939 version "4.0.0" 6706 version "4.0.0"
6940 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 6707 resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -7043,11 +6810,6 @@ piece-length@^2.0.1:
7043 resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-2.0.1.tgz#dbed4e78976955f34466d0a65304d0cb21914ac9" 6810 resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-2.0.1.tgz#dbed4e78976955f34466d0a65304d0cb21914ac9"
7044 integrity sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug== 6811 integrity sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug==
7045 6812
7046pify@^3.0.0:
7047 version "3.0.0"
7048 resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
7049 integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
7050
7051pify@^4.0.1: 6813pify@^4.0.1:
7052 version "4.0.1" 6814 version "4.0.1"
7053 resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" 6815 resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@@ -7371,11 +7133,6 @@ queue-tick@^1.0.0:
7371 resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725" 7133 resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725"
7372 integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ== 7134 integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==
7373 7135
7374quick-lru@^1.0.0:
7375 version "1.1.0"
7376 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
7377 integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
7378
7379quick-lru@^5.1.1: 7136quick-lru@^5.1.1:
7380 version "5.1.1" 7137 version "5.1.1"
7381 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" 7138 resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
@@ -7470,23 +7227,6 @@ rdf-canonize@^3.0.0:
7470 dependencies: 7227 dependencies:
7471 setimmediate "^1.0.5" 7228 setimmediate "^1.0.5"
7472 7229
7473read-pkg-up@^3.0.0:
7474 version "3.0.0"
7475 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
7476 integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
7477 dependencies:
7478 find-up "^2.0.0"
7479 read-pkg "^3.0.0"
7480
7481read-pkg@^3.0.0:
7482 version "3.0.0"
7483 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
7484 integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
7485 dependencies:
7486 load-json-file "^4.0.0"
7487 normalize-package-data "^2.3.2"
7488 path-type "^3.0.0"
7489
7490read@1.0.x: 7230read@1.0.x:
7491 version "1.0.7" 7231 version "1.0.7"
7492 resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" 7232 resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
@@ -7555,14 +7295,6 @@ record-cache@^1.0.2:
7555 resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.1.tgz#ba3088a489f50491a4af7b14d410822c394fb811" 7295 resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.1.tgz#ba3088a489f50491a4af7b14d410822c394fb811"
7556 integrity sha512-L5hZlgWc7CmGbztnemQoKE1bLu9rtI2skOB0ttE4C5+TVszLE8Rd0YLTROSgvXKLAqPumS/soyN5tJW5wJLmJQ== 7296 integrity sha512-L5hZlgWc7CmGbztnemQoKE1bLu9rtI2skOB0ttE4C5+TVszLE8Rd0YLTROSgvXKLAqPumS/soyN5tJW5wJLmJQ==
7557 7297
7558redent@^2.0.0:
7559 version "2.0.0"
7560 resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"
7561 integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=
7562 dependencies:
7563 indent-string "^3.0.0"
7564 strip-indent "^2.0.0"
7565
7566redis-commands@1.7.0, redis-commands@^1.7.0: 7298redis-commands@1.7.0, redis-commands@^1.7.0:
7567 version "1.7.0" 7299 version "1.7.0"
7568 resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" 7300 resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
@@ -7655,7 +7387,7 @@ resolve-from@^4.0.0:
7655 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 7387 resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
7656 integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 7388 integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
7657 7389
7658resolve@^1.10.0, resolve@^1.10.1, resolve@^1.15.1, resolve@^1.20.0: 7390resolve@^1.10.1, resolve@^1.15.1, resolve@^1.20.0:
7659 version "1.20.0" 7391 version "1.20.0"
7660 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" 7392 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
7661 integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== 7393 integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -7801,19 +7533,7 @@ semver-diff@^3.1.1:
7801 dependencies: 7533 dependencies:
7802 semver "^6.3.0" 7534 semver "^6.3.0"
7803 7535
7804semver-regex@^2.0.0: 7536semver@^5.7.1:
7805 version "2.0.0"
7806 resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
7807 integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==
7808
7809semver-truncate@^1.1.2:
7810 version "1.1.2"
7811 resolved "https://registry.yarnpkg.com/semver-truncate/-/semver-truncate-1.1.2.tgz#57f41de69707a62709a7e0104ba2117109ea47e8"
7812 integrity sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=
7813 dependencies:
7814 semver "^5.3.0"
7815
7816"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
7817 version "5.7.1" 7537 version "5.7.1"
7818 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 7538 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
7819 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 7539 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -7917,13 +7637,6 @@ setprototypeof@1.2.0:
7917 resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 7637 resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
7918 integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 7638 integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
7919 7639
7920shebang-command@^1.2.0:
7921 version "1.2.0"
7922 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
7923 integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
7924 dependencies:
7925 shebang-regex "^1.0.0"
7926
7927shebang-command@^2.0.0: 7640shebang-command@^2.0.0:
7928 version "2.0.0" 7641 version "2.0.0"
7929 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 7642 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -7931,11 +7644,6 @@ shebang-command@^2.0.0:
7931 dependencies: 7644 dependencies:
7932 shebang-regex "^3.0.0" 7645 shebang-regex "^3.0.0"
7933 7646
7934shebang-regex@^1.0.0:
7935 version "1.0.0"
7936 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
7937 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
7938
7939shebang-regex@^3.0.0: 7647shebang-regex@^3.0.0:
7940 version "3.0.0" 7648 version "3.0.0"
7941 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 7649 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
@@ -8176,32 +7884,6 @@ spawn-command@^0.0.2-1:
8176 resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" 7884 resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
8177 integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= 7885 integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
8178 7886
8179spdx-correct@^3.0.0:
8180 version "3.1.1"
8181 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
8182 integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
8183 dependencies:
8184 spdx-expression-parse "^3.0.0"
8185 spdx-license-ids "^3.0.0"
8186
8187spdx-exceptions@^2.1.0:
8188 version "2.3.0"
8189 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
8190 integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
8191
8192spdx-expression-parse@^3.0.0:
8193 version "3.0.1"
8194 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
8195 integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
8196 dependencies:
8197 spdx-exceptions "^2.1.0"
8198 spdx-license-ids "^3.0.0"
8199
8200spdx-license-ids@^3.0.0:
8201 version "3.0.10"
8202 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
8203 integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
8204
8205speed-limiter@^1.0.2: 7887speed-limiter@^1.0.2:
8206 version "1.0.2" 7888 version "1.0.2"
8207 resolved "https://registry.yarnpkg.com/speed-limiter/-/speed-limiter-1.0.2.tgz#e4632f476a1d25d32557aad7bd089b3a0d948116" 7889 resolved "https://registry.yarnpkg.com/speed-limiter/-/speed-limiter-1.0.2.tgz#e4632f476a1d25d32557aad7bd089b3a0d948116"
@@ -8331,11 +8013,6 @@ stream-with-known-length-to-buffer@^1.0.4:
8331 dependencies: 8013 dependencies:
8332 once "^1.4.0" 8014 once "^1.4.0"
8333 8015
8334streamify@~1.0.0:
8335 version "1.0.0"
8336 resolved "https://registry.yarnpkg.com/streamify/-/streamify-1.0.0.tgz#c80a1347d6d3b905c0382011adac67402a3b1e2b"
8337 integrity sha512-pe2ZoxE+ie5wAjRgKWb5Ur4R5Oa++eoQmHLqGGy4nQn/8BetJcpHkHXRuP3ZIJ/Ptl/rbd76fdn9aQJNys8cKA==
8338
8339streamsearch@0.1.2: 8016streamsearch@0.1.2:
8340 version "0.1.2" 8017 version "0.1.2"
8341 resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" 8018 resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@@ -8444,21 +8121,11 @@ strip-bom@^3.0.0:
8444 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 8121 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
8445 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= 8122 integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
8446 8123
8447strip-eof@^1.0.0:
8448 version "1.0.0"
8449 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
8450 integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
8451
8452strip-final-newline@^2.0.0: 8124strip-final-newline@^2.0.0:
8453 version "2.0.0" 8125 version "2.0.0"
8454 resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" 8126 resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
8455 integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== 8127 integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
8456 8128
8457strip-indent@^2.0.0:
8458 version "2.0.0"
8459 resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
8460 integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
8461
8462strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: 8129strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
8463 version "3.1.1" 8130 version "3.1.1"
8464 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 8131 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
@@ -8726,11 +8393,6 @@ tree-kill@^1.2.2:
8726 resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" 8393 resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
8727 integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== 8394 integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
8728 8395
8729trim-newlines@^2.0.0:
8730 version "2.0.0"
8731 resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
8732 integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=
8733
8734triple-beam@^1.2.0, triple-beam@^1.3.0: 8396triple-beam@^1.2.0, triple-beam@^1.3.0:
8735 version "1.3.0" 8397 version "1.3.0"
8736 resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" 8398 resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
@@ -8896,7 +8558,7 @@ unique-string@^2.0.0:
8896 dependencies: 8558 dependencies:
8897 crypto-random-string "^2.0.0" 8559 crypto-random-string "^2.0.0"
8898 8560
8899universalify@^2.0.0, universalify@~2.0.0: 8561universalify@^2.0.0:
8900 version "2.0.0" 8562 version "2.0.0"
8901 resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" 8563 resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
8902 integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== 8564 integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
@@ -9051,14 +8713,6 @@ valid-data-url@^3.0.0:
9051 resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" 8713 resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f"
9052 integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== 8714 integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==
9053 8715
9054validate-npm-package-license@^3.0.1:
9055 version "3.0.4"
9056 resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
9057 integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
9058 dependencies:
9059 spdx-correct "^3.0.0"
9060 spdx-expression-parse "^3.0.0"
9061
9062validator@^13.0.0, validator@^13.6.0: 8716validator@^13.0.0, validator@^13.6.0:
9063 version "13.6.0" 8717 version "13.6.0"
9064 resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" 8718 resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059"
@@ -9203,7 +8857,7 @@ which@2.0.2, which@^2.0.1, which@^2.0.2:
9203 dependencies: 8857 dependencies:
9204 isexe "^2.0.0" 8858 isexe "^2.0.0"
9205 8859
9206which@^1.1.1, which@^1.2.9: 8860which@^1.1.1:
9207 version "1.3.1" 8861 version "1.3.1"
9208 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 8862 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
9209 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 8863 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -9429,13 +9083,6 @@ yargs-parser@20.2.4:
9429 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" 9083 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
9430 integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== 9084 integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
9431 9085
9432yargs-parser@^10.0.0:
9433 version "10.1.0"
9434 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
9435 integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
9436 dependencies:
9437 camelcase "^4.1.0"
9438
9439yargs-parser@^18.1.2: 9086yargs-parser@^18.1.2:
9440 version "18.1.3" 9087 version "18.1.3"
9441 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" 9088 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
@@ -9504,19 +9151,6 @@ yocto-queue@^0.1.0:
9504 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 9151 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
9505 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 9152 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
9506 9153
9507youtube-dl@^3.0.2:
9508 version "3.5.0"
9509 resolved "https://registry.yarnpkg.com/youtube-dl/-/youtube-dl-3.5.0.tgz#75e7be8647128de34244cb74606edf87b9ce60fa"
9510 integrity sha512-+I9o908rD154LqaVdP1f9Xlu+qYp3/m/bZeUwaxsAV7nR8W0IObqz0oAyxvE1Qrn7oTCvvg6MZ1oqkHIA8LA+g==
9511 dependencies:
9512 bin-version-check-cli "~2.0.0"
9513 execa "~5.0.0"
9514 got "~11.8.1"
9515 hh-mm-ss "~1.2.0"
9516 mkdirp "~1.0.4"
9517 streamify "~1.0.0"
9518 universalify "~2.0.0"
9519
9520z-schema@^5.0.1: 9154z-schema@^5.0.1:
9521 version "5.0.1" 9155 version "5.0.1"
9522 resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.1.tgz#f4d4efb1e8763c968b5539e42d11b6a47e91da62" 9156 resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.1.tgz#f4d4efb1e8763c968b5539e42d11b6a47e91da62"
@@ -9527,8 +9161,3 @@ z-schema@^5.0.1:
9527 validator "^13.6.0" 9161 validator "^13.6.0"
9528 optionalDependencies: 9162 optionalDependencies:
9529 commander "^2.7.1" 9163 commander "^2.7.1"
9530
9531zero-fill@^2.2.3:
9532 version "2.2.4"
9533 resolved "https://registry.yarnpkg.com/zero-fill/-/zero-fill-2.2.4.tgz#b041320973dbcb03cd90193270ac8d4a3da05fc1"
9534 integrity sha512-/N5GEDauLHz2uGnuJXWO1Wfib4EC+q4yp9C1jojM7RubwEKADqIqMcYpETMm1lRop403fi3v1qTOdgDE8DIOdw==