mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-08-17 17:30:32 +03:00
Handle .srt subtitles
This commit is contained in:
parent
16f7022b06
commit
f4001cf408
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -151,7 +151,7 @@
|
||||
|
||||
<div class="form-group" *ngFor="let videoCaption of videoCaptions">
|
||||
|
||||
<div class="caption-entry">
|
||||
<div *ngIf="videoCaption.action !== 'REMOVE'" class="caption-entry">
|
||||
<div class="caption-entry-label">{{ videoCaption.language.label }}</div>
|
||||
|
||||
<span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
|
||||
@ -200,5 +200,5 @@
|
||||
</div>
|
||||
|
||||
<my-video-caption-add-modal
|
||||
#videoCaptionAddModal [existingCaptions]="getExistingCaptions()" (captionAdded)="onCaptionAdded($event)"
|
||||
#videoCaptionAddModal [existingCaptions]="existingCaptions" (captionAdded)="onCaptionAdded($event)"
|
||||
></my-video-caption-add-modal>
|
@ -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' })
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
47
server/helpers/captions-utils.ts
Normal file
47
server/helpers/captions-utils.ts
Normal file
@ -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())
|
||||
})
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -33,7 +33,8 @@ export {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function removeCacheDirectories () {
|
||||
const cacheDirectories = CACHE.DIRECTORIES
|
||||
const cacheDirectories = Object.keys(CACHE)
|
||||
.map(k => CACHE[k].DIRECTORY)
|
||||
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
@ -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)) {
|
||||
|
@ -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 <T> {
|
||||
|
||||
@ -17,9 +14,10 @@ export abstract class AbstractVideoStaticFileCache <T> {
|
||||
// Load and save the remote file, then return the local path from filesystem
|
||||
protected abstract loadRemoteFile (key: string): Promise<string>
|
||||
|
||||
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 <T> {
|
||||
})
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
|
2
server/lib/cache/videos-caption-cache.ts
vendored
2
server/lib/cache/videos-caption-cache.ts
vendored
@ -42,7 +42,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
|
||||
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)
|
||||
}
|
||||
|
2
server/lib/cache/videos-preview-cache.ts
vendored
2
server/lib/cache/videos-preview-cache.ts
vendored
@ -31,7 +31,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
|
||||
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)
|
||||
}
|
||||
|
@ -75,14 +75,18 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
|
||||
|
||||
@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
|
||||
|
@ -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({
|
||||
|
@ -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)
|
||||
})
|
||||
|
11
server/tests/fixtures/subtitle-bad.txt
vendored
Normal file
11
server/tests/fixtures/subtitle-bad.txt
vendored
Normal file
@ -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
|
11
server/tests/fixtures/subtitle-good.srt
vendored
Normal file
11
server/tests/fixtures/subtitle-good.srt
vendored
Normal file
@ -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
|
@ -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)
|
||||
|
117
yarn.lock
117
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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user