aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-07-16 14:22:16 +0200
committerChocobozzz <me@florianbigard.com>2018-07-16 14:31:40 +0200
commitf4001cf408a99049d01a356bfb20a62342de06ea (patch)
tree421776dfe64335dca2725ac3ac5f3b3e6b7564c6
parent16f7022b06fb76c0b00c23c970bc8df605b0ec63 (diff)
downloadPeerTube-f4001cf408a99049d01a356bfb20a62342de06ea.tar.gz
PeerTube-f4001cf408a99049d01a356bfb20a62342de06ea.tar.zst
PeerTube-f4001cf408a99049d01a356bfb20a62342de06ea.zip
Handle .srt subtitles
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts13
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html4
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts18
-rw-r--r--package.json1
-rw-r--r--server.ts6
-rw-r--r--server/controllers/api/videos/captions.ts10
-rw-r--r--server/helpers/captions-utils.ts47
-rw-r--r--server/helpers/custom-validators/video-captions.ts11
-rw-r--r--server/initializers/constants.ts17
-rw-r--r--server/initializers/installer.ts6
-rw-r--r--server/lib/cache/abstract-video-static-file-cache.ts9
-rw-r--r--server/lib/cache/videos-caption-cache.ts2
-rw-r--r--server/lib/cache/videos-preview-cache.ts2
-rw-r--r--server/models/video/video-caption.ts14
-rw-r--r--server/tests/api/check-params/video-captions.ts35
-rw-r--r--server/tests/api/videos/video-captions.ts53
-rw-r--r--server/tests/fixtures/subtitle-bad.txt11
-rw-r--r--server/tests/fixtures/subtitle-good.srt11
-rw-r--r--server/tests/utils/videos/videos.ts2
-rw-r--r--yarn.lock117
20 files changed, 336 insertions, 53 deletions
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 45b8c71f8..5498dac22 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -49,10 +49,14 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
49 } 49 }
50 50
51 show () { 51 show () {
52 this.closingModal = false
53
52 this.modal.show() 54 this.modal.show()
53 } 55 }
54 56
55 hide () { 57 hide () {
58 this.closingModal = true
59
56 this.modal.hide() 60 this.modal.hide()
57 } 61 }
58 62
@@ -65,7 +69,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
65 } 69 }
66 70
67 async addCaption () { 71 async addCaption () {
68 this.closingModal = true 72 this.hide()
69 73
70 const languageId = this.form.value[ 'language' ] 74 const languageId = this.form.value[ 'language' ]
71 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) 75 const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId)
@@ -74,7 +78,12 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
74 language: languageObject, 78 language: languageObject,
75 captionfile: this.form.value['captionfile'] 79 captionfile: this.form.value['captionfile']
76 }) 80 })
81 //
82 // this.form.patchValue({
83 // language: null,
84 // captionfile: null
85 // })
77 86
78 this.hide() 87 this.form.reset()
79 } 88 }
80} 89}
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 14d5f3614..4675cb827 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -151,7 +151,7 @@
151 151
152 <div class="form-group" *ngFor="let videoCaption of videoCaptions"> 152 <div class="form-group" *ngFor="let videoCaption of videoCaptions">
153 153
154 <div class="caption-entry"> 154 <div *ngIf="videoCaption.action !== 'REMOVE'" class="caption-entry">
155 <div class="caption-entry-label">{{ videoCaption.language.label }}</div> 155 <div class="caption-entry-label">{{ videoCaption.language.label }}</div>
156 156
157 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span> 157 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
@@ -200,5 +200,5 @@
200</div> 200</div>
201 201
202<my-video-caption-add-modal 202<my-video-caption-add-modal
203 #videoCaptionAddModal [existingCaptions]="getExistingCaptions()" (captionAdded)="onCaptionAdded($event)" 203 #videoCaptionAddModal [existingCaptions]="existingCaptions" (captionAdded)="onCaptionAdded($event)"
204></my-video-caption-add-modal> \ No newline at end of file 204></my-video-caption-add-modal> \ No newline at end of file
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
index 9394d7dab..c7beccb30 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts
@@ -68,6 +68,12 @@ export class VideoEditComponent implements OnInit, OnDestroy {
68 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() 68 this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
69 } 69 }
70 70
71 get existingCaptions () {
72 return this.videoCaptions
73 .filter(c => c.action !== 'REMOVE')
74 .map(c => c.language.id)
75 }
76
71 updateForm () { 77 updateForm () {
72 const defaultValues = { 78 const defaultValues = {
73 nsfw: 'false', 79 nsfw: 'false',
@@ -126,11 +132,15 @@ export class VideoEditComponent implements OnInit, OnDestroy {
126 if (this.schedulerInterval) clearInterval(this.schedulerInterval) 132 if (this.schedulerInterval) clearInterval(this.schedulerInterval)
127 } 133 }
128 134
129 getExistingCaptions () {
130 return this.videoCaptions.map(c => c.language.id)
131 }
132
133 onCaptionAdded (caption: VideoCaptionEdit) { 135 onCaptionAdded (caption: VideoCaptionEdit) {
136 const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
137
138 // Replace existing caption?
139 if (existingCaption) {
140 Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
141 return
142 }
143
134 this.videoCaptions.push( 144 this.videoCaptions.push(
135 Object.assign(caption, { action: 'CREATE' as 'CREATE' }) 145 Object.assign(caption, { action: 'CREATE' as 'CREATE' })
136 ) 146 )
diff --git a/package.json b/package.json
index 96b082363..586db76f4 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
121 "sequelize": "4.38.0", 121 "sequelize": "4.38.0",
122 "sequelize-typescript": "0.6.6-beta.1", 122 "sequelize-typescript": "0.6.6-beta.1",
123 "sharp": "^0.20.0", 123 "sharp": "^0.20.0",
124 "srt-to-vtt": "^1.1.2",
124 "uuid": "^3.1.0", 125 "uuid": "^3.1.0",
125 "validator": "^10.2.0", 126 "validator": "^10.2.0",
126 "webfinger.js": "^2.6.6", 127 "webfinger.js": "^2.6.6",
diff --git a/server.ts b/server.ts
index a7fea34da..a6052faed 100644
--- a/server.ts
+++ b/server.ts
@@ -26,7 +26,7 @@ import { checkMissedConfig, checkFFmpeg, checkConfig, checkActivityPubUrls } fro
26 26
27// Do not use barrels because we don't want to load all modules here (we need to initialize database first) 27// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
28import { logger } from './server/helpers/logger' 28import { logger } from './server/helpers/logger'
29import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants' 29import { API_VERSION, CONFIG, STATIC_PATHS, CACHE } from './server/initializers/constants'
30 30
31const missed = checkMissedConfig() 31const missed = checkMissedConfig()
32if (missed.length !== 0) { 32if (missed.length !== 0) {
@@ -182,8 +182,8 @@ async function startApplication () {
182 await JobQueue.Instance.init() 182 await JobQueue.Instance.init()
183 183
184 // Caches initializations 184 // Caches initializations
185 VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE) 185 VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, CACHE.PREVIEWS.MAX_AGE)
186 VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE) 186 VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, CACHE.VIDEO_CAPTIONS.MAX_AGE)
187 187
188 // Enable Schedulers 188 // Enable Schedulers
189 BadActorFollowScheduler.Instance.enable() 189 BadActorFollowScheduler.Instance.enable()
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 05412a17f..4cf8de1ef 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -9,11 +9,10 @@ import { createReqFiles } from '../../../helpers/express-utils'
9import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 9import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
10import { getFormattedObjects } from '../../../helpers/utils' 10import { getFormattedObjects } from '../../../helpers/utils'
11import { VideoCaptionModel } from '../../../models/video/video-caption' 11import { VideoCaptionModel } from '../../../models/video/video-caption'
12import { renamePromise } from '../../../helpers/core-utils'
13import { join } from 'path'
14import { VideoModel } from '../../../models/video/video' 12import { VideoModel } from '../../../models/video/video'
15import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
16import { federateVideoIfNeeded } from '../../../lib/activitypub' 14import { federateVideoIfNeeded } from '../../../lib/activitypub'
15import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
17 16
18const reqVideoCaptionAdd = createReqFiles( 17const reqVideoCaptionAdd = createReqFiles(
19 [ 'captionfile' ], 18 [ 'captionfile' ],
@@ -66,12 +65,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
66 videoCaption.Video = video 65 videoCaption.Video = video
67 66
68 // Move physical file 67 // Move physical file
69 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR 68 await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption)
70 const destination = join(videoCaptionsDir, videoCaption.getCaptionName())
71 await renamePromise(videoCaptionPhysicalFile.path, destination)
72 // This is important in case if there is another attempt in the retry process
73 videoCaptionPhysicalFile.filename = videoCaption.getCaptionName()
74 videoCaptionPhysicalFile.path = destination
75 69
76 await sequelizeTypescript.transaction(async t => { 70 await sequelizeTypescript.transaction(async t => {
77 await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, t) 71 await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, t)
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts
new file mode 100644
index 000000000..8b04f878d
--- /dev/null
+++ b/server/helpers/captions-utils.ts
@@ -0,0 +1,47 @@
1import { renamePromise, unlinkPromise } from './core-utils'
2import { join } from 'path'
3import { CONFIG } from '../initializers'
4import { VideoCaptionModel } from '../models/video/video-caption'
5import * as srt2vtt from 'srt-to-vtt'
6import { createReadStream, createWriteStream } from 'fs'
7
8async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) {
9 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
10 const destination = join(videoCaptionsDir, videoCaption.getCaptionName())
11
12 // Convert this srt file to vtt
13 if (physicalFile.path.endsWith('.srt')) {
14 await convertSrtToVtt(physicalFile.path, destination)
15 await unlinkPromise(physicalFile.path)
16 } else { // Just move the vtt file
17 await renamePromise(physicalFile.path, destination)
18 }
19
20 // This is important in case if there is another attempt in the retry process
21 physicalFile.filename = videoCaption.getCaptionName()
22 physicalFile.path = destination
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 moveAndProcessCaptionFile
29}
30
31// ---------------------------------------------------------------------------
32
33function convertSrtToVtt (source: string, destination: string) {
34 return new Promise((res, rej) => {
35 const file = createReadStream(source)
36 const converter = srt2vtt()
37 const writer = createWriteStream(destination)
38
39 for (const s of [ file, converter, writer ]) {
40 s.on('error', err => rej(err))
41 }
42
43 return file.pipe(converter)
44 .pipe(writer)
45 .on('finish', () => res())
46 })
47}
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index fd4dc740b..6a9c6d75c 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES, VIDEO_MIMETYPE_EXT } from '../../initializers'
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3import { Response } from 'express' 3import { Response } from 'express'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
@@ -8,13 +8,10 @@ function isVideoCaptionLanguageValid (value: any) {
8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined 8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined
9} 9}
10 10
11const videoCaptionTypes = CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME 11const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT).map(m => `(${m})`)
12 .map(v => v.replace('.', '')) 12const videoCaptionTypesRegex = videoCaptionTypes.join('|')
13 .join('|')
14const videoCaptionsTypesRegex = `text/(${videoCaptionTypes})`
15
16function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { 13function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
17 return isFileValid(files, videoCaptionsTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) 14 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
18} 15}
19 16
20async function isVideoCaptionExist (video: VideoModel, language: string, res: Response) { 17async function isVideoCaptionExist (video: VideoModel, language: string, res: Response) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 49809e64c..3837f7062 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -231,7 +231,7 @@ const CONSTRAINTS_FIELDS = {
231 }, 231 },
232 VIDEO_CAPTIONS: { 232 VIDEO_CAPTIONS: {
233 CAPTION_FILE: { 233 CAPTION_FILE: {
234 EXTNAME: [ '.vtt' ], 234 EXTNAME: [ '.vtt', '.srt' ],
235 FILE_SIZE: { 235 FILE_SIZE: {
236 max: 2 * 1024 * 1024 // 2MB 236 max: 2 * 1024 * 1024 // 2MB
237 } 237 }
@@ -364,7 +364,8 @@ const IMAGE_MIMETYPE_EXT = {
364} 364}
365 365
366const VIDEO_CAPTIONS_MIMETYPE_EXT = { 366const VIDEO_CAPTIONS_MIMETYPE_EXT = {
367 'text/vtt': '.vtt' 367 'text/vtt': '.vtt',
368 'application/x-subrip': '.srt'
368} 369}
369 370
370// --------------------------------------------------------------------------- 371// ---------------------------------------------------------------------------
@@ -451,9 +452,13 @@ const EMBED_SIZE = {
451 452
452// Sub folders of cache directory 453// Sub folders of cache directory
453const CACHE = { 454const CACHE = {
454 DIRECTORIES: { 455 PREVIEWS: {
455 PREVIEWS: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), 456 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
456 VIDEO_CAPTIONS: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions') 457 MAX_AGE: 1000 * 3600 * 3 // 3 hours
458 },
459 VIDEO_CAPTIONS: {
460 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'),
461 MAX_AGE: 1000 * 3600 * 3 // 3 hours
457 } 462 }
458} 463}
459 464
@@ -500,6 +505,8 @@ if (isTestInstance() === true) {
500 VIDEO_VIEW_LIFETIME = 1000 // 1 second 505 VIDEO_VIEW_LIFETIME = 1000 // 1 second
501 506
502 JOB_ATTEMPTS['email'] = 1 507 JOB_ATTEMPTS['email'] = 1
508
509 CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
503} 510}
504 511
505updateWebserverConfig() 512updateWebserverConfig()
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index b0084b368..1f513a9c3 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -33,7 +33,8 @@ export {
33// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
34 34
35function removeCacheDirectories () { 35function removeCacheDirectories () {
36 const cacheDirectories = CACHE.DIRECTORIES 36 const cacheDirectories = Object.keys(CACHE)
37 .map(k => CACHE[k].DIRECTORY)
37 38
38 const tasks: Promise<any>[] = [] 39 const tasks: Promise<any>[] = []
39 40
@@ -48,7 +49,8 @@ function removeCacheDirectories () {
48 49
49function createDirectoriesIfNotExist () { 50function createDirectoriesIfNotExist () {
50 const storage = CONFIG.STORAGE 51 const storage = CONFIG.STORAGE
51 const cacheDirectories = CACHE.DIRECTORIES 52 const cacheDirectories = Object.keys(CACHE)
53 .map(k => CACHE[k].DIRECTORY)
52 54
53 const tasks = [] 55 const tasks = []
54 for (const key of Object.keys(storage)) { 56 for (const key of Object.keys(storage)) {
diff --git a/server/lib/cache/abstract-video-static-file-cache.ts b/server/lib/cache/abstract-video-static-file-cache.ts
index 7eeeb6b3a..8e895cc82 100644
--- a/server/lib/cache/abstract-video-static-file-cache.ts
+++ b/server/lib/cache/abstract-video-static-file-cache.ts
@@ -1,12 +1,9 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import { createWriteStream } from 'fs' 2import { createWriteStream } from 'fs'
3import { join } from 'path'
4import { unlinkPromise } from '../../helpers/core-utils' 3import { unlinkPromise } from '../../helpers/core-utils'
5import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
6import { CACHE, CONFIG } from '../../initializers'
7import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
8import { fetchRemoteVideoStaticFile } from '../activitypub' 6import { fetchRemoteVideoStaticFile } from '../activitypub'
9import { VideoCaptionModel } from '../../models/video/video-caption'
10 7
11export abstract class AbstractVideoStaticFileCache <T> { 8export abstract class AbstractVideoStaticFileCache <T> {
12 9
@@ -17,9 +14,10 @@ export abstract class AbstractVideoStaticFileCache <T> {
17 // Load and save the remote file, then return the local path from filesystem 14 // Load and save the remote file, then return the local path from filesystem
18 protected abstract loadRemoteFile (key: string): Promise<string> 15 protected abstract loadRemoteFile (key: string): Promise<string>
19 16
20 init (max: number) { 17 init (max: number, maxAge: number) {
21 this.lru = new AsyncLRU({ 18 this.lru = new AsyncLRU({
22 max, 19 max,
20 maxAge,
23 load: (key, cb) => { 21 load: (key, cb) => {
24 this.loadRemoteFile(key) 22 this.loadRemoteFile(key)
25 .then(res => cb(null, res)) 23 .then(res => cb(null, res))
@@ -28,7 +26,8 @@ export abstract class AbstractVideoStaticFileCache <T> {
28 }) 26 })
29 27
30 this.lru.on('evict', (obj: { key: string, value: string }) => { 28 this.lru.on('evict', (obj: { key: string, value: string }) => {
31 unlinkPromise(obj.value).then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) 29 unlinkPromise(obj.value)
30 .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name))
32 }) 31 })
33 } 32 }
34 33
diff --git a/server/lib/cache/videos-caption-cache.ts b/server/lib/cache/videos-caption-cache.ts
index 1336610b2..380d42b2c 100644
--- a/server/lib/cache/videos-caption-cache.ts
+++ b/server/lib/cache/videos-caption-cache.ts
@@ -42,7 +42,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
42 if (!video) return undefined 42 if (!video) return undefined
43 43
44 const remoteStaticPath = videoCaption.getCaptionStaticPath() 44 const remoteStaticPath = videoCaption.getCaptionStaticPath()
45 const destPath = join(CACHE.DIRECTORIES.VIDEO_CAPTIONS, videoCaption.getCaptionName()) 45 const destPath = join(CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
46 46
47 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 47 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
48 } 48 }
diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/cache/videos-preview-cache.ts
index 1c0e7ed9d..22b6d9cb0 100644
--- a/server/lib/cache/videos-preview-cache.ts
+++ b/server/lib/cache/videos-preview-cache.ts
@@ -31,7 +31,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
31 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') 31 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
32 32
33 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) 33 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
34 const destPath = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) 34 const destPath = join(CACHE.PREVIEWS.DIRECTORY, video.getPreviewName())
35 35
36 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 36 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
37 } 37 }
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 9920dfc7c..5a1becc47 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -75,14 +75,18 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
75 75
76 @BeforeDestroy 76 @BeforeDestroy
77 static async removeFiles (instance: VideoCaptionModel) { 77 static async removeFiles (instance: VideoCaptionModel) {
78 if (!instance.Video) {
79 instance.Video = await instance.$get('Video') as VideoModel
80 }
78 81
79 if (instance.isOwned()) { 82 if (instance.isOwned()) {
80 if (!instance.Video) {
81 instance.Video = await instance.$get('Video') as VideoModel
82 }
83
84 logger.debug('Removing captions %s of video %s.', instance.Video.uuid, instance.language) 83 logger.debug('Removing captions %s of video %s.', instance.Video.uuid, instance.language)
85 return instance.removeCaptionFile() 84
85 try {
86 await instance.removeCaptionFile()
87 } catch (err) {
88 logger.error('Cannot remove caption file of video %s.', instance.Video.uuid)
89 }
86 } 90 }
87 91
88 return undefined 92 return undefined
diff --git a/server/tests/api/check-params/video-captions.ts b/server/tests/api/check-params/video-captions.ts
index 12f890db8..a3d7ac35d 100644
--- a/server/tests/api/check-params/video-captions.ts
+++ b/server/tests/api/check-params/video-captions.ts
@@ -1,6 +1,5 @@
1/* tslint:disable:no-unused-expression */ 1/* tslint:disable:no-unused-expression */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { 4import {
6 createUser, 5 createUser,
@@ -127,6 +126,40 @@ describe('Test video captions API validator', function () {
127 }) 126 })
128 }) 127 })
129 128
129 it('Should fail with an invalid captionfile extension', async function () {
130 const attaches = {
131 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.txt')
132 }
133
134 const captionPath = path + videoUUID + '/captions/fr'
135 await makeUploadRequest({
136 method: 'PUT',
137 url: server.url,
138 path: captionPath,
139 token: server.accessToken,
140 fields,
141 attaches,
142 statusCodeExpected: 400
143 })
144 })
145
146 // it('Should fail with an invalid captionfile srt', async function () {
147 // const attaches = {
148 // 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.srt')
149 // }
150 //
151 // const captionPath = path + videoUUID + '/captions/fr'
152 // await makeUploadRequest({
153 // method: 'PUT',
154 // url: server.url,
155 // path: captionPath,
156 // token: server.accessToken,
157 // fields,
158 // attaches,
159 // statusCodeExpected: 500
160 // })
161 // })
162
130 it('Should success with the correct parameters', async function () { 163 it('Should success with the correct parameters', async function () {
131 const captionPath = path + videoUUID + '/captions/fr' 164 const captionPath = path + videoUUID + '/captions/fr'
132 await makeUploadRequest({ 165 await makeUploadRequest({
diff --git a/server/tests/api/videos/video-captions.ts b/server/tests/api/videos/video-captions.ts
index cbf5268f0..eb73c5baf 100644
--- a/server/tests/api/videos/video-captions.ts
+++ b/server/tests/api/videos/video-captions.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { doubleFollow, flushAndRunMultipleServers, uploadVideo } from '../../utils' 5import { checkVideoFilesWereRemoved, doubleFollow, flushAndRunMultipleServers, removeVideo, uploadVideo, wait } from '../../utils'
6import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' 6import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
7import { waitJobs } from '../../utils/server/jobs' 7import { waitJobs } from '../../utils/server/jobs'
8import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' 8import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions'
@@ -110,6 +110,51 @@ describe('Test video captions', function () {
110 } 110 }
111 }) 111 })
112 112
113 it('Should replace an existing caption with a srt file and convert it', async function () {
114 this.timeout(30000)
115
116 await createVideoCaption({
117 url: servers[0].url,
118 accessToken: servers[0].accessToken,
119 language: 'ar',
120 videoId: videoUUID,
121 fixture: 'subtitle-good.srt'
122 })
123
124 await waitJobs(servers)
125
126 // Cache invalidation
127 await wait(3000)
128 })
129
130 it('Should have this caption updated and converted', async function () {
131 for (const server of servers) {
132 const res = await listVideoCaptions(server.url, videoUUID)
133 expect(res.body.total).to.equal(2)
134 expect(res.body.data).to.have.lengthOf(2)
135
136 const caption1: VideoCaption = res.body.data[0]
137 expect(caption1.language.id).to.equal('ar')
138 expect(caption1.language.label).to.equal('Arabic')
139 expect(caption1.captionPath).to.equal('/static/video-captions/' + videoUUID + '-ar.vtt')
140
141 const expected = 'WEBVTT FILE\r\n' +
142 '\r\n' +
143 '1\r\n' +
144 '00:00:01.600 --> 00:00:04.200\r\n' +
145 'English (US)\r\n' +
146 '\r\n' +
147 '2\r\n' +
148 '00:00:05.900 --> 00:00:07.999\r\n' +
149 'This is a subtitle in American English\r\n' +
150 '\r\n' +
151 '3\r\n' +
152 '00:00:10.000 --> 00:00:14.000\r\n' +
153 'Adding subtitles is very easy to do\r\n'
154 await testCaptionFile(server.url, caption1.captionPath, expected)
155 }
156 })
157
113 it('Should remove one caption', async function () { 158 it('Should remove one caption', async function () {
114 this.timeout(30000) 159 this.timeout(30000)
115 160
@@ -133,6 +178,12 @@ describe('Test video captions', function () {
133 } 178 }
134 }) 179 })
135 180
181 it('Should remove the video, and thus all video captions', async function () {
182 await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
183
184 await checkVideoFilesWereRemoved(videoUUID, 1)
185 })
186
136 after(async function () { 187 after(async function () {
137 killallServers(servers) 188 killallServers(servers)
138 }) 189 })
diff --git a/server/tests/fixtures/subtitle-bad.txt b/server/tests/fixtures/subtitle-bad.txt
new file mode 100644
index 000000000..a2a30ae47
--- /dev/null
+++ b/server/tests/fixtures/subtitle-bad.txt
@@ -0,0 +1,11 @@
11
200:00:01,600 --> 00:00:04,200
3English (US)
4
52
600:00:05,900 --> 00:00:07,999
7This is a subtitle in American English
8
93
1000:00:10,000 --> 00:00:14,000
11Adding subtitles is very easy to do \ No newline at end of file
diff --git a/server/tests/fixtures/subtitle-good.srt b/server/tests/fixtures/subtitle-good.srt
new file mode 100644
index 000000000..a2a30ae47
--- /dev/null
+++ b/server/tests/fixtures/subtitle-good.srt
@@ -0,0 +1,11 @@
11
200:00:01,600 --> 00:00:04,200
3English (US)
4
52
600:00:05,900 --> 00:00:07,999
7This is a subtitle in American English
8
93
1000:00:10,000 --> 00:00:14,000
11Adding subtitles is very easy to do \ No newline at end of file
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index 4f7ce6d6b..74bf7354e 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -301,7 +301,7 @@ function searchVideoWithSort (url: string, search: string, sort: string) {
301async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) { 301async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) {
302 const testDirectory = 'test' + serverNumber 302 const testDirectory = 'test' + serverNumber
303 303
304 for (const directory of [ 'videos', 'thumbnails', 'torrents', 'previews' ]) { 304 for (const directory of [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ]) {
305 const directoryPath = join(root(), testDirectory, directory) 305 const directoryPath = join(root(), testDirectory, directory)
306 306
307 const directoryExists = existsSync(directoryPath) 307 const directoryExists = existsSync(directoryPath)
diff --git a/yarn.lock b/yarn.lock
index 27e1365a3..2949f5989 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1166,6 +1166,10 @@ charenc@~0.0.1:
1166 version "0.0.2" 1166 version "0.0.2"
1167 resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 1167 resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
1168 1168
1169charset-detector@0.0.2:
1170 version "0.0.2"
1171 resolved "https://registry.yarnpkg.com/charset-detector/-/charset-detector-0.0.2.tgz#1cd5ddaf56e83259c6ef8e906ccf06f75fe9a1b2"
1172
1169check-error@^1.0.1: 1173check-error@^1.0.1:
1170 version "1.0.2" 1174 version "1.0.2"
1171 resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" 1175 resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@@ -1945,6 +1949,15 @@ duplexer@^0.1.1, duplexer@~0.1.1:
1945 version "0.1.1" 1949 version "0.1.1"
1946 resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" 1950 resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
1947 1951
1952duplexify@^3.2.0, duplexify@^3.5.0, duplexify@^3.6.0:
1953 version "3.6.0"
1954 resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410"
1955 dependencies:
1956 end-of-stream "^1.0.0"
1957 inherits "^2.0.1"
1958 readable-stream "^2.0.0"
1959 stream-shift "^1.0.0"
1960
1948each-async@^1.0.0: 1961each-async@^1.0.0:
1949 version "1.1.1" 1962 version "1.1.1"
1950 resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473" 1963 resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473"
@@ -3751,7 +3764,7 @@ is-windows@^1.0.2:
3751 version "1.0.2" 3764 version "1.0.2"
3752 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" 3765 resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
3753 3766
3754isarray@0.0.1: 3767isarray@0.0.1, isarray@~0.0.1:
3755 version "0.0.1" 3768 version "0.0.1"
3756 resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 3769 resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
3757 3770
@@ -5382,6 +5395,14 @@ pause-stream@0.0.11:
5382 dependencies: 5395 dependencies:
5383 through "~2.3" 5396 through "~2.3"
5384 5397
5398peek-stream@^1.1.1:
5399 version "1.1.3"
5400 resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67"
5401 dependencies:
5402 buffer-from "^1.0.0"
5403 duplexify "^3.5.0"
5404 through2 "^2.0.3"
5405
5385pem@^1.12.3: 5406pem@^1.12.3:
5386 version "1.12.5" 5407 version "1.12.5"
5387 resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.5.tgz#97bf2e459537c54e0ee5b0aa11b5ca18d6b5fef2" 5408 resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.5.tgz#97bf2e459537c54e0ee5b0aa11b5ca18d6b5fef2"
@@ -5655,7 +5676,7 @@ pump@^1.0.0, pump@^1.0.1:
5655 end-of-stream "^1.1.0" 5676 end-of-stream "^1.1.0"
5656 once "^1.3.1" 5677 once "^1.3.1"
5657 5678
5658pump@^2.0.1: 5679pump@^2.0.0, pump@^2.0.1:
5659 version "2.0.1" 5680 version "2.0.1"
5660 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" 5681 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
5661 dependencies: 5682 dependencies:
@@ -5669,6 +5690,14 @@ pump@^3.0.0:
5669 end-of-stream "^1.1.0" 5690 end-of-stream "^1.1.0"
5670 once "^1.3.1" 5691 once "^1.3.1"
5671 5692
5693pumpify@^1.3.3:
5694 version "1.5.1"
5695 resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
5696 dependencies:
5697 duplexify "^3.6.0"
5698 inherits "^2.0.3"
5699 pump "^2.0.0"
5700
5672punycode@^1.4.1: 5701punycode@^1.4.1:
5673 version "1.4.1" 5702 version "1.4.1"
5674 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 5703 resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@@ -5813,7 +5842,7 @@ readable-stream@1.1:
5813 isarray "0.0.1" 5842 isarray "0.0.1"
5814 string_decoder "~0.10.x" 5843 string_decoder "~0.10.x"
5815 5844
5816readable-stream@1.1.x: 5845readable-stream@1.1.x, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.1.13-1:
5817 version "1.1.14" 5846 version "1.1.14"
5818 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 5847 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
5819 dependencies: 5848 dependencies:
@@ -5822,7 +5851,16 @@ readable-stream@1.1.x:
5822 isarray "0.0.1" 5851 isarray "0.0.1"
5823 string_decoder "~0.10.x" 5852 string_decoder "~0.10.x"
5824 5853
5825readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.2, readable-stream@^2.3.4, readable-stream@^2.3.5, readable-stream@^2.3.6: 5854"readable-stream@>=1.0.33-1 <1.1.0-0":
5855 version "1.0.34"
5856 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
5857 dependencies:
5858 core-util-is "~1.0.0"
5859 inherits "~2.0.1"
5860 isarray "0.0.1"
5861 string_decoder "~0.10.x"
5862
5863readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.2, readable-stream@^2.3.4, readable-stream@^2.3.5, readable-stream@^2.3.6:
5826 version "2.3.6" 5864 version "2.3.6"
5827 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 5865 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
5828 dependencies: 5866 dependencies:
@@ -5834,6 +5872,12 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable
5834 string_decoder "~1.1.1" 5872 string_decoder "~1.1.1"
5835 util-deprecate "~1.0.1" 5873 util-deprecate "~1.0.1"
5836 5874
5875readable-wrap@^1.0.0:
5876 version "1.0.0"
5877 resolved "https://registry.yarnpkg.com/readable-wrap/-/readable-wrap-1.0.0.tgz#3b5a211c631e12303a54991c806c17e7ae206bff"
5878 dependencies:
5879 readable-stream "^1.1.13-1"
5880
5837readdirp@^2.0.0: 5881readdirp@^2.0.0:
5838 version "2.1.0" 5882 version "2.1.0"
5839 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" 5883 resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
@@ -6677,6 +6721,12 @@ split-string@^3.0.1, split-string@^3.0.2:
6677 dependencies: 6721 dependencies:
6678 extend-shallow "^3.0.0" 6722 extend-shallow "^3.0.0"
6679 6723
6724split2@^0.2.1:
6725 version "0.2.1"
6726 resolved "https://registry.yarnpkg.com/split2/-/split2-0.2.1.tgz#02ddac9adc03ec0bb78c1282ec079ca6e85ae900"
6727 dependencies:
6728 through2 "~0.6.1"
6729
6680split@0.3: 6730split@0.3:
6681 version "0.3.3" 6731 version "0.3.3"
6682 resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" 6732 resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
@@ -6693,6 +6743,17 @@ sprintf-js@~1.0.2:
6693 version "1.0.3" 6743 version "1.0.3"
6694 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 6744 resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
6695 6745
6746srt-to-vtt@^1.1.2:
6747 version "1.1.2"
6748 resolved "https://registry.yarnpkg.com/srt-to-vtt/-/srt-to-vtt-1.1.2.tgz#634c5228b34f2b5fb410cd4eaab5accbb09780d6"
6749 dependencies:
6750 duplexify "^3.2.0"
6751 minimist "^1.1.0"
6752 pumpify "^1.3.3"
6753 split2 "^0.2.1"
6754 through2 "^0.6.3"
6755 to-utf-8 "^1.2.0"
6756
6696sshpk@^1.7.0: 6757sshpk@^1.7.0:
6697 version "1.14.2" 6758 version "1.14.2"
6698 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" 6759 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"
@@ -6755,6 +6816,21 @@ stream-combiner@~0.0.4:
6755 dependencies: 6816 dependencies:
6756 duplexer "~0.1.1" 6817 duplexer "~0.1.1"
6757 6818
6819stream-shift@^1.0.0:
6820 version "1.0.0"
6821 resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
6822
6823stream-splicer@^1.3.1:
6824 version "1.3.2"
6825 resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-1.3.2.tgz#3c0441be15b9bf4e226275e6dc83964745546661"
6826 dependencies:
6827 indexof "0.0.1"
6828 inherits "^2.0.1"
6829 isarray "~0.0.1"
6830 readable-stream "^1.1.13-1"
6831 readable-wrap "^1.0.0"
6832 through2 "^1.0.0"
6833
6758stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.0: 6834stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.0:
6759 version "2.1.1" 6835 version "2.1.1"
6760 resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2" 6836 resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
@@ -7042,6 +7118,27 @@ thirty-two@^1.0.1:
7042 version "1.0.2" 7118 version "1.0.2"
7043 resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" 7119 resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
7044 7120
7121through2@^0.6.3, through2@~0.6.1:
7122 version "0.6.5"
7123 resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
7124 dependencies:
7125 readable-stream ">=1.0.33-1 <1.1.0-0"
7126 xtend ">=4.0.0 <4.1.0-0"
7127
7128through2@^1.0.0:
7129 version "1.1.1"
7130 resolved "https://registry.yarnpkg.com/through2/-/through2-1.1.1.tgz#0847cbc4449f3405574dbdccd9bb841b83ac3545"
7131 dependencies:
7132 readable-stream ">=1.1.13-1 <1.2.0-0"
7133 xtend ">=4.0.0 <4.1.0-0"
7134
7135through2@^2.0.3:
7136 version "2.0.3"
7137 resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
7138 dependencies:
7139 readable-stream "^2.1.5"
7140 xtend "~4.0.1"
7141
7045through@2, through@^2.3.6, through@~2.3, through@~2.3.1: 7142through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
7046 version "2.3.8" 7143 version "2.3.8"
7047 resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 7144 resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -7103,6 +7200,16 @@ to-regex@^3.0.1, to-regex@^3.0.2:
7103 regex-not "^1.0.2" 7200 regex-not "^1.0.2"
7104 safe-regex "^1.1.0" 7201 safe-regex "^1.1.0"
7105 7202
7203to-utf-8@^1.2.0:
7204 version "1.3.0"
7205 resolved "https://registry.yarnpkg.com/to-utf-8/-/to-utf-8-1.3.0.tgz#b2af7be9e003f4c3817cc116d3baed2a054993c9"
7206 dependencies:
7207 charset-detector "0.0.2"
7208 iconv-lite "^0.4.4"
7209 minimist "^1.1.0"
7210 peek-stream "^1.1.1"
7211 stream-splicer "^1.3.1"
7212
7106toposort-class@^1.0.1: 7213toposort-class@^1.0.1:
7107 version "1.0.1" 7214 version "1.0.1"
7108 resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" 7215 resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"
@@ -7774,7 +7881,7 @@ xmlhttprequest-ssl@1.5.3:
7774 version "1.5.3" 7881 version "1.5.3"
7775 resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" 7882 resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"
7776 7883
7777xtend@^4.0.0, xtend@^4.0.1: 7884"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
7778 version "4.0.1" 7885 version "4.0.1"
7779 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 7886 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
7780 7887