diff options
author | Kim <1877318+kimsible@users.noreply.github.com> | 2020-04-20 10:28:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-20 10:28:38 +0200 |
commit | b1770a0af464ad6350d156245b1abcc1395e142e (patch) | |
tree | 0712a2cd912ecd3f41084134d513920a6f723fb6 | |
parent | 8f31261f77c6e521917b3f629b223ccc8df50960 (diff) | |
download | PeerTube-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.ts | 37 | ||||
-rw-r--r-- | server/controllers/api/videos/import.ts | 46 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-import.ts | 42 | ||||
-rw-r--r-- | server/tests/api/videos/video-imports.ts | 5 | ||||
-rw-r--r-- | server/tests/fixtures/video_import_preview.jpg | bin | 0 -> 37360 bytes | |||
-rw-r--r-- | server/tests/fixtures/video_import_thumbnail.jpg | bin | 0 -> 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' | |||
11 | import { FormValidatorService } from '@app/shared' | 11 | import { FormValidatorService } from '@app/shared' |
12 | import { VideoCaptionService } from '@app/shared/video-caption' | 12 | import { VideoCaptionService } from '@app/shared/video-caption' |
13 | import { VideoImportService } from '@app/shared/video-import' | 13 | import { VideoImportService } from '@app/shared/video-import' |
14 | import { scrollToTop } from '@app/shared/misc/utils' | 14 | import { scrollToTop, getAbsoluteAPIUrl } from '@app/shared/misc/utils' |
15 | import { switchMap, map } from 'rxjs/operators' | 15 | import { 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' | |||
23 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 23 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
24 | import { CONFIG } from '../../../initializers/config' | 24 | import { CONFIG } from '../../../initializers/config' |
25 | import { sequelizeTypescript } from '../../../initializers/database' | 25 | import { sequelizeTypescript } from '../../../initializers/database' |
26 | import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 26 | import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail' |
27 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 27 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
28 | import { | 28 | import { |
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 | ||
280 | async 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 | |||
289 | async 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 | |||
264 | function insertIntoDB (parameters: { | 298 | function 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' | |||
16 | import { Notifier } from '../../notifier' | 16 | import { Notifier } from '../../notifier' |
17 | import { CONFIG } from '../../../initializers/config' | 17 | import { CONFIG } from '../../../initializers/config' |
18 | import { sequelizeTypescript } from '../../../initializers/database' | 18 | import { sequelizeTypescript } from '../../../initializers/database' |
19 | import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' | 19 | import { generateVideoMiniature } from '../../thumbnail' |
20 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 20 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
21 | import { MThumbnail } from '../../../typings/models/video/thumbnail' | 21 | import { MThumbnail } from '../../../typings/models/video/thumbnail' |
22 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' | 22 | import { 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) { | |||
107 | type ProcessFileOptions = { | 99 | type 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' |
20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
21 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' | 21 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' |
22 | import { testImage } from '../../../../shared/extra-utils/miscs/miscs' | ||
22 | 23 | ||
23 | const expect = chai.expect | 24 | const 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 | |||