Add thumbnail / preview generation from url on the fly (#2646)
authorKim <1877318+kimsible@users.noreply.github.com>
Mon, 20 Apr 2020 08:28:38 +0000 (10:28 +0200)
committerGitHub <noreply@github.com>
Mon, 20 Apr 2020 08:28:38 +0000 (10:28 +0200)
* Add thumbnails generation on the fly to URL import

* Display generated preview to import first edit

* Use ternary to get type inference

* Move preview/thumbnail test just after import

Co-authored-by: kimsible <kimsible@users.noreply.github.com>
client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
server/controllers/api/videos/import.ts
server/lib/job-queue/handlers/video-import.ts
server/tests/api/videos/video-imports.ts
server/tests/fixtures/video_import_preview.jpg [new file with mode: 0644]
server/tests/fixtures/video_import_thumbnail.jpg [new file with mode: 0644]

index a17d736834319dc32db1eb7351b557a5ec5b7a59..213c42333dd1a6ae38cf0248c7cd8f5587689a1c 100644 (file)
@@ -11,7 +11,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
 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 { scrollToTop, getAbsoluteAPIUrl } from '@app/shared/misc/utils'
 import { switchMap, map } from 'rxjs/operators'
 
 @Component({
@@ -95,12 +95,22 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
             this.isImportingVideo = false
             this.hasImportedVideo = true
 
+            const absoluteAPIUrl = getAbsoluteAPIUrl()
+
+            const thumbnailUrl = video.thumbnailPath
+              ? absoluteAPIUrl + video.thumbnailPath
+              : null
+
+            const previewUrl = video.previewPath
+              ? absoluteAPIUrl + video.previewPath
+              : null
+
             this.video = new VideoEdit(Object.assign(video, {
               commentsEnabled: videoUpdate.commentsEnabled,
               downloadEnabled: videoUpdate.downloadEnabled,
               support: null,
-              thumbnailUrl: null,
-              previewUrl: null
+              thumbnailUrl,
+              previewUrl
             }))
 
             this.videoCaptions = videoCaptions
@@ -147,5 +157,26 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
 
   private hydrateFormFromVideo () {
     this.form.patchValue(this.video.toFormPatch())
+
+    const objects = [
+      {
+        url: 'thumbnailUrl',
+        name: 'thumbnailfile'
+      },
+      {
+        url: 'previewUrl',
+        name: 'previewfile'
+      }
+    ]
+
+    for (const obj of objects) {
+      fetch(this.video[obj.url])
+        .then(response => response.blob())
+        .then(data => {
+          this.form.patchValue({
+            [ obj.name ]: data
+          })
+        })
+    }
   }
 }
index f4630375eea7cc4c770faf0d6813c864b33e18a9..fb2de5dc0be932d1641818bb904180a9e4faeeb9 100644 (file)
@@ -23,7 +23,7 @@ import { move, readFile } from 'fs-extra'
 import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
+import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 import {
   MChannelAccountDefault,
@@ -153,8 +153,25 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
 
   const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
 
-  const thumbnailModel = await processThumbnail(req, video)
-  const previewModel = await processPreview(req, video)
+  let thumbnailModel: MThumbnail
+
+  // Process video thumbnail from request.files
+  thumbnailModel = await processThumbnail(req, video)
+
+  // Process video thumbnail from url if processing from request.files failed
+  if (!thumbnailModel) {
+    thumbnailModel = await processThumbnailFromUrl(youtubeDLInfo.thumbnailUrl, video)
+  }
+
+  let previewModel: MThumbnail
+
+  // Process video preview from request.files
+  previewModel = await processPreview(req, video)
+
+  // Process video preview from url if processing from request.files failed
+  if (!previewModel) {
+    previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
+  }
 
   const tags = body.tags || youtubeDLInfo.tags
   const videoImportAttributes = {
@@ -200,9 +217,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
   const payload = {
     type: 'youtube-dl' as 'youtube-dl',
     videoImportId: videoImport.id,
-    thumbnailUrl: youtubeDLInfo.thumbnailUrl,
-    downloadThumbnail: !thumbnailModel,
-    downloadPreview: !previewModel,
+    generateThumbnail: !thumbnailModel,
+    generatePreview: !previewModel,
     fileExt: youtubeDLInfo.fileExt
       ? `.${youtubeDLInfo.fileExt}`
       : '.mp4'
@@ -261,6 +277,24 @@ async function processPreview (req: express.Request, video: VideoModel) {
   return undefined
 }
 
+async function processThumbnailFromUrl (url: string, video: VideoModel) {
+  try {
+    return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE)
+  } catch (err) {
+    logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err })
+    return undefined
+  }
+}
+
+async function processPreviewFromUrl (url: string, video: VideoModel) {
+  try {
+    return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW)
+  } catch (err) {
+    logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err })
+    return undefined
+  }
+}
+
 function insertIntoDB (parameters: {
   video: MVideoThumbnailAccountDefault
   thumbnailModel: MThumbnail
index d8052da723bf834ffbc148ce480a32a51b6e586e..6cdae5b03a3729e3e9d2ddda71c1c67c73bb1f84 100644 (file)
@@ -16,7 +16,7 @@ import { move, remove, stat } from 'fs-extra'
 import { Notifier } from '../../notifier'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
+import { generateVideoMiniature } from '../../thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 import { MThumbnail } from '../../../typings/models/video/thumbnail'
 import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
@@ -27,9 +27,8 @@ type VideoImportYoutubeDLPayload = {
   type: 'youtube-dl'
   videoImportId: number
 
-  thumbnailUrl: string
-  downloadThumbnail: boolean
-  downloadPreview: boolean
+  generateThumbnail: boolean
+  generatePreview: boolean
 
   fileExt?: string
 }
@@ -64,9 +63,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
   const options = {
     videoImportId: payload.videoImportId,
 
-    downloadThumbnail: false,
-    downloadPreview: false,
-
     generateThumbnail: true,
     generatePreview: true
   }
@@ -84,12 +80,8 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
   const options = {
     videoImportId: videoImport.id,
 
-    downloadThumbnail: payload.downloadThumbnail,
-    downloadPreview: payload.downloadPreview,
-    thumbnailUrl: payload.thumbnailUrl,
-
-    generateThumbnail: false,
-    generatePreview: false
+    generateThumbnail: payload.generateThumbnail,
+    generatePreview: payload.generatePreview
   }
 
   return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
@@ -107,10 +99,6 @@ async function getVideoImportOrDie (videoImportId: number) {
 type ProcessFileOptions = {
   videoImportId: number
 
-  downloadThumbnail: boolean
-  downloadPreview: boolean
-  thumbnailUrl?: string
-
   generateThumbnail: boolean
   generatePreview: boolean
 }
@@ -155,29 +143,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
 
     // Process thumbnail
     let thumbnailModel: MThumbnail
-    if (options.downloadThumbnail && options.thumbnailUrl) {
-      try {
-        thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
-      } catch (err) {
-        logger.warn('Cannot generate video thumbnail %s for %s.', options.thumbnailUrl, videoImportWithFiles.Video.url, { err })
-      }
-    }
-
-    if (!thumbnailModel && (options.generateThumbnail || options.downloadThumbnail)) {
+    if (options.generateThumbnail) {
       thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
     }
 
     // Process preview
     let previewModel: MThumbnail
-    if (options.downloadPreview && options.thumbnailUrl) {
-      try {
-        previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
-      } catch (err) {
-        logger.warn('Cannot generate video preview %s for %s.', options.thumbnailUrl, videoImportWithFiles.Video.url, { err })
-      }
-    }
-
-    if (!previewModel && (options.generatePreview || options.downloadPreview)) {
+    if (options.generatePreview) {
       previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
     }
 
index 8e179b8254bc50129b9c280b4b28f58b6e46dfa9..4d5989f43995e25a377e877f56e2528f4ae20510 100644 (file)
@@ -19,6 +19,7 @@ import {
 } from '../../../../shared/extra-utils'
 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
+import { testImage } from '../../../../shared/extra-utils/miscs/miscs'
 
 const expect = chai.expect
 
@@ -118,6 +119,10 @@ describe('Test video imports', function () {
       const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() })
       const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
       expect(res.body.video.name).to.equal('small video - youtube')
+      expect(res.body.video.thumbnailPath).to.equal(`/static/thumbnails/${res.body.video.uuid}.jpg`)
+      expect(res.body.video.previewPath).to.equal(`/static/previews/${res.body.video.uuid}.jpg`)
+      await testImage(servers[0].url, 'video_import_thumbnail', res.body.video.thumbnailPath)
+      await testImage(servers[0].url, 'video_import_preview', res.body.video.previewPath)
 
       const resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id)
       const videoCaptions: VideoCaption[] = resCaptions.body.data
diff --git a/server/tests/fixtures/video_import_preview.jpg b/server/tests/fixtures/video_import_preview.jpg
new file mode 100644 (file)
index 0000000..1f8d1d9
Binary files /dev/null and b/server/tests/fixtures/video_import_preview.jpg differ
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg
new file mode 100644 (file)
index 0000000..fcc50b7
Binary files /dev/null and b/server/tests/fixtures/video_import_thumbnail.jpg differ