Add getSubs to YoutubeDL video import
authorkimsible <kimsible@users.noreply.github.com>
Sat, 11 Apr 2020 02:24:42 +0000 (04:24 +0200)
committerkimsible <kimsible@users.noreply.github.com>
Tue, 14 Apr 2020 12:39:30 +0000 (14:39 +0200)
client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
server/controllers/api/videos/import.ts
server/helpers/youtube-dl.ts

index a5578bebd76e879718fc347ef658a884be44d8c9..a17d736834319dc32db1eb7351b557a5ec5b7a59 100644 (file)
@@ -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.video = new VideoEdit(Object.assign(res.video, {
-          commentsEnabled: videoUpdate.commentsEnabled,
-          downloadEnabled: videoUpdate.downloadEnabled,
-          support: null,
-          thumbnailUrl: null,
-          previewUrl: null
-        }))
-
-        this.hydrateFormFromVideo()
-      },
-
-      err => {
-        this.loadingBar.complete()
-        this.isImportingVideo = false
-        this.firstStepError.emit()
-        this.notifier.error(err.message)
-      }
-    )
+    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(video, {
+              commentsEnabled: videoUpdate.commentsEnabled,
+              downloadEnabled: videoUpdate.downloadEnabled,
+              support: null,
+              thumbnailUrl: null,
+              previewUrl: null
+            }))
+
+            this.videoCaptions = videoCaptions
+
+            this.hydrateFormFromVideo()
+          },
+
+          err => {
+            this.loadingBar.complete()
+            this.isImportingVideo = false
+            this.firstStepError.emit()
+            this.notifier.error(err.message)
+          }
+        )
   }
 
   updateSecondStep () {
index da08322589c6eb84e333f7108acb877608d31a8b..e9b9d68d73fd07e5b48ed13c0b213205e0aa2728 100644 (file)
@@ -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',
index 07c85797a7caa496302e75c85fed19fd6e410d24..277422645cdaa923f88202f1ce952ac2d33f0127 100644 (file)
@@ -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<YoutubeDLInfo>
   })
 }
 
+function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> {
+  return new Promise<YoutubeDLSubs>((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