aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKim <1877318+kimsible@users.noreply.github.com>2020-04-20 10:28:38 +0200
committerGitHub <noreply@github.com>2020-04-20 10:28:38 +0200
commitb1770a0af464ad6350d156245b1abcc1395e142e (patch)
tree0712a2cd912ecd3f41084134d513920a6f723fb6
parent8f31261f77c6e521917b3f629b223ccc8df50960 (diff)
downloadPeerTube-b1770a0af464ad6350d156245b1abcc1395e142e.tar.gz
PeerTube-b1770a0af464ad6350d156245b1abcc1395e142e.tar.zst
PeerTube-b1770a0af464ad6350d156245b1abcc1395e142e.zip
Add thumbnail / preview generation from url on the fly (#2646)
* 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>
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts37
-rw-r--r--server/controllers/api/videos/import.ts46
-rw-r--r--server/lib/job-queue/handlers/video-import.ts42
-rw-r--r--server/tests/api/videos/video-imports.ts5
-rw-r--r--server/tests/fixtures/video_import_preview.jpgbin0 -> 37360 bytes
-rw-r--r--server/tests/fixtures/video_import_thumbnail.jpgbin0 -> 5885 bytes
6 files changed, 86 insertions, 44 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 a17d73683..213c42333 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
@@ -11,7 +11,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
11import { FormValidatorService } from '@app/shared' 11import { FormValidatorService } from '@app/shared'
12import { VideoCaptionService } from '@app/shared/video-caption' 12import { VideoCaptionService } from '@app/shared/video-caption'
13import { VideoImportService } from '@app/shared/video-import' 13import { VideoImportService } from '@app/shared/video-import'
14import { scrollToTop } from '@app/shared/misc/utils' 14import { scrollToTop, getAbsoluteAPIUrl } from '@app/shared/misc/utils'
15import { switchMap, map } from 'rxjs/operators' 15import { switchMap, map } from 'rxjs/operators'
16 16
17@Component({ 17@Component({
@@ -95,12 +95,22 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
95 this.isImportingVideo = false 95 this.isImportingVideo = false
96 this.hasImportedVideo = true 96 this.hasImportedVideo = true
97 97
98 const absoluteAPIUrl = getAbsoluteAPIUrl()
99
100 const thumbnailUrl = video.thumbnailPath
101 ? absoluteAPIUrl + video.thumbnailPath
102 : null
103
104 const previewUrl = video.previewPath
105 ? absoluteAPIUrl + video.previewPath
106 : null
107
98 this.video = new VideoEdit(Object.assign(video, { 108 this.video = new VideoEdit(Object.assign(video, {
99 commentsEnabled: videoUpdate.commentsEnabled, 109 commentsEnabled: videoUpdate.commentsEnabled,
100 downloadEnabled: videoUpdate.downloadEnabled, 110 downloadEnabled: videoUpdate.downloadEnabled,
101 support: null, 111 support: null,
102 thumbnailUrl: null, 112 thumbnailUrl,
103 previewUrl: null 113 previewUrl
104 })) 114 }))
105 115
106 this.videoCaptions = videoCaptions 116 this.videoCaptions = videoCaptions
@@ -147,5 +157,26 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
147 157
148 private hydrateFormFromVideo () { 158 private hydrateFormFromVideo () {
149 this.form.patchValue(this.video.toFormPatch()) 159 this.form.patchValue(this.video.toFormPatch())
160
161 const objects = [
162 {
163 url: 'thumbnailUrl',
164 name: 'thumbnailfile'
165 },
166 {
167 url: 'previewUrl',
168 name: 'previewfile'
169 }
170 ]
171
172 for (const obj of objects) {
173 fetch(this.video[obj.url])
174 .then(response => response.blob())
175 .then(data => {
176 this.form.patchValue({
177 [ obj.name ]: data
178 })
179 })
180 }
150 } 181 }
151} 182}
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index f4630375e..fb2de5dc0 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -23,7 +23,7 @@ import { move, readFile } from 'fs-extra'
23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
24import { CONFIG } from '../../../initializers/config' 24import { CONFIG } from '../../../initializers/config'
25import { sequelizeTypescript } from '../../../initializers/database' 25import { sequelizeTypescript } from '../../../initializers/database'
26import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' 26import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail'
27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
28import { 28import {
29 MChannelAccountDefault, 29 MChannelAccountDefault,
@@ -153,8 +153,25 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
153 153
154 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 154 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
155 155
156 const thumbnailModel = await processThumbnail(req, video) 156 let thumbnailModel: MThumbnail
157 const previewModel = await processPreview(req, video) 157
158 // Process video thumbnail from request.files
159 thumbnailModel = await processThumbnail(req, video)
160
161 // Process video thumbnail from url if processing from request.files failed
162 if (!thumbnailModel) {
163 thumbnailModel = await processThumbnailFromUrl(youtubeDLInfo.thumbnailUrl, video)
164 }
165
166 let previewModel: MThumbnail
167
168 // Process video preview from request.files
169 previewModel = await processPreview(req, video)
170
171 // Process video preview from url if processing from request.files failed
172 if (!previewModel) {
173 previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
174 }
158 175
159 const tags = body.tags || youtubeDLInfo.tags 176 const tags = body.tags || youtubeDLInfo.tags
160 const videoImportAttributes = { 177 const videoImportAttributes = {
@@ -200,9 +217,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
200 const payload = { 217 const payload = {
201 type: 'youtube-dl' as 'youtube-dl', 218 type: 'youtube-dl' as 'youtube-dl',
202 videoImportId: videoImport.id, 219 videoImportId: videoImport.id,
203 thumbnailUrl: youtubeDLInfo.thumbnailUrl, 220 generateThumbnail: !thumbnailModel,
204 downloadThumbnail: !thumbnailModel, 221 generatePreview: !previewModel,
205 downloadPreview: !previewModel,
206 fileExt: youtubeDLInfo.fileExt 222 fileExt: youtubeDLInfo.fileExt
207 ? `.${youtubeDLInfo.fileExt}` 223 ? `.${youtubeDLInfo.fileExt}`
208 : '.mp4' 224 : '.mp4'
@@ -261,6 +277,24 @@ async function processPreview (req: express.Request, video: VideoModel) {
261 return undefined 277 return undefined
262} 278}
263 279
280async function processThumbnailFromUrl (url: string, video: VideoModel) {
281 try {
282 return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE)
283 } catch (err) {
284 logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err })
285 return undefined
286 }
287}
288
289async function processPreviewFromUrl (url: string, video: VideoModel) {
290 try {
291 return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW)
292 } catch (err) {
293 logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err })
294 return undefined
295 }
296}
297
264function insertIntoDB (parameters: { 298function insertIntoDB (parameters: {
265 video: MVideoThumbnailAccountDefault 299 video: MVideoThumbnailAccountDefault
266 thumbnailModel: MThumbnail 300 thumbnailModel: MThumbnail
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index d8052da72..6cdae5b03 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -16,7 +16,7 @@ import { move, remove, stat } from 'fs-extra'
16import { Notifier } from '../../notifier' 16import { Notifier } from '../../notifier'
17import { CONFIG } from '../../../initializers/config' 17import { CONFIG } from '../../../initializers/config'
18import { sequelizeTypescript } from '../../../initializers/database' 18import { sequelizeTypescript } from '../../../initializers/database'
19import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' 19import { generateVideoMiniature } from '../../thumbnail'
20import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 20import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
21import { MThumbnail } from '../../../typings/models/video/thumbnail' 21import { MThumbnail } from '../../../typings/models/video/thumbnail'
22import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' 22import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
@@ -27,9 +27,8 @@ type VideoImportYoutubeDLPayload = {
27 type: 'youtube-dl' 27 type: 'youtube-dl'
28 videoImportId: number 28 videoImportId: number
29 29
30 thumbnailUrl: string 30 generateThumbnail: boolean
31 downloadThumbnail: boolean 31 generatePreview: boolean
32 downloadPreview: boolean
33 32
34 fileExt?: string 33 fileExt?: string
35} 34}
@@ -64,9 +63,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
64 const options = { 63 const options = {
65 videoImportId: payload.videoImportId, 64 videoImportId: payload.videoImportId,
66 65
67 downloadThumbnail: false,
68 downloadPreview: false,
69
70 generateThumbnail: true, 66 generateThumbnail: true,
71 generatePreview: true 67 generatePreview: true
72 } 68 }
@@ -84,12 +80,8 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
84 const options = { 80 const options = {
85 videoImportId: videoImport.id, 81 videoImportId: videoImport.id,
86 82
87 downloadThumbnail: payload.downloadThumbnail, 83 generateThumbnail: payload.generateThumbnail,
88 downloadPreview: payload.downloadPreview, 84 generatePreview: payload.generatePreview
89 thumbnailUrl: payload.thumbnailUrl,
90
91 generateThumbnail: false,
92 generatePreview: false
93 } 85 }
94 86
95 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options) 87 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
@@ -107,10 +99,6 @@ async function getVideoImportOrDie (videoImportId: number) {
107type ProcessFileOptions = { 99type ProcessFileOptions = {
108 videoImportId: number 100 videoImportId: number
109 101
110 downloadThumbnail: boolean
111 downloadPreview: boolean
112 thumbnailUrl?: string
113
114 generateThumbnail: boolean 102 generateThumbnail: boolean
115 generatePreview: boolean 103 generatePreview: boolean
116} 104}
@@ -155,29 +143,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
155 143
156 // Process thumbnail 144 // Process thumbnail
157 let thumbnailModel: MThumbnail 145 let thumbnailModel: MThumbnail
158 if (options.downloadThumbnail && options.thumbnailUrl) { 146 if (options.generateThumbnail) {
159 try {
160 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
161 } catch (err) {
162 logger.warn('Cannot generate video thumbnail %s for %s.', options.thumbnailUrl, videoImportWithFiles.Video.url, { err })
163 }
164 }
165
166 if (!thumbnailModel && (options.generateThumbnail || options.downloadThumbnail)) {
167 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) 147 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
168 } 148 }
169 149
170 // Process preview 150 // Process preview
171 let previewModel: MThumbnail 151 let previewModel: MThumbnail
172 if (options.downloadPreview && options.thumbnailUrl) { 152 if (options.generatePreview) {
173 try {
174 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
175 } catch (err) {
176 logger.warn('Cannot generate video preview %s for %s.', options.thumbnailUrl, videoImportWithFiles.Video.url, { err })
177 }
178 }
179
180 if (!previewModel && (options.generatePreview || options.downloadPreview)) {
181 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) 153 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
182 } 154 }
183 155
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index 8e179b825..4d5989f43 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -19,6 +19,7 @@ import {
19} from '../../../../shared/extra-utils' 19} from '../../../../shared/extra-utils'
20import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' 21import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
22import { testImage } from '../../../../shared/extra-utils/miscs/miscs'
22 23
23const expect = chai.expect 24const expect = chai.expect
24 25
@@ -118,6 +119,10 @@ describe('Test video imports', function () {
118 const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() }) 119 const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() })
119 const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) 120 const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
120 expect(res.body.video.name).to.equal('small video - youtube') 121 expect(res.body.video.name).to.equal('small video - youtube')
122 expect(res.body.video.thumbnailPath).to.equal(`/static/thumbnails/${res.body.video.uuid}.jpg`)
123 expect(res.body.video.previewPath).to.equal(`/static/previews/${res.body.video.uuid}.jpg`)
124 await testImage(servers[0].url, 'video_import_thumbnail', res.body.video.thumbnailPath)
125 await testImage(servers[0].url, 'video_import_preview', res.body.video.previewPath)
121 126
122 const resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id) 127 const resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id)
123 const videoCaptions: VideoCaption[] = resCaptions.body.data 128 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
index 000000000..1f8d1d91d
--- /dev/null
+++ b/server/tests/fixtures/video_import_preview.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg
new file mode 100644
index 000000000..fcc50b75f
--- /dev/null
+++ b/server/tests/fixtures/video_import_thumbnail.jpg
Binary files differ