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>
pull/2669/head
Kim 2020-04-20 10:28:38 +02:00 committed by GitHub
parent 8f31261f77
commit b1770a0af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 86 additions and 44 deletions

View File

@ -11,7 +11,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
import { FormValidatorService } from '@app/shared' import { FormValidatorService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption' import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoImportService } from '@app/shared/video-import' 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' import { switchMap, map } from 'rxjs/operators'
@Component({ @Component({
@ -95,12 +95,22 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
this.isImportingVideo = false this.isImportingVideo = false
this.hasImportedVideo = true 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, { this.video = new VideoEdit(Object.assign(video, {
commentsEnabled: videoUpdate.commentsEnabled, commentsEnabled: videoUpdate.commentsEnabled,
downloadEnabled: videoUpdate.downloadEnabled, downloadEnabled: videoUpdate.downloadEnabled,
support: null, support: null,
thumbnailUrl: null, thumbnailUrl,
previewUrl: null previewUrl
})) }))
this.videoCaptions = videoCaptions this.videoCaptions = videoCaptions
@ -147,5 +157,26 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
private hydrateFormFromVideo () { private hydrateFormFromVideo () {
this.form.patchValue(this.video.toFormPatch()) 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
})
})
}
} }
} }

View File

@ -23,7 +23,7 @@ import { move, readFile } from 'fs-extra'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import { CONFIG } from '../../../initializers/config' import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database' 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 { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { import {
MChannelAccountDefault, MChannelAccountDefault,
@ -153,8 +153,25 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
const thumbnailModel = await processThumbnail(req, video) let thumbnailModel: MThumbnail
const previewModel = await processPreview(req, video)
// 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 tags = body.tags || youtubeDLInfo.tags
const videoImportAttributes = { const videoImportAttributes = {
@ -200,9 +217,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
const payload = { const payload = {
type: 'youtube-dl' as 'youtube-dl', type: 'youtube-dl' as 'youtube-dl',
videoImportId: videoImport.id, videoImportId: videoImport.id,
thumbnailUrl: youtubeDLInfo.thumbnailUrl, generateThumbnail: !thumbnailModel,
downloadThumbnail: !thumbnailModel, generatePreview: !previewModel,
downloadPreview: !previewModel,
fileExt: youtubeDLInfo.fileExt fileExt: youtubeDLInfo.fileExt
? `.${youtubeDLInfo.fileExt}` ? `.${youtubeDLInfo.fileExt}`
: '.mp4' : '.mp4'
@ -261,6 +277,24 @@ async function processPreview (req: express.Request, video: VideoModel) {
return undefined 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: { function insertIntoDB (parameters: {
video: MVideoThumbnailAccountDefault video: MVideoThumbnailAccountDefault
thumbnailModel: MThumbnail thumbnailModel: MThumbnail

View File

@ -16,7 +16,7 @@ import { move, remove, stat } from 'fs-extra'
import { Notifier } from '../../notifier' import { Notifier } from '../../notifier'
import { CONFIG } from '../../../initializers/config' import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' import { generateVideoMiniature } from '../../thumbnail'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { MThumbnail } from '../../../typings/models/video/thumbnail' import { MThumbnail } from '../../../typings/models/video/thumbnail'
import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
@ -27,9 +27,8 @@ type VideoImportYoutubeDLPayload = {
type: 'youtube-dl' type: 'youtube-dl'
videoImportId: number videoImportId: number
thumbnailUrl: string generateThumbnail: boolean
downloadThumbnail: boolean generatePreview: boolean
downloadPreview: boolean
fileExt?: string fileExt?: string
} }
@ -64,9 +63,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
const options = { const options = {
videoImportId: payload.videoImportId, videoImportId: payload.videoImportId,
downloadThumbnail: false,
downloadPreview: false,
generateThumbnail: true, generateThumbnail: true,
generatePreview: true generatePreview: true
} }
@ -84,12 +80,8 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
const options = { const options = {
videoImportId: videoImport.id, videoImportId: videoImport.id,
downloadThumbnail: payload.downloadThumbnail, generateThumbnail: payload.generateThumbnail,
downloadPreview: payload.downloadPreview, generatePreview: payload.generatePreview
thumbnailUrl: payload.thumbnailUrl,
generateThumbnail: false,
generatePreview: false
} }
return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options) return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
@ -107,10 +99,6 @@ async function getVideoImportOrDie (videoImportId: number) {
type ProcessFileOptions = { type ProcessFileOptions = {
videoImportId: number videoImportId: number
downloadThumbnail: boolean
downloadPreview: boolean
thumbnailUrl?: string
generateThumbnail: boolean generateThumbnail: boolean
generatePreview: boolean generatePreview: boolean
} }
@ -155,29 +143,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
// Process thumbnail // Process thumbnail
let thumbnailModel: MThumbnail let thumbnailModel: MThumbnail
if (options.downloadThumbnail && options.thumbnailUrl) { if (options.generateThumbnail) {
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)) {
thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
} }
// Process preview // Process preview
let previewModel: MThumbnail let previewModel: MThumbnail
if (options.downloadPreview && options.thumbnailUrl) { if (options.generatePreview) {
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)) {
previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
} }

View File

@ -19,6 +19,7 @@ import {
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
import { testImage } from '../../../../shared/extra-utils/miscs/miscs'
const expect = chai.expect const expect = chai.expect
@ -118,6 +119,10 @@ describe('Test video imports', function () {
const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() }) const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() })
const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) 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.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 resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id)
const videoCaptions: VideoCaption[] = resCaptions.body.data const videoCaptions: VideoCaption[] = resCaptions.body.data

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB