From 50ad0a1c1699fb1799c9ba2a99bf888894f88df4 Mon Sep 17 00:00:00 2001 From: kimsible Date: Sat, 11 Apr 2020 04:24:42 +0200 Subject: [PATCH] Add getSubs to YoutubeDL video import --- .../video-import-url.component.ts | 58 ++++++++++++------- server/controllers/api/videos/import.ts | 29 +++++++++- server/helpers/youtube-dl.ts | 36 ++++++++++++ 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts index a5578bebd..a17d73683 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts @@ -12,6 +12,7 @@ import { FormValidatorService } from '@app/shared' import { VideoCaptionService } from '@app/shared/video-caption' import { VideoImportService } from '@app/shared/video-import' import { scrollToTop } from '@app/shared/misc/utils' +import { switchMap, map } from 'rxjs/operators' @Component({ selector: 'my-video-import-url', @@ -76,31 +77,44 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom this.loadingBar.start() - this.videoImportService.importVideoUrl(this.targetUrl, videoUpdate).subscribe( - res => { - this.loadingBar.complete() - this.firstStepDone.emit(res.video.name) - this.isImportingVideo = false - this.hasImportedVideo = true + this.videoImportService + .importVideoUrl(this.targetUrl, videoUpdate) + .pipe( + switchMap(res => { + return this.videoCaptionService + .listCaptions(res.video.id) + .pipe( + map(result => ({ video: res.video, videoCaptions: result.data })) + ) + }) + ) + .subscribe( + ({ video, videoCaptions }) => { + this.loadingBar.complete() + this.firstStepDone.emit(video.name) + this.isImportingVideo = false + this.hasImportedVideo = true - this.video = new VideoEdit(Object.assign(res.video, { - commentsEnabled: videoUpdate.commentsEnabled, - downloadEnabled: videoUpdate.downloadEnabled, - support: null, - thumbnailUrl: null, - previewUrl: null - })) + this.video = new VideoEdit(Object.assign(video, { + commentsEnabled: videoUpdate.commentsEnabled, + downloadEnabled: videoUpdate.downloadEnabled, + support: null, + thumbnailUrl: null, + previewUrl: null + })) - this.hydrateFormFromVideo() - }, + this.videoCaptions = videoCaptions - err => { - this.loadingBar.complete() - this.isImportingVideo = false - this.firstStepError.emit() - this.notifier.error(err.message) - } - ) + this.hydrateFormFromVideo() + }, + + err => { + this.loadingBar.complete() + this.isImportingVideo = false + this.firstStepError.emit() + this.notifier.error(err.message) + } + ) } updateSecondStep () { diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index da0832258..e9b9d68d7 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -3,11 +3,13 @@ import * as magnetUtil from 'magnet-uri' import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' import { MIMETYPES } from '../../../initializers/constants' -import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' +import { getYoutubeDLInfo, YoutubeDLInfo, getYoutubeDLSubs } from '../../../helpers/youtube-dl' import { createReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' import { VideoModel } from '../../../models/video/video' +import { VideoCaptionModel } from '../../../models/video/video-caption' +import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' import { getVideoActivityPubUrl } from '../../../lib/activitypub' import { TagModel } from '../../../models/video/tag' import { VideoImportModel } from '../../../models/video/video-import' @@ -28,6 +30,7 @@ import { MThumbnail, MUser, MVideoAccountDefault, + MVideoCaptionVideo, MVideoTag, MVideoThumbnailAccountDefault, MVideoWithBlacklistLight @@ -136,6 +139,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) const targetUrl = body.targetUrl const user = res.locals.oauth.token.User + // Get video infos let youtubeDLInfo: YoutubeDLInfo try { youtubeDLInfo = await getYoutubeDLInfo(targetUrl) @@ -168,6 +172,29 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) user }) + + // Get video subtitles + try { + const subtitles = await getYoutubeDLSubs(targetUrl) + + for (const subtitle of subtitles) { + const videoCaption = new VideoCaptionModel({ + videoId: video.id, + language: subtitle.language + }) as MVideoCaptionVideo + videoCaption.Video = video + + // Move physical file + await moveAndProcessCaptionFile(subtitle, videoCaption) + + await sequelizeTypescript.transaction(async t => { + await VideoCaptionModel.insertOrReplaceLanguage(video.id, subtitle.language, null, t) + }) + } + } catch (err) { + logger.warn('Cannot get video subtitles.', { err }) + } + // Create job to import the video const payload = { type: 'youtube-dl' as 'youtube-dl', diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 07c85797a..277422645 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts @@ -20,6 +20,12 @@ export type YoutubeDLInfo = { originallyPublishedAt?: Date } +export type YoutubeDLSubs = { + language: string, + filename: string, + path: string +}[] + const processOptions = { maxBuffer: 1024 * 1024 * 10 // 10MB } @@ -45,6 +51,35 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise }) } +function getYoutubeDLSubs (url: string, opts?: object): Promise { + return new Promise((res, rej) => { + const cwd = CONFIG.STORAGE.TMP_DIR + const options = opts || { all: true, format: 'vtt', cwd } + + safeGetYoutubeDL() + .then(youtubeDL => { + youtubeDL.getSubs(url, options, (err, files) => { + if (err) return rej(err) + + const subtitles = files.reduce((acc, filename) => { + const matched = filename.match(/\.([a-z]{2})\.(vtt|ttml)/i) + + if (matched[1]) { + return [...acc, { + language: matched[1], + path: join(cwd, filename), + filename + }] + } + }, []) + + return res(subtitles) + }) + }) + .catch(err => rej(err)) + }) +} + function downloadYoutubeDLVideo (url: string, extension: string, timeout: number) { const path = generateVideoImportTmpPath(url, extension) let timer @@ -185,6 +220,7 @@ function buildOriginallyPublishedAt (obj: any) { export { updateYoutubeDLBinary, downloadYoutubeDLVideo, + getYoutubeDLSubs, getYoutubeDLInfo, safeGetYoutubeDL, buildOriginallyPublishedAt