Generate a name for caption files
This commit is contained in:
parent
a8b1b40485
commit
6302d599cd
|
@ -1,17 +1,17 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
|
import { MVideoCaption } from '@server/types/models'
|
||||||
|
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||||
|
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
|
||||||
|
import { createReqFiles } from '../../../helpers/express-utils'
|
||||||
|
import { logger } from '../../../helpers/logger'
|
||||||
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
|
import { CONFIG } from '../../../initializers/config'
|
||||||
|
import { MIMETYPES } from '../../../initializers/constants'
|
||||||
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
|
import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
|
||||||
import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
|
import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
|
||||||
import { createReqFiles } from '../../../helpers/express-utils'
|
|
||||||
import { MIMETYPES } from '../../../initializers/constants'
|
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
|
||||||
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
||||||
import { logger } from '../../../helpers/logger'
|
|
||||||
import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
|
|
||||||
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
|
|
||||||
import { CONFIG } from '../../../initializers/config'
|
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
|
||||||
import { MVideoCaptionVideo } from '@server/types/models'
|
|
||||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
|
||||||
|
|
||||||
const reqVideoCaptionAdd = createReqFiles(
|
const reqVideoCaptionAdd = createReqFiles(
|
||||||
[ 'captionfile' ],
|
[ 'captionfile' ],
|
||||||
|
@ -57,17 +57,19 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
|
||||||
const videoCaptionPhysicalFile = req.files['captionfile'][0]
|
const videoCaptionPhysicalFile = req.files['captionfile'][0]
|
||||||
const video = res.locals.videoAll
|
const video = res.locals.videoAll
|
||||||
|
|
||||||
|
const captionLanguage = req.params.captionLanguage
|
||||||
|
|
||||||
const videoCaption = new VideoCaptionModel({
|
const videoCaption = new VideoCaptionModel({
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
language: req.params.captionLanguage
|
filename: VideoCaptionModel.generateCaptionName(captionLanguage),
|
||||||
}) as MVideoCaptionVideo
|
language: captionLanguage
|
||||||
videoCaption.Video = video
|
}) as MVideoCaption
|
||||||
|
|
||||||
// Move physical file
|
// Move physical file
|
||||||
await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption)
|
await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption)
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, null, t)
|
await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
|
||||||
|
|
||||||
// Update video update
|
// Update video update
|
||||||
await federateVideoIfNeeded(video, false, t)
|
await federateVideoIfNeeded(video, false, t)
|
||||||
|
|
|
@ -9,9 +9,9 @@ import {
|
||||||
MThumbnail,
|
MThumbnail,
|
||||||
MUser,
|
MUser,
|
||||||
MVideoAccountDefault,
|
MVideoAccountDefault,
|
||||||
MVideoCaptionVideo,
|
MVideoCaption,
|
||||||
MVideoTag,
|
MVideoTag,
|
||||||
MVideoThumbnailAccountDefault,
|
MVideoThumbnail,
|
||||||
MVideoWithBlacklistLight
|
MVideoWithBlacklistLight
|
||||||
} from '@server/types/models'
|
} from '@server/types/models'
|
||||||
import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import'
|
import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import'
|
||||||
|
@ -154,20 +154,16 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
|
|
||||||
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
||||||
|
|
||||||
let thumbnailModel: MThumbnail
|
|
||||||
|
|
||||||
// Process video thumbnail from request.files
|
// Process video thumbnail from request.files
|
||||||
thumbnailModel = await processThumbnail(req, video)
|
let thumbnailModel = await processThumbnail(req, video)
|
||||||
|
|
||||||
// Process video thumbnail from url if processing from request.files failed
|
// Process video thumbnail from url if processing from request.files failed
|
||||||
if (!thumbnailModel && youtubeDLInfo.thumbnailUrl) {
|
if (!thumbnailModel && youtubeDLInfo.thumbnailUrl) {
|
||||||
thumbnailModel = await processThumbnailFromUrl(youtubeDLInfo.thumbnailUrl, video)
|
thumbnailModel = await processThumbnailFromUrl(youtubeDLInfo.thumbnailUrl, video)
|
||||||
}
|
}
|
||||||
|
|
||||||
let previewModel: MThumbnail
|
|
||||||
|
|
||||||
// Process video preview from request.files
|
// Process video preview from request.files
|
||||||
previewModel = await processPreview(req, video)
|
let previewModel = await processPreview(req, video)
|
||||||
|
|
||||||
// Process video preview from url if processing from request.files failed
|
// Process video preview from url if processing from request.files failed
|
||||||
if (!previewModel && youtubeDLInfo.thumbnailUrl) {
|
if (!previewModel && youtubeDLInfo.thumbnailUrl) {
|
||||||
|
@ -199,15 +195,15 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
for (const subtitle of subtitles) {
|
for (const subtitle of subtitles) {
|
||||||
const videoCaption = new VideoCaptionModel({
|
const videoCaption = new VideoCaptionModel({
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
language: subtitle.language
|
language: subtitle.language,
|
||||||
}) as MVideoCaptionVideo
|
filename: VideoCaptionModel.generateCaptionName(subtitle.language)
|
||||||
videoCaption.Video = video
|
}) as MVideoCaption
|
||||||
|
|
||||||
// Move physical file
|
// Move physical file
|
||||||
await moveAndProcessCaptionFile(subtitle, videoCaption)
|
await moveAndProcessCaptionFile(subtitle, videoCaption)
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
await VideoCaptionModel.insertOrReplaceLanguage(video.id, subtitle.language, null, t)
|
await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -227,7 +223,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
return res.json(videoImport.toFormattedJSON()).end()
|
return res.json(videoImport.toFormattedJSON()).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) {
|
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo): MVideoThumbnail {
|
||||||
const videoData = {
|
const videoData = {
|
||||||
name: body.name || importData.name || 'Unknown name',
|
name: body.name || importData.name || 'Unknown name',
|
||||||
remote: false,
|
remote: false,
|
||||||
|
@ -252,7 +248,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
|
||||||
return video
|
return video
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processThumbnail (req: express.Request, video: VideoModel) {
|
async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
|
||||||
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
|
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
|
||||||
if (thumbnailField) {
|
if (thumbnailField) {
|
||||||
const thumbnailPhysicalFile = thumbnailField[0]
|
const thumbnailPhysicalFile = thumbnailField[0]
|
||||||
|
@ -268,7 +264,7 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPreview (req: express.Request, video: VideoModel) {
|
async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
|
||||||
const previewField = req.files ? req.files['previewfile'] : undefined
|
const previewField = req.files ? req.files['previewfile'] : undefined
|
||||||
if (previewField) {
|
if (previewField) {
|
||||||
const previewPhysicalFile = previewField[0]
|
const previewPhysicalFile = previewField[0]
|
||||||
|
@ -284,7 +280,7 @@ async function processPreview (req: express.Request, video: VideoModel) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processThumbnailFromUrl (url: string, video: VideoModel) {
|
async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) {
|
||||||
try {
|
try {
|
||||||
return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE)
|
return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -293,7 +289,7 @@ async function processThumbnailFromUrl (url: string, video: VideoModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPreviewFromUrl (url: string, video: VideoModel) {
|
async function processPreviewFromUrl (url: string, video: MVideoThumbnail) {
|
||||||
try {
|
try {
|
||||||
return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW)
|
return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -303,7 +299,7 @@ async function processPreviewFromUrl (url: string, video: VideoModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertIntoDB (parameters: {
|
function insertIntoDB (parameters: {
|
||||||
video: MVideoThumbnailAccountDefault
|
video: MVideoThumbnail
|
||||||
thumbnailModel: MThumbnail
|
thumbnailModel: MThumbnail
|
||||||
previewModel: MThumbnail
|
previewModel: MThumbnail
|
||||||
videoChannel: MChannelAccountDefault
|
videoChannel: MChannelAccountDefault
|
||||||
|
|
|
@ -23,7 +23,7 @@ lazyStaticRouter.use(
|
||||||
)
|
)
|
||||||
|
|
||||||
lazyStaticRouter.use(
|
lazyStaticRouter.use(
|
||||||
LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':videoId-:captionLanguage([a-z]+).vtt',
|
LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename',
|
||||||
asyncMiddleware(getVideoCaption)
|
asyncMiddleware(getVideoCaption)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,10 +78,7 @@ async function getPreview (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoCaption (req: express.Request, res: express.Response) {
|
async function getVideoCaption (req: express.Request, res: express.Response) {
|
||||||
const result = await VideosCaptionCache.Instance.getFilePath({
|
const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename)
|
||||||
videoId: req.params.videoId,
|
|
||||||
language: req.params.captionLanguage
|
|
||||||
})
|
|
||||||
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||||
|
|
||||||
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { join } from 'path'
|
|
||||||
import { CONFIG } from '../initializers/config'
|
|
||||||
import * as srt2vtt from 'srt-to-vtt'
|
|
||||||
import { createReadStream, createWriteStream, move, remove } from 'fs-extra'
|
import { createReadStream, createWriteStream, move, remove } from 'fs-extra'
|
||||||
import { MVideoCaptionFormattable } from '@server/types/models'
|
import { join } from 'path'
|
||||||
|
import * as srt2vtt from 'srt-to-vtt'
|
||||||
|
import { MVideoCaption } from '@server/types/models'
|
||||||
|
import { CONFIG } from '../initializers/config'
|
||||||
|
|
||||||
async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: MVideoCaptionFormattable) {
|
async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: MVideoCaption) {
|
||||||
const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
|
const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
|
||||||
const destination = join(videoCaptionsDir, videoCaption.getCaptionName())
|
const destination = join(videoCaptionsDir, videoCaption.filename)
|
||||||
|
|
||||||
// Convert this srt file to vtt
|
// Convert this srt file to vtt
|
||||||
if (physicalFile.path.endsWith('.srt')) {
|
if (physicalFile.path.endsWith('.srt')) {
|
||||||
|
@ -17,7 +17,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is important in case if there is another attempt in the retry process
|
// This is important in case if there is another attempt in the retry process
|
||||||
physicalFile.filename = videoCaption.getCaptionName()
|
physicalFile.filename = videoCaption.filename
|
||||||
physicalFile.path = destination
|
physicalFile.path = destination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ async function doesVideoCaptionExist (video: MVideoId, language: string, res: Re
|
||||||
if (!videoCaption) {
|
if (!videoCaption) {
|
||||||
res.status(HttpStatusCode.NOT_FOUND_404)
|
res.status(HttpStatusCode.NOT_FOUND_404)
|
||||||
.json({ error: 'Video caption not found' })
|
.json({ error: 'Video caption not found' })
|
||||||
.end()
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 575
|
const LAST_MIGRATION_VERSION = 580
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
48
server/initializers/migrations/0580-caption-filename.ts
Normal file
48
server/initializers/migrations/0580-caption-filename.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.addColumn('videoCaption', 'filename', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const query = `UPDATE "videoCaption" SET "filename" = s.uuid || '-' || s.language || '.vtt' ` +
|
||||||
|
`FROM (` +
|
||||||
|
` SELECT "videoCaption"."id", video.uuid, "videoCaption".language ` +
|
||||||
|
` FROM "videoCaption" INNER JOIN video ON video.id = "videoCaption"."videoId"` +
|
||||||
|
`) AS s ` +
|
||||||
|
`WHERE "videoCaption".id = s.id`
|
||||||
|
|
||||||
|
await utils.sequelize.query(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.changeColumn('videoCaption', 'filename', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -99,8 +99,6 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('toto', { playlist, id: playlist.id })
|
|
||||||
|
|
||||||
const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
|
const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
|
||||||
|
|
||||||
if (playlistObject.icon) {
|
if (playlistObject.icon) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import {
|
||||||
MVideoAccountLightBlacklistAllFiles,
|
MVideoAccountLightBlacklistAllFiles,
|
||||||
MVideoAP,
|
MVideoAP,
|
||||||
MVideoAPWithoutCaption,
|
MVideoAPWithoutCaption,
|
||||||
|
MVideoCaption,
|
||||||
MVideoFile,
|
MVideoFile,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoId,
|
MVideoId,
|
||||||
|
@ -90,7 +91,7 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
|
||||||
// Fetch more attributes that we will need to serialize in AP object
|
// Fetch more attributes that we will need to serialize in AP object
|
||||||
if (isArray(video.VideoCaptions) === false) {
|
if (isArray(video.VideoCaptions) === false) {
|
||||||
video.VideoCaptions = await video.$get('VideoCaptions', {
|
video.VideoCaptions = await video.$get('VideoCaptions', {
|
||||||
attributes: [ 'language' ],
|
attributes: [ 'filename', 'language' ],
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -423,7 +424,14 @@ async function updateVideoFromAP (options: {
|
||||||
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
|
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
|
||||||
|
|
||||||
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
|
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
|
||||||
return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
|
const caption = new VideoCaptionModel({
|
||||||
|
videoId: videoUpdated.id,
|
||||||
|
filename: VideoCaptionModel.generateCaptionName(c.identifier),
|
||||||
|
language: c.identifier,
|
||||||
|
fileUrl: c.url
|
||||||
|
}) as MVideoCaption
|
||||||
|
|
||||||
|
return VideoCaptionModel.insertOrReplaceLanguage(caption, t)
|
||||||
})
|
})
|
||||||
await Promise.all(videoCaptionsPromises)
|
await Promise.all(videoCaptionsPromises)
|
||||||
}
|
}
|
||||||
|
@ -629,7 +637,14 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
|
||||||
|
|
||||||
// Process captions
|
// Process captions
|
||||||
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
|
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
|
||||||
return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
|
const caption = new VideoCaptionModel({
|
||||||
|
videoId: videoCreated.id,
|
||||||
|
filename: VideoCaptionModel.generateCaptionName(c.identifier),
|
||||||
|
language: c.identifier,
|
||||||
|
fileUrl: c.url
|
||||||
|
}) as MVideoCaption
|
||||||
|
|
||||||
|
return VideoCaptionModel.insertOrReplaceLanguage(caption, t)
|
||||||
})
|
})
|
||||||
await Promise.all(videoCaptionsPromises)
|
await Promise.all(videoCaptionsPromises)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { doRequestAndSaveToFile } from '@server/helpers/requests'
|
||||||
|
import { CONFIG } from '../../initializers/config'
|
||||||
import { FILES_CACHE } from '../../initializers/constants'
|
import { FILES_CACHE } from '../../initializers/constants'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
import { VideoCaptionModel } from '../../models/video/video-caption'
|
import { VideoCaptionModel } from '../../models/video/video-caption'
|
||||||
import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
|
import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
|
||||||
import { CONFIG } from '../../initializers/config'
|
|
||||||
import { logger } from '../../helpers/logger'
|
|
||||||
import { doRequestAndSaveToFile } from '@server/helpers/requests'
|
|
||||||
|
|
||||||
type GetPathParam = { videoId: string, language: string }
|
class VideosCaptionCache extends AbstractVideoStaticFileCache <string> {
|
||||||
|
|
||||||
class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
|
|
||||||
|
|
||||||
private static readonly KEY_DELIMITER = '%'
|
|
||||||
private static instance: VideosCaptionCache
|
private static instance: VideosCaptionCache
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
|
@ -22,32 +18,28 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilePathImpl (params: GetPathParam) {
|
async getFilePathImpl (filename: string) {
|
||||||
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
|
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename)
|
||||||
if (!videoCaption) return undefined
|
if (!videoCaption) return undefined
|
||||||
|
|
||||||
if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) }
|
if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.filename) }
|
||||||
|
|
||||||
const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
|
return this.loadRemoteFile(filename)
|
||||||
return this.loadRemoteFile(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key is the caption filename
|
||||||
protected async loadRemoteFile (key: string) {
|
protected async loadRemoteFile (key: string) {
|
||||||
logger.debug('Loading remote caption file %s.', key)
|
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key)
|
||||||
|
|
||||||
const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER)
|
|
||||||
|
|
||||||
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language)
|
|
||||||
if (!videoCaption) return undefined
|
if (!videoCaption) return undefined
|
||||||
|
|
||||||
if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.')
|
if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.')
|
||||||
|
|
||||||
// Used to fetch the path
|
// Used to fetch the path
|
||||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoCaption.videoId)
|
||||||
if (!video) return undefined
|
if (!video) return undefined
|
||||||
|
|
||||||
const remoteUrl = videoCaption.getFileUrl(video)
|
const remoteUrl = videoCaption.getFileUrl(video)
|
||||||
const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
|
const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename)
|
||||||
|
|
||||||
await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
|
await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
|
||||||
return this.loadRemoteFile(thumbnail.Video.uuid)
|
return this.loadRemoteFile(thumbnail.Video.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key is the video UUID
|
||||||
protected async loadRemoteFile (key: string) {
|
protected async loadRemoteFile (key: string) {
|
||||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(key)
|
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(key)
|
||||||
if (!video) return undefined
|
if (!video) return undefined
|
||||||
|
|
|
@ -166,6 +166,9 @@ async function createThumbnailFromFunction (parameters: {
|
||||||
}) {
|
}) {
|
||||||
const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
|
const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
|
||||||
|
|
||||||
|
// Remove old file
|
||||||
|
if (existingThumbnail) await existingThumbnail.removeThumbnail()
|
||||||
|
|
||||||
const thumbnail = existingThumbnail || new ThumbnailModel()
|
const thumbnail = existingThumbnail || new ThumbnailModel()
|
||||||
|
|
||||||
thumbnail.filename = filename
|
thumbnail.filename = filename
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
|
||||||
import { MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
|
import { MVideoAccountLight, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
|
||||||
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
|
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
|
||||||
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
|
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
|
@ -24,6 +24,7 @@ import { CONFIG } from '../../initializers/config'
|
||||||
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
|
||||||
import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
|
import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
|
WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
|
||||||
|
@ -44,6 +45,10 @@ export enum ScopeNames {
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'videoCaption',
|
tableName: 'videoCaption',
|
||||||
indexes: [
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'filename' ],
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fields: [ 'videoId' ]
|
fields: [ 'videoId' ]
|
||||||
},
|
},
|
||||||
|
@ -65,6 +70,10 @@ export class VideoCaptionModel extends Model {
|
||||||
@Column
|
@Column
|
||||||
language: string
|
language: string
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Column
|
||||||
|
filename: string
|
||||||
|
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
|
@Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
|
||||||
fileUrl: string
|
fileUrl: string
|
||||||
|
@ -88,12 +97,12 @@ export class VideoCaptionModel extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.isOwned()) {
|
if (instance.isOwned()) {
|
||||||
logger.info('Removing captions %s of video %s.', instance.Video.uuid, instance.language)
|
logger.info('Removing caption %s.', instance.filename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await instance.removeCaptionFile()
|
await instance.removeCaptionFile()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot remove caption file of video %s.', instance.Video.uuid)
|
logger.error('Cannot remove caption file %s.', instance.filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,15 +128,28 @@ export class VideoCaptionModel extends Model {
|
||||||
return VideoCaptionModel.findOne(query)
|
return VideoCaptionModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) {
|
static loadWithVideoByFilename (filename: string): Promise<MVideoCaptionVideo> {
|
||||||
const values = {
|
const query = {
|
||||||
videoId,
|
where: {
|
||||||
language,
|
filename
|
||||||
fileUrl
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: VideoModel.unscoped(),
|
||||||
|
attributes: [ 'id', 'remote', 'uuid' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoCaptionModel.upsert(values, { transaction, returning: true })
|
return VideoCaptionModel.findOne(query)
|
||||||
.then(([ caption ]) => caption)
|
}
|
||||||
|
|
||||||
|
static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) {
|
||||||
|
const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language)
|
||||||
|
// Delete existing file
|
||||||
|
if (existing) await existing.destroy({ transaction })
|
||||||
|
|
||||||
|
return caption.save({ transaction })
|
||||||
}
|
}
|
||||||
|
|
||||||
static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> {
|
static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> {
|
||||||
|
@ -156,6 +178,10 @@ export class VideoCaptionModel extends Model {
|
||||||
return VideoCaptionModel.destroy(query)
|
return VideoCaptionModel.destroy(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static generateCaptionName (language: string) {
|
||||||
|
return `${uuidv4()}-${language}.vtt`
|
||||||
|
}
|
||||||
|
|
||||||
isOwned () {
|
isOwned () {
|
||||||
return this.Video.remote === false
|
return this.Video.remote === false
|
||||||
}
|
}
|
||||||
|
@ -170,16 +196,12 @@ export class VideoCaptionModel extends Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCaptionStaticPath (this: MVideoCaptionFormattable) {
|
getCaptionStaticPath (this: MVideoCaption) {
|
||||||
return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
|
return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCaptionName (this: MVideoCaptionFormattable) {
|
removeCaptionFile (this: MVideoCaption) {
|
||||||
return `${this.Video.uuid}-${this.language}.vtt`
|
return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
|
||||||
}
|
|
||||||
|
|
||||||
removeCaptionFile (this: MVideoCaptionFormattable) {
|
|
||||||
return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileUrl (video: MVideoAccountLight) {
|
getFileUrl (video: MVideoAccountLight) {
|
||||||
|
|
|
@ -471,7 +471,7 @@ export class VideoPlaylistModel extends Model {
|
||||||
generateThumbnailName () {
|
generateThumbnailName () {
|
||||||
const extension = '.jpg'
|
const extension = '.jpg'
|
||||||
|
|
||||||
return 'playlist-' + this.uuid + extension
|
return 'playlist-' + uuidv4() + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailUrl () {
|
getThumbnailUrl () {
|
||||||
|
|
|
@ -2,24 +2,25 @@
|
||||||
|
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
|
import { Video, VideoPlaylistPrivacy } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
|
addVideoInPlaylist,
|
||||||
|
createVideoPlaylist,
|
||||||
getOEmbed,
|
getOEmbed,
|
||||||
getVideosList,
|
getVideosList,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel,
|
setDefaultVideoChannel,
|
||||||
uploadVideo,
|
uploadVideo
|
||||||
createVideoPlaylist,
|
|
||||||
addVideoInPlaylist
|
|
||||||
} from '../../../../shared/extra-utils'
|
} from '../../../../shared/extra-utils'
|
||||||
import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
|
import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
|
||||||
import { VideoPlaylistPrivacy } from '@shared/models'
|
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Test services', function () {
|
describe('Test services', function () {
|
||||||
let server: ServerInfo = null
|
let server: ServerInfo = null
|
||||||
let playlistUUID: string
|
let playlistUUID: string
|
||||||
|
let video: Video
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
@ -36,7 +37,7 @@ describe('Test services', function () {
|
||||||
await uploadVideo(server.url, server.accessToken, videoAttributes)
|
await uploadVideo(server.url, server.accessToken, videoAttributes)
|
||||||
|
|
||||||
const res = await getVideosList(server.url)
|
const res = await getVideosList(server.url)
|
||||||
server.video = res.body.data[0]
|
video = res.body.data[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -57,23 +58,23 @@ describe('Test services', function () {
|
||||||
token: server.accessToken,
|
token: server.accessToken,
|
||||||
playlistId: res.body.videoPlaylist.id,
|
playlistId: res.body.videoPlaylist.id,
|
||||||
elementAttrs: {
|
elementAttrs: {
|
||||||
videoId: server.video.id
|
videoId: video.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have a valid oEmbed video response', async function () {
|
it('Should have a valid oEmbed video response', async function () {
|
||||||
const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + server.video.uuid
|
const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + video.uuid
|
||||||
|
|
||||||
const res = await getOEmbed(server.url, oembedUrl)
|
const res = await getOEmbed(server.url, oembedUrl)
|
||||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
|
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
|
||||||
`src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
|
`src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
|
||||||
'frameborder="0" allowfullscreen></iframe>'
|
'frameborder="0" allowfullscreen></iframe>'
|
||||||
const expectedThumbnailUrl = 'http://localhost:' + server.port + '/lazy-static/previews/' + server.video.uuid + '.jpg'
|
const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath
|
||||||
|
|
||||||
expect(res.body.html).to.equal(expectedHtml)
|
expect(res.body.html).to.equal(expectedHtml)
|
||||||
expect(res.body.title).to.equal(server.video.name)
|
expect(res.body.title).to.equal(video.name)
|
||||||
expect(res.body.author_name).to.equal(server.videoChannel.displayName)
|
expect(res.body.author_name).to.equal(server.videoChannel.displayName)
|
||||||
expect(res.body.width).to.equal(560)
|
expect(res.body.width).to.equal(560)
|
||||||
expect(res.body.height).to.equal(315)
|
expect(res.body.height).to.equal(315)
|
||||||
|
@ -101,18 +102,18 @@ describe('Test services', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have a valid oEmbed response with small max height query', async function () {
|
it('Should have a valid oEmbed response with small max height query', async function () {
|
||||||
const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + server.video.uuid
|
const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + video.uuid
|
||||||
const format = 'json'
|
const format = 'json'
|
||||||
const maxHeight = 50
|
const maxHeight = 50
|
||||||
const maxWidth = 50
|
const maxWidth = 50
|
||||||
|
|
||||||
const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
|
const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
|
||||||
const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
|
const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
|
||||||
`src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
|
`src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
|
||||||
'frameborder="0" allowfullscreen></iframe>'
|
'frameborder="0" allowfullscreen></iframe>'
|
||||||
|
|
||||||
expect(res.body.html).to.equal(expectedHtml)
|
expect(res.body.html).to.equal(expectedHtml)
|
||||||
expect(res.body.title).to.equal(server.video.name)
|
expect(res.body.title).to.equal(video.name)
|
||||||
expect(res.body.author_name).to.equal(server.videoChannel.displayName)
|
expect(res.body.author_name).to.equal(server.videoChannel.displayName)
|
||||||
expect(res.body.height).to.equal(50)
|
expect(res.body.height).to.equal(50)
|
||||||
expect(res.body.width).to.equal(50)
|
expect(res.body.width).to.equal(50)
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { VideoCaption } from '../../../../shared/models/videos/caption/video-cap
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Test video captions', function () {
|
describe('Test video captions', function () {
|
||||||
|
const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||||
|
|
||||||
let servers: ServerInfo[]
|
let servers: ServerInfo[]
|
||||||
let videoUUID: string
|
let videoUUID: string
|
||||||
|
|
||||||
|
@ -83,13 +85,13 @@ describe('Test video captions', function () {
|
||||||
const caption1: VideoCaption = res.body.data[0]
|
const caption1: VideoCaption = res.body.data[0]
|
||||||
expect(caption1.language.id).to.equal('ar')
|
expect(caption1.language.id).to.equal('ar')
|
||||||
expect(caption1.language.label).to.equal('Arabic')
|
expect(caption1.language.label).to.equal('Arabic')
|
||||||
expect(caption1.captionPath).to.equal('/lazy-static/video-captions/' + videoUUID + '-ar.vtt')
|
expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
|
||||||
await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.')
|
await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.')
|
||||||
|
|
||||||
const caption2: VideoCaption = res.body.data[1]
|
const caption2: VideoCaption = res.body.data[1]
|
||||||
expect(caption2.language.id).to.equal('zh')
|
expect(caption2.language.id).to.equal('zh')
|
||||||
expect(caption2.language.label).to.equal('Chinese')
|
expect(caption2.language.label).to.equal('Chinese')
|
||||||
expect(caption2.captionPath).to.equal('/lazy-static/video-captions/' + videoUUID + '-zh.vtt')
|
expect(caption2.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
|
||||||
await testCaptionFile(server.url, caption2.captionPath, 'Subtitle good 2.')
|
await testCaptionFile(server.url, caption2.captionPath, 'Subtitle good 2.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -117,7 +119,7 @@ describe('Test video captions', function () {
|
||||||
const caption1: VideoCaption = res.body.data[0]
|
const caption1: VideoCaption = res.body.data[0]
|
||||||
expect(caption1.language.id).to.equal('ar')
|
expect(caption1.language.id).to.equal('ar')
|
||||||
expect(caption1.language.label).to.equal('Arabic')
|
expect(caption1.language.label).to.equal('Arabic')
|
||||||
expect(caption1.captionPath).to.equal('/lazy-static/video-captions/' + videoUUID + '-ar.vtt')
|
expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
|
||||||
await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 2.')
|
await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 2.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -148,7 +150,7 @@ describe('Test video captions', function () {
|
||||||
const caption1: VideoCaption = res.body.data[0]
|
const caption1: VideoCaption = res.body.data[0]
|
||||||
expect(caption1.language.id).to.equal('ar')
|
expect(caption1.language.id).to.equal('ar')
|
||||||
expect(caption1.language.label).to.equal('Arabic')
|
expect(caption1.language.label).to.equal('Arabic')
|
||||||
expect(caption1.captionPath).to.equal('/lazy-static/video-captions/' + videoUUID + '-ar.vtt')
|
expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
|
||||||
|
|
||||||
const expected = 'WEBVTT FILE\r\n' +
|
const expected = 'WEBVTT FILE\r\n' +
|
||||||
'\r\n' +
|
'\r\n' +
|
||||||
|
@ -185,7 +187,7 @@ describe('Test video captions', function () {
|
||||||
|
|
||||||
expect(caption.language.id).to.equal('zh')
|
expect(caption.language.id).to.equal('zh')
|
||||||
expect(caption.language.label).to.equal('Chinese')
|
expect(caption.language.label).to.equal('Chinese')
|
||||||
expect(caption.captionPath).to.equal('/lazy-static/video-captions/' + videoUUID + '-zh.vtt')
|
expect(caption.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
|
||||||
await testCaptionFile(server.url, caption.captionPath, 'Subtitle good 2.')
|
await testCaptionFile(server.url, caption.captionPath, 'Subtitle good 2.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { PickWith } from '@shared/core-utils'
|
||||||
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
import { VideoCaptionModel } from '../../../models/video/video-caption'
|
||||||
import { FunctionProperties, PickWith } from '@shared/core-utils'
|
|
||||||
import { MVideo, MVideoUUID } from './video'
|
import { MVideo, MVideoUUID } from './video'
|
||||||
|
|
||||||
type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
|
type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
|
||||||
|
@ -22,6 +22,6 @@ export type MVideoCaptionVideo =
|
||||||
// Format for API or AP object
|
// Format for API or AP object
|
||||||
|
|
||||||
export type MVideoCaptionFormattable =
|
export type MVideoCaptionFormattable =
|
||||||
FunctionProperties<MVideoCaption> &
|
MVideoCaption &
|
||||||
Pick<MVideoCaption, 'language'> &
|
Pick<MVideoCaption, 'language'> &
|
||||||
Use<'Video', MVideoUUID>
|
Use<'Video', MVideoUUID>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ChildProcess, exec, fork } from 'child_process'
|
||||||
import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
|
import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { randomInt } from '../../core-utils/miscs/miscs'
|
import { randomInt } from '../../core-utils/miscs/miscs'
|
||||||
import { VideoChannel } from '../../models/videos'
|
import { Video, VideoChannel } from '../../models/videos'
|
||||||
import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
|
import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
|
||||||
import { makeGetRequest } from '../requests/requests'
|
import { makeGetRequest } from '../requests/requests'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user