From f4001cf408a99049d01a356bfb20a62342de06ea Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 16 Jul 2018 14:22:16 +0200 Subject: [PATCH] Handle .srt subtitles --- .../video-caption-add-modal.component.ts | 13 +- .../shared/video-edit.component.html | 4 +- .../shared/video-edit.component.ts | 18 ++- package.json | 1 + server.ts | 6 +- server/controllers/api/videos/captions.ts | 10 +- server/helpers/captions-utils.ts | 47 +++++++ .../custom-validators/video-captions.ts | 11 +- server/initializers/constants.ts | 17 ++- server/initializers/installer.ts | 6 +- .../cache/abstract-video-static-file-cache.ts | 9 +- server/lib/cache/videos-caption-cache.ts | 2 +- server/lib/cache/videos-preview-cache.ts | 2 +- server/models/video/video-caption.ts | 14 ++- .../tests/api/check-params/video-captions.ts | 35 +++++- server/tests/api/videos/video-captions.ts | 53 +++++++- server/tests/fixtures/subtitle-bad.txt | 11 ++ server/tests/fixtures/subtitle-good.srt | 11 ++ server/tests/utils/videos/videos.ts | 2 +- yarn.lock | 117 +++++++++++++++++- 20 files changed, 336 insertions(+), 53 deletions(-) create mode 100644 server/helpers/captions-utils.ts create mode 100644 server/tests/fixtures/subtitle-bad.txt create mode 100644 server/tests/fixtures/subtitle-good.srt 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 } show () { + this.closingModal = false + this.modal.show() } hide () { + this.closingModal = true + this.modal.hide() } @@ -65,7 +69,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni } async addCaption () { - this.closingModal = true + this.hide() const languageId = this.form.value[ 'language' ] const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) @@ -74,7 +78,12 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni language: languageObject, captionfile: this.form.value['captionfile'] }) + // + // this.form.patchValue({ + // language: null, + // captionfile: null + // }) - this.hide() + this.form.reset() } } 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 @@
-
+
Delete @@ -200,5 +200,5 @@
\ 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 { this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() } + get existingCaptions () { + return this.videoCaptions + .filter(c => c.action !== 'REMOVE') + .map(c => c.language.id) + } + updateForm () { const defaultValues = { nsfw: 'false', @@ -126,11 +132,15 @@ export class VideoEditComponent implements OnInit, OnDestroy { if (this.schedulerInterval) clearInterval(this.schedulerInterval) } - getExistingCaptions () { - return this.videoCaptions.map(c => c.language.id) - } - onCaptionAdded (caption: VideoCaptionEdit) { + const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id) + + // Replace existing caption? + if (existingCaption) { + Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' }) + return + } + this.videoCaptions.push( Object.assign(caption, { action: 'CREATE' as 'CREATE' }) ) diff --git a/package.json b/package.json index 96b082363..586db76f4 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "sequelize": "4.38.0", "sequelize-typescript": "0.6.6-beta.1", "sharp": "^0.20.0", + "srt-to-vtt": "^1.1.2", "uuid": "^3.1.0", "validator": "^10.2.0", "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 // Do not use barrels because we don't want to load all modules here (we need to initialize database first) import { logger } from './server/helpers/logger' -import { API_VERSION, CONFIG, STATIC_PATHS } from './server/initializers/constants' +import { API_VERSION, CONFIG, STATIC_PATHS, CACHE } from './server/initializers/constants' const missed = checkMissedConfig() if (missed.length !== 0) { @@ -182,8 +182,8 @@ async function startApplication () { await JobQueue.Instance.init() // Caches initializations - VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE) - VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE) + VideosPreviewCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, CACHE.PREVIEWS.MAX_AGE) + VideosCaptionCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, CACHE.VIDEO_CAPTIONS.MAX_AGE) // Enable Schedulers 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' import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' import { getFormattedObjects } from '../../../helpers/utils' import { VideoCaptionModel } from '../../../models/video/video-caption' -import { renamePromise } from '../../../helpers/core-utils' -import { join } from 'path' import { VideoModel } from '../../../models/video/video' import { logger } from '../../../helpers/logger' import { federateVideoIfNeeded } from '../../../lib/activitypub' +import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' const reqVideoCaptionAdd = createReqFiles( [ 'captionfile' ], @@ -66,12 +65,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) { videoCaption.Video = video // Move physical file - const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR - const destination = join(videoCaptionsDir, videoCaption.getCaptionName()) - await renamePromise(videoCaptionPhysicalFile.path, destination) - // This is important in case if there is another attempt in the retry process - videoCaptionPhysicalFile.filename = videoCaption.getCaptionName() - videoCaptionPhysicalFile.path = destination + await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption) await sequelizeTypescript.transaction(async t => { 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 @@ +import { renamePromise, unlinkPromise } from './core-utils' +import { join } from 'path' +import { CONFIG } from '../initializers' +import { VideoCaptionModel } from '../models/video/video-caption' +import * as srt2vtt from 'srt-to-vtt' +import { createReadStream, createWriteStream } from 'fs' + +async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { + const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR + const destination = join(videoCaptionsDir, videoCaption.getCaptionName()) + + // Convert this srt file to vtt + if (physicalFile.path.endsWith('.srt')) { + await convertSrtToVtt(physicalFile.path, destination) + await unlinkPromise(physicalFile.path) + } else { // Just move the vtt file + await renamePromise(physicalFile.path, destination) + } + + // This is important in case if there is another attempt in the retry process + physicalFile.filename = videoCaption.getCaptionName() + physicalFile.path = destination +} + +// --------------------------------------------------------------------------- + +export { + moveAndProcessCaptionFile +} + +// --------------------------------------------------------------------------- + +function convertSrtToVtt (source: string, destination: string) { + return new Promise((res, rej) => { + const file = createReadStream(source) + const converter = srt2vtt() + const writer = createWriteStream(destination) + + for (const s of [ file, converter, writer ]) { + s.on('error', err => rej(err)) + } + + return file.pipe(converter) + .pipe(writer) + .on('finish', () => res()) + }) +} 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 @@ -import { CONSTRAINTS_FIELDS, VIDEO_LANGUAGES } from '../../initializers' +import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES, VIDEO_MIMETYPE_EXT } from '../../initializers' import { exists, isFileValid } from './misc' import { Response } from 'express' import { VideoModel } from '../../models/video/video' @@ -8,13 +8,10 @@ function isVideoCaptionLanguageValid (value: any) { return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined } -const videoCaptionTypes = CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME - .map(v => v.replace('.', '')) - .join('|') -const videoCaptionsTypesRegex = `text/(${videoCaptionTypes})` - +const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT).map(m => `(${m})`) +const videoCaptionTypesRegex = videoCaptionTypes.join('|') function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { - return isFileValid(files, videoCaptionsTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) + return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) } 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 = { }, VIDEO_CAPTIONS: { CAPTION_FILE: { - EXTNAME: [ '.vtt' ], + EXTNAME: [ '.vtt', '.srt' ], FILE_SIZE: { max: 2 * 1024 * 1024 // 2MB } @@ -364,7 +364,8 @@ const IMAGE_MIMETYPE_EXT = { } const VIDEO_CAPTIONS_MIMETYPE_EXT = { - 'text/vtt': '.vtt' + 'text/vtt': '.vtt', + 'application/x-subrip': '.srt' } // --------------------------------------------------------------------------- @@ -451,9 +452,13 @@ const EMBED_SIZE = { // Sub folders of cache directory const CACHE = { - DIRECTORIES: { - PREVIEWS: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), - VIDEO_CAPTIONS: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions') + PREVIEWS: { + DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), + MAX_AGE: 1000 * 3600 * 3 // 3 hours + }, + VIDEO_CAPTIONS: { + DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'), + MAX_AGE: 1000 * 3600 * 3 // 3 hours } } @@ -500,6 +505,8 @@ if (isTestInstance() === true) { VIDEO_VIEW_LIFETIME = 1000 // 1 second JOB_ATTEMPTS['email'] = 1 + + CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 } 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 { // --------------------------------------------------------------------------- function removeCacheDirectories () { - const cacheDirectories = CACHE.DIRECTORIES + const cacheDirectories = Object.keys(CACHE) + .map(k => CACHE[k].DIRECTORY) const tasks: Promise[] = [] @@ -48,7 +49,8 @@ function removeCacheDirectories () { function createDirectoriesIfNotExist () { const storage = CONFIG.STORAGE - const cacheDirectories = CACHE.DIRECTORIES + const cacheDirectories = Object.keys(CACHE) + .map(k => CACHE[k].DIRECTORY) const tasks = [] 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 @@ import * as AsyncLRU from 'async-lru' import { createWriteStream } from 'fs' -import { join } from 'path' import { unlinkPromise } from '../../helpers/core-utils' import { logger } from '../../helpers/logger' -import { CACHE, CONFIG } from '../../initializers' import { VideoModel } from '../../models/video/video' import { fetchRemoteVideoStaticFile } from '../activitypub' -import { VideoCaptionModel } from '../../models/video/video-caption' export abstract class AbstractVideoStaticFileCache { @@ -17,9 +14,10 @@ export abstract class AbstractVideoStaticFileCache { // Load and save the remote file, then return the local path from filesystem protected abstract loadRemoteFile (key: string): Promise - init (max: number) { + init (max: number, maxAge: number) { this.lru = new AsyncLRU({ max, + maxAge, load: (key, cb) => { this.loadRemoteFile(key) .then(res => cb(null, res)) @@ -28,7 +26,8 @@ export abstract class AbstractVideoStaticFileCache { }) this.lru.on('evict', (obj: { key: string, value: string }) => { - unlinkPromise(obj.value).then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) + unlinkPromise(obj.value) + .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) }) } 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 { if (!video) return undefined const remoteStaticPath = videoCaption.getCaptionStaticPath() - const destPath = join(CACHE.DIRECTORIES.VIDEO_CAPTIONS, videoCaption.getCaptionName()) + const destPath = join(CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) } 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 { if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) - const destPath = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) + const destPath = join(CACHE.PREVIEWS.DIRECTORY, video.getPreviewName()) return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) } 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 { @BeforeDestroy static async removeFiles (instance: VideoCaptionModel) { + if (!instance.Video) { + instance.Video = await instance.$get('Video') as VideoModel + } if (instance.isOwned()) { - if (!instance.Video) { - instance.Video = await instance.$get('Video') as VideoModel - } - logger.debug('Removing captions %s of video %s.', instance.Video.uuid, instance.language) - return instance.removeCaptionFile() + + try { + await instance.removeCaptionFile() + } catch (err) { + logger.error('Cannot remove caption file of video %s.', instance.Video.uuid) + } } 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 @@ /* tslint:disable:no-unused-expression */ -import * as chai from 'chai' import 'mocha' import { createUser, @@ -127,6 +126,40 @@ describe('Test video captions API validator', function () { }) }) + it('Should fail with an invalid captionfile extension', async function () { + const attaches = { + 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.txt') + } + + const captionPath = path + videoUUID + '/captions/fr' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + token: server.accessToken, + fields, + attaches, + statusCodeExpected: 400 + }) + }) + + // it('Should fail with an invalid captionfile srt', async function () { + // const attaches = { + // 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.srt') + // } + // + // const captionPath = path + videoUUID + '/captions/fr' + // await makeUploadRequest({ + // method: 'PUT', + // url: server.url, + // path: captionPath, + // token: server.accessToken, + // fields, + // attaches, + // statusCodeExpected: 500 + // }) + // }) + it('Should success with the correct parameters', async function () { const captionPath = path + videoUUID + '/captions/fr' 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 @@ import * as chai from 'chai' import 'mocha' -import { doubleFollow, flushAndRunMultipleServers, uploadVideo } from '../../utils' +import { checkVideoFilesWereRemoved, doubleFollow, flushAndRunMultipleServers, removeVideo, uploadVideo, wait } from '../../utils' import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' import { waitJobs } from '../../utils/server/jobs' import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../utils/videos/video-captions' @@ -110,6 +110,51 @@ describe('Test video captions', function () { } }) + it('Should replace an existing caption with a srt file and convert it', async function () { + this.timeout(30000) + + await createVideoCaption({ + url: servers[0].url, + accessToken: servers[0].accessToken, + language: 'ar', + videoId: videoUUID, + fixture: 'subtitle-good.srt' + }) + + await waitJobs(servers) + + // Cache invalidation + await wait(3000) + }) + + it('Should have this caption updated and converted', async function () { + for (const server of servers) { + const res = await listVideoCaptions(server.url, videoUUID) + expect(res.body.total).to.equal(2) + expect(res.body.data).to.have.lengthOf(2) + + const caption1: VideoCaption = res.body.data[0] + expect(caption1.language.id).to.equal('ar') + expect(caption1.language.label).to.equal('Arabic') + expect(caption1.captionPath).to.equal('/static/video-captions/' + videoUUID + '-ar.vtt') + + const expected = 'WEBVTT FILE\r\n' + + '\r\n' + + '1\r\n' + + '00:00:01.600 --> 00:00:04.200\r\n' + + 'English (US)\r\n' + + '\r\n' + + '2\r\n' + + '00:00:05.900 --> 00:00:07.999\r\n' + + 'This is a subtitle in American English\r\n' + + '\r\n' + + '3\r\n' + + '00:00:10.000 --> 00:00:14.000\r\n' + + 'Adding subtitles is very easy to do\r\n' + await testCaptionFile(server.url, caption1.captionPath, expected) + } + }) + it('Should remove one caption', async function () { this.timeout(30000) @@ -133,6 +178,12 @@ describe('Test video captions', function () { } }) + it('Should remove the video, and thus all video captions', async function () { + await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) + + await checkVideoFilesWereRemoved(videoUUID, 1) + }) + after(async function () { killallServers(servers) }) 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 +00:00:01,600 --> 00:00:04,200 +English (US) + +2 +00:00:05,900 --> 00:00:07,999 +This is a subtitle in American English + +3 +00:00:10,000 --> 00:00:14,000 +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 +00:00:01,600 --> 00:00:04,200 +English (US) + +2 +00:00:05,900 --> 00:00:07,999 +This is a subtitle in American English + +3 +00:00:10,000 --> 00:00:14,000 +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) { async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) { const testDirectory = 'test' + serverNumber - for (const directory of [ 'videos', 'thumbnails', 'torrents', 'previews' ]) { + for (const directory of [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ]) { const directoryPath = join(root(), testDirectory, directory) 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: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" +charset-detector@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charset-detector/-/charset-detector-0.0.2.tgz#1cd5ddaf56e83259c6ef8e906ccf06f75fe9a1b2" + check-error@^1.0.1: version "1.0.2" 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: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" +duplexify@^3.2.0, duplexify@^3.5.0, duplexify@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + each-async@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473" @@ -3751,7 +3764,7 @@ is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" -isarray@0.0.1: +isarray@0.0.1, isarray@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -5382,6 +5395,14 @@ pause-stream@0.0.11: dependencies: through "~2.3" +peek-stream@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" + dependencies: + buffer-from "^1.0.0" + duplexify "^3.5.0" + through2 "^2.0.3" + pem@^1.12.3: version "1.12.5" resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.5.tgz#97bf2e459537c54e0ee5b0aa11b5ca18d6b5fef2" @@ -5655,7 +5676,7 @@ pump@^1.0.0, pump@^1.0.1: end-of-stream "^1.1.0" once "^1.3.1" -pump@^2.0.1: +pump@^2.0.0, pump@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" dependencies: @@ -5669,6 +5690,14 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -5813,7 +5842,7 @@ readable-stream@1.1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@1.1.x: +readable-stream@1.1.x, "readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@^1.1.13-1: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" dependencies: @@ -5822,7 +5851,16 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -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: +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +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: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: @@ -5834,6 +5872,12 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-wrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/readable-wrap/-/readable-wrap-1.0.0.tgz#3b5a211c631e12303a54991c806c17e7ae206bff" + dependencies: + readable-stream "^1.1.13-1" + readdirp@^2.0.0: version "2.1.0" 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: dependencies: extend-shallow "^3.0.0" +split2@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/split2/-/split2-0.2.1.tgz#02ddac9adc03ec0bb78c1282ec079ca6e85ae900" + dependencies: + through2 "~0.6.1" + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -6693,6 +6743,17 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" +srt-to-vtt@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/srt-to-vtt/-/srt-to-vtt-1.1.2.tgz#634c5228b34f2b5fb410cd4eaab5accbb09780d6" + dependencies: + duplexify "^3.2.0" + minimist "^1.1.0" + pumpify "^1.3.3" + split2 "^0.2.1" + through2 "^0.6.3" + to-utf-8 "^1.2.0" + sshpk@^1.7.0: version "1.14.2" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" @@ -6755,6 +6816,21 @@ stream-combiner@~0.0.4: dependencies: duplexer "~0.1.1" +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +stream-splicer@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-1.3.2.tgz#3c0441be15b9bf4e226275e6dc83964745546661" + dependencies: + indexof "0.0.1" + inherits "^2.0.1" + isarray "~0.0.1" + readable-stream "^1.1.13-1" + readable-wrap "^1.0.0" + through2 "^1.0.0" + stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.0: version "2.1.1" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" +through2@^0.6.3, through2@~0.6.1: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-1.1.1.tgz#0847cbc4449f3405574dbdccd9bb841b83ac3545" + dependencies: + readable-stream ">=1.1.13-1 <1.2.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + through@2, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" 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: regex-not "^1.0.2" safe-regex "^1.1.0" +to-utf-8@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/to-utf-8/-/to-utf-8-1.3.0.tgz#b2af7be9e003f4c3817cc116d3baed2a054993c9" + dependencies: + charset-detector "0.0.2" + iconv-lite "^0.4.4" + minimist "^1.1.0" + peek-stream "^1.1.1" + stream-splicer "^1.3.1" + toposort-class@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" @@ -7774,7 +7881,7 @@ xmlhttprequest-ssl@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" -xtend@^4.0.0, xtend@^4.0.1: +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"