mirror of https://github.com/Chocobozzz/PeerTube
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
parent
8f31261f77
commit
b1770a0af4
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 |
Loading…
Reference in New Issue