diff options
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", |
@@ -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) |
28 | import { logger } from './server/helpers/logger' | 28 | import { logger } from './server/helpers/logger' |
29 | import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants' | 29 | import { API_VERSION, CONFIG, STATIC_PATHS, CACHE } from './server/initializers/constants' |
30 | 30 | ||
31 | const missed = checkMissedConfig() | 31 | const missed = checkMissedConfig() |
32 | if (missed.length !== 0) { | 32 | if (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' | |||
9 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' | 9 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' |
10 | import { getFormattedObjects } from '../../../helpers/utils' | 10 | import { getFormattedObjects } from '../../../helpers/utils' |
11 | import { VideoCaptionModel } from '../../../models/video/video-caption' | 11 | import { VideoCaptionModel } from '../../../models/video/video-caption' |
12 | import { renamePromise } from '../../../helpers/core-utils' | ||
13 | import { join } from 'path' | ||
14 | import { VideoModel } from '../../../models/video/video' | 12 | import { VideoModel } from '../../../models/video/video' |
15 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
16 | import { federateVideoIfNeeded } from '../../../lib/activitypub' | 14 | import { federateVideoIfNeeded } from '../../../lib/activitypub' |
15 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | ||
17 | 16 | ||
18 | const reqVideoCaptionAdd = createReqFiles( | 17 | const 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 @@ | |||
1 | import { renamePromise, unlinkPromise } from './core-utils' | ||
2 | import { join } from 'path' | ||
3 | import { CONFIG } from '../initializers' | ||
4 | import { VideoCaptionModel } from '../models/video/video-caption' | ||
5 | import * as srt2vtt from 'srt-to-vtt' | ||
6 | import { createReadStream, createWriteStream } from 'fs' | ||
7 | |||
8 | async 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 | |||
27 | export { | ||
28 | moveAndProcessCaptionFile | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
33 | function 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 @@ | |||
1 | import { CONSTRAINTS_FIELDS, VIDEO_LANGUAGES } from '../../initializers' | 1 | import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES, VIDEO_MIMETYPE_EXT } from '../../initializers' |
2 | import { exists, isFileValid } from './misc' | 2 | import { exists, isFileValid } from './misc' |
3 | import { Response } from 'express' | 3 | import { Response } from 'express' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { 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 | ||
11 | const videoCaptionTypes = CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | 11 | const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT).map(m => `(${m})`) |
12 | .map(v => v.replace('.', '')) | 12 | const videoCaptionTypesRegex = videoCaptionTypes.join('|') |
13 | .join('|') | ||
14 | const videoCaptionsTypesRegex = `text/(${videoCaptionTypes})` | ||
15 | |||
16 | function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { | 13 | function 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 | ||
20 | async function isVideoCaptionExist (video: VideoModel, language: string, res: Response) { | 17 | async 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 | ||
366 | const VIDEO_CAPTIONS_MIMETYPE_EXT = { | 366 | const 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 |
453 | const CACHE = { | 454 | const 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 | ||
505 | updateWebserverConfig() | 512 | updateWebserverConfig() |
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 | ||
35 | function removeCacheDirectories () { | 35 | function 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 | ||
49 | function createDirectoriesIfNotExist () { | 50 | function 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 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | 1 | import * as AsyncLRU from 'async-lru' |
2 | import { createWriteStream } from 'fs' | 2 | import { createWriteStream } from 'fs' |
3 | import { join } from 'path' | ||
4 | import { unlinkPromise } from '../../helpers/core-utils' | 3 | import { unlinkPromise } from '../../helpers/core-utils' |
5 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
6 | import { CACHE, CONFIG } from '../../initializers' | ||
7 | import { VideoModel } from '../../models/video/video' | 5 | import { VideoModel } from '../../models/video/video' |
8 | import { fetchRemoteVideoStaticFile } from '../activitypub' | 6 | import { fetchRemoteVideoStaticFile } from '../activitypub' |
9 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
10 | 7 | ||
11 | export abstract class AbstractVideoStaticFileCache <T> { | 8 | export 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 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { | 4 | import { |
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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { doubleFollow, flushAndRunMultipleServers, uploadVideo } from '../../utils' | 5 | import { checkVideoFilesWereRemoved, doubleFollow, flushAndRunMultipleServers, removeVideo, uploadVideo, wait } from '../../utils' |
6 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 6 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' |
7 | import { waitJobs } from '../../utils/server/jobs' | 7 | import { waitJobs } from '../../utils/server/jobs' |
8 | import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' | 8 | import { 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 @@ | |||
1 | 1 | ||
2 | 00:00:01,600 --> 00:00:04,200 | ||
3 | English (US) | ||
4 | |||
5 | 2 | ||
6 | 00:00:05,900 --> 00:00:07,999 | ||
7 | This is a subtitle in American English | ||
8 | |||
9 | 3 | ||
10 | 00:00:10,000 --> 00:00:14,000 | ||
11 | Adding 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 @@ | |||
1 | 1 | ||
2 | 00:00:01,600 --> 00:00:04,200 | ||
3 | English (US) | ||
4 | |||
5 | 2 | ||
6 | 00:00:05,900 --> 00:00:07,999 | ||
7 | This is a subtitle in American English | ||
8 | |||
9 | 3 | ||
10 | 00:00:10,000 --> 00:00:14,000 | ||
11 | Adding 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) { | |||
301 | async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) { | 301 | async 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) |
@@ -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 | ||
1169 | charset-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 | |||
1169 | check-error@^1.0.1: | 1173 | check-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 | ||
1952 | duplexify@^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 | |||
1948 | each-async@^1.0.0: | 1961 | each-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 | ||
3754 | isarray@0.0.1: | 3767 | isarray@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 | ||
5398 | peek-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 | |||
5385 | pem@^1.12.3: | 5406 | pem@^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 | ||
5658 | pump@^2.0.1: | 5679 | pump@^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 | ||
5693 | pumpify@^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 | |||
5672 | punycode@^1.4.1: | 5701 | punycode@^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 | ||
5816 | readable-stream@1.1.x: | 5845 | readable-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 | ||
5825 | 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.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 | |||
5863 | readable-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 | ||
5875 | readable-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 | |||
5837 | readdirp@^2.0.0: | 5881 | readdirp@^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 | ||
6724 | split2@^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 | |||
6680 | split@0.3: | 6730 | split@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 | ||
6746 | srt-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 | |||
6696 | sshpk@^1.7.0: | 6757 | sshpk@^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 | ||
6819 | stream-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 | |||
6823 | stream-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 | |||
6758 | stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.0: | 6834 | stream-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 | ||
7121 | through2@^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 | |||
7128 | through2@^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 | |||
7135 | through2@^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 | |||
7045 | through@2, through@^2.3.6, through@~2.3, through@~2.3.1: | 7142 | through@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 | ||
7203 | to-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 | |||
7106 | toposort-class@^1.0.1: | 7213 | toposort-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 | ||
7777 | xtend@^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 | ||