Support audio files import

pull/2315/head
Chocobozzz 2020-04-03 15:41:39 +02:00
parent 1fe654e096
commit d57d1d83c6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
6 changed files with 73 additions and 46 deletions

View File

@ -174,7 +174,10 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
videoImportId: videoImport.id, videoImportId: videoImport.id,
thumbnailUrl: youtubeDLInfo.thumbnailUrl, thumbnailUrl: youtubeDLInfo.thumbnailUrl,
downloadThumbnail: !thumbnailModel, downloadThumbnail: !thumbnailModel,
downloadPreview: !previewModel downloadPreview: !previewModel,
fileExt: youtubeDLInfo.fileExt
? `.${youtubeDLInfo.fileExt}`
: '.mp4'
} }
await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload }) await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload })

View File

@ -1,7 +1,7 @@
import * as express from 'express' import * as express from 'express'
import { extname } from 'path' import { extname } from 'path'
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
import { getVideoFileFPS, getVideoFileResolution, getMetadataFromFile } from '../../../helpers/ffmpeg-utils' import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils' import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
@ -32,13 +32,13 @@ import {
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
setDefaultSort, setDefaultSort,
videoFileMetadataGetValidator,
videosAddValidator, videosAddValidator,
videosCustomGetValidator, videosCustomGetValidator,
videosGetValidator, videosGetValidator,
videosRemoveValidator, videosRemoveValidator,
videosSortValidator, videosSortValidator,
videosUpdateValidator, videosUpdateValidator
videoFileMetadataGetValidator
} from '../../../middlewares' } from '../../../middlewares'
import { TagModel } from '../../../models/video/tag' import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -62,12 +62,12 @@ import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
import { Hooks } from '../../../lib/plugins/hooks' import { Hooks } from '../../../lib/plugins/hooks'
import { MVideoDetails, MVideoFullLight } from '@server/typings/models' import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
import { getVideoFilePath } from '@server/lib/video-paths' import { getVideoFilePath } from '@server/lib/video-paths'
import toInt from 'validator/lib/toInt' import toInt from 'validator/lib/toInt'
import { addOptimizeOrMergeAudioJob } from '@server/lib/videos'
const auditLogger = auditLoggerFactory('videos') const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router() const videosRouter = express.Router()
@ -296,25 +296,7 @@ async function addVideo (req: express.Request, res: express.Response) {
Notifier.Instance.notifyOnNewVideoIfNeeded(videoCreated) Notifier.Instance.notifyOnNewVideoIfNeeded(videoCreated)
if (video.state === VideoState.TO_TRANSCODE) { if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now await addOptimizeOrMergeAudioJob(videoCreated, videoFile)
let dataInput: VideoTranscodingPayload
if (videoFile.isAudio()) {
dataInput = {
type: 'merge-audio' as 'merge-audio',
resolution: DEFAULT_AUDIO_RESOLUTION,
videoUUID: videoCreated.uuid,
isNewVideo: true
}
} else {
dataInput = {
type: 'optimize' as 'optimize',
videoUUID: videoCreated.uuid,
isNewVideo: true
}
}
await JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput })
} }
Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) Hooks.runAction('action:api.video.uploaded', { video: videoCreated })

View File

@ -7,6 +7,7 @@ import { Instance as ParseTorrent } from 'parse-torrent'
import { remove } from 'fs-extra' import { remove } from 'fs-extra'
import * as memoizee from 'memoizee' import * as memoizee from 'memoizee'
import { CONFIG } from '../initializers/config' import { CONFIG } from '../initializers/config'
import { isVideoFileExtnameValid } from './custom-validators/videos'
function deleteFileAsync (path: string) { function deleteFileAsync (path: string) {
remove(path) remove(path)
@ -42,11 +43,18 @@ const getServerActor = memoizee(async function () {
return actor return actor
}, { promise: true }) }, { promise: true })
function generateVideoImportTmpPath (target: string | ParseTorrent) { function generateVideoImportTmpPath (target: string | ParseTorrent, extensionArg?: string) {
const id = typeof target === 'string' ? target : target.infoHash const id = typeof target === 'string'
? target
: target.infoHash
let extension = '.mp4'
if (extensionArg && isVideoFileExtnameValid(extensionArg)) {
extension = extensionArg
}
const hash = sha256(id) const hash = sha256(id)
return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') return join(CONFIG.STORAGE.TMP_DIR, `${hash}-import${extension}`)
} }
function getSecureTorrentName (originalName: string) { function getSecureTorrentName (originalName: string) {

View File

@ -16,6 +16,7 @@ export type YoutubeDLInfo = {
nsfw?: boolean nsfw?: boolean
tags?: string[] tags?: string[]
thumbnailUrl?: string thumbnailUrl?: string
fileExt?: string
originallyPublishedAt?: Date originallyPublishedAt?: Date
} }
@ -44,11 +45,11 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
}) })
} }
function downloadYoutubeDLVideo (url: string, timeout: number) { function downloadYoutubeDLVideo (url: string, extension: string, timeout: number) {
const path = generateVideoImportTmpPath(url) const path = generateVideoImportTmpPath(url, extension)
let timer let timer
logger.info('Importing youtubeDL video %s', url) logger.info('Importing youtubeDL video %s to %s', url, path)
let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
options = wrapWithProxyOptions(options) options = wrapWithProxyOptions(options)
@ -219,7 +220,8 @@ function buildVideoInfo (obj: any) {
nsfw: isNSFW(obj), nsfw: isNSFW(obj),
tags: getTags(obj.tags), tags: getTags(obj.tags),
thumbnailUrl: obj.thumbnail || undefined, thumbnailUrl: obj.thumbnail || undefined,
originallyPublishedAt: buildOriginallyPublishedAt(obj) originallyPublishedAt: buildOriginallyPublishedAt(obj),
fileExt: obj.ext
} }
} }

View File

@ -8,7 +8,6 @@ import { extname } from 'path'
import { VideoFileModel } from '../../../models/video/video-file' import { VideoFileModel } from '../../../models/video/video-file'
import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
import { VideoState } from '../../../../shared' import { VideoState } from '../../../../shared'
import { JobQueue } from '../index'
import { federateVideoIfNeeded } from '../../activitypub' import { federateVideoIfNeeded } from '../../activitypub'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
@ -22,6 +21,7 @@ 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'
import { getVideoFilePath } from '@server/lib/video-paths' import { getVideoFilePath } from '@server/lib/video-paths'
import { addOptimizeOrMergeAudioJob } from '@server/lib/videos'
type VideoImportYoutubeDLPayload = { type VideoImportYoutubeDLPayload = {
type: 'youtube-dl' type: 'youtube-dl'
@ -30,6 +30,8 @@ type VideoImportYoutubeDLPayload = {
thumbnailUrl: string thumbnailUrl: string
downloadThumbnail: boolean downloadThumbnail: boolean
downloadPreview: boolean downloadPreview: boolean
fileExt?: string
} }
type VideoImportTorrentPayload = { type VideoImportTorrentPayload = {
@ -90,7 +92,7 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
generatePreview: false generatePreview: false
} }
return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
} }
async function getVideoImportOrDie (videoImportId: number) { async function getVideoImportOrDie (videoImportId: number) {
@ -154,16 +156,28 @@ 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.downloadThumbnail && options.thumbnailUrl) {
try {
thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE) thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
} else if (options.generateThumbnail || options.downloadThumbnail) { } 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.downloadPreview && options.thumbnailUrl) {
try {
previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW) previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
} else if (options.generatePreview || options.downloadPreview) { } 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)
} }
@ -214,14 +228,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
// Create transcoding jobs? // Create transcoding jobs?
if (video.state === VideoState.TO_TRANSCODE) { if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile)
const dataInput = {
type: 'optimize' as 'optimize',
videoUUID: videoImportUpdated.Video.uuid,
isNewVideo: true
}
await JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput })
} }
} catch (err) { } catch (err) {

View File

@ -1,4 +1,7 @@
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models' import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/typings/models'
import { VideoTranscodingPayload } from '@server/lib/job-queue/handlers/video-transcoding'
import { DEFAULT_AUDIO_RESOLUTION } from '@server/initializers/constants'
import { JobQueue } from '@server/lib/job-queue'
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
return isStreamingPlaylist(videoOrPlaylist) return isStreamingPlaylist(videoOrPlaylist)
@ -6,6 +9,28 @@ function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
: videoOrPlaylist : videoOrPlaylist
} }
function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile) {
let dataInput: VideoTranscodingPayload
if (videoFile.isAudio()) {
dataInput = {
type: 'merge-audio' as 'merge-audio',
resolution: DEFAULT_AUDIO_RESOLUTION,
videoUUID: video.uuid,
isNewVideo: true
}
} else {
dataInput = {
type: 'optimize' as 'optimize',
videoUUID: video.uuid,
isNewVideo: true
}
}
return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput })
}
export { export {
addOptimizeOrMergeAudioJob,
extractVideo extractVideo
} }