video-import.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import * as Bull from 'bull'
  2. import { move, remove, stat } from 'fs-extra'
  3. import { extname } from 'path'
  4. import { addOptimizeOrMergeAudioJob } from '@server/helpers/video'
  5. import { isPostImportVideoAccepted } from '@server/lib/moderation'
  6. import { Hooks } from '@server/lib/plugins/hooks'
  7. import { isAbleToUploadVideo } from '@server/lib/user'
  8. import { getVideoFilePath } from '@server/lib/video-paths'
  9. import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
  10. import {
  11. VideoImportPayload,
  12. VideoImportTorrentPayload,
  13. VideoImportTorrentPayloadType,
  14. VideoImportYoutubeDLPayload,
  15. VideoImportYoutubeDLPayloadType,
  16. VideoState
  17. } from '../../../../shared'
  18. import { VideoImportState } from '../../../../shared/models/videos'
  19. import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
  20. import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
  21. import { logger } from '../../../helpers/logger'
  22. import { getSecureTorrentName } from '../../../helpers/utils'
  23. import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
  24. import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl'
  25. import { CONFIG } from '../../../initializers/config'
  26. import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
  27. import { sequelizeTypescript } from '../../../initializers/database'
  28. import { VideoModel } from '../../../models/video/video'
  29. import { VideoFileModel } from '../../../models/video/video-file'
  30. import { VideoImportModel } from '../../../models/video/video-import'
  31. import { MThumbnail } from '../../../types/models/video/thumbnail'
  32. import { federateVideoIfNeeded } from '../../activitypub/videos'
  33. import { Notifier } from '../../notifier'
  34. import { generateVideoMiniature } from '../../thumbnail'
  35. async function processVideoImport (job: Bull.Job) {
  36. const payload = job.data as VideoImportPayload
  37. if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload)
  38. if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload)
  39. }
  40. // ---------------------------------------------------------------------------
  41. export {
  42. processVideoImport
  43. }
  44. // ---------------------------------------------------------------------------
  45. async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentPayload) {
  46. logger.info('Processing torrent video import in job %d.', job.id)
  47. const videoImport = await getVideoImportOrDie(payload.videoImportId)
  48. const options = {
  49. type: payload.type,
  50. videoImportId: payload.videoImportId,
  51. generateThumbnail: true,
  52. generatePreview: true
  53. }
  54. const target = {
  55. torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
  56. magnetUri: videoImport.magnetUri
  57. }
  58. return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options)
  59. }
  60. async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutubeDLPayload) {
  61. logger.info('Processing youtubeDL video import in job %d.', job.id)
  62. const videoImport = await getVideoImportOrDie(payload.videoImportId)
  63. const options = {
  64. type: payload.type,
  65. videoImportId: videoImport.id,
  66. generateThumbnail: payload.generateThumbnail,
  67. generatePreview: payload.generatePreview
  68. }
  69. return processFile(
  70. () => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT, payload.mergeExt),
  71. videoImport,
  72. options
  73. )
  74. }
  75. async function getVideoImportOrDie (videoImportId: number) {
  76. const videoImport = await VideoImportModel.loadAndPopulateVideo(videoImportId)
  77. if (!videoImport || !videoImport.Video) {
  78. throw new Error('Cannot import video %s: the video import or video linked to this import does not exist anymore.')
  79. }
  80. return videoImport
  81. }
  82. type ProcessFileOptions = {
  83. type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType
  84. videoImportId: number
  85. generateThumbnail: boolean
  86. generatePreview: boolean
  87. }
  88. async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
  89. let tempVideoPath: string
  90. let videoDestFile: string
  91. let videoFile: VideoFileModel
  92. try {
  93. // Download video from youtubeDL
  94. tempVideoPath = await downloader()
  95. // Get information about this video
  96. const stats = await stat(tempVideoPath)
  97. const isAble = await isAbleToUploadVideo(videoImport.User.id, stats.size)
  98. if (isAble === false) {
  99. throw new Error('The user video quota is exceeded with this video to import.')
  100. }
  101. const { videoFileResolution } = await getVideoFileResolution(tempVideoPath)
  102. const fps = await getVideoFileFPS(tempVideoPath)
  103. const duration = await getDurationFromVideoFile(tempVideoPath)
  104. // Prepare video file object for creation in database
  105. const videoFileData = {
  106. extname: extname(tempVideoPath),
  107. resolution: videoFileResolution,
  108. size: stats.size,
  109. fps,
  110. videoId: videoImport.videoId
  111. }
  112. videoFile = new VideoFileModel(videoFileData)
  113. const hookName = options.type === 'youtube-dl'
  114. ? 'filter:api.video.post-import-url.accept.result'
  115. : 'filter:api.video.post-import-torrent.accept.result'
  116. // Check we accept this video
  117. const acceptParameters = {
  118. videoImport,
  119. video: videoImport.Video,
  120. videoFilePath: tempVideoPath,
  121. videoFile,
  122. user: videoImport.User
  123. }
  124. const acceptedResult = await Hooks.wrapFun(isPostImportVideoAccepted, acceptParameters, hookName)
  125. if (acceptedResult.accepted !== true) {
  126. logger.info('Refused imported video.', { acceptedResult, acceptParameters })
  127. videoImport.state = VideoImportState.REJECTED
  128. await videoImport.save()
  129. throw new Error(acceptedResult.errorMessage)
  130. }
  131. // Video is accepted, resuming preparation
  132. const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ], VideoStreamingPlaylists: [] })
  133. // To clean files if the import fails
  134. const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
  135. // Move file
  136. videoDestFile = getVideoFilePath(videoImportWithFiles.Video, videoFile)
  137. await move(tempVideoPath, videoDestFile)
  138. tempVideoPath = null // This path is not used anymore
  139. // Process thumbnail
  140. let thumbnailModel: MThumbnail
  141. if (options.generateThumbnail) {
  142. thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
  143. }
  144. // Process preview
  145. let previewModel: MThumbnail
  146. if (options.generatePreview) {
  147. previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
  148. }
  149. // Create torrent
  150. await createTorrentAndSetInfoHash(videoImportWithFiles.Video, videoFile)
  151. const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => {
  152. const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
  153. // Refresh video
  154. const video = await VideoModel.load(videoImportToUpdate.videoId, t)
  155. if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.')
  156. const videoFileCreated = await videoFile.save({ transaction: t })
  157. videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] })
  158. // Update video DB object
  159. video.duration = duration
  160. video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
  161. await video.save({ transaction: t })
  162. if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
  163. if (previewModel) await video.addAndSaveThumbnail(previewModel, t)
  164. // Now we can federate the video (reload from database, we need more attributes)
  165. const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
  166. await federateVideoIfNeeded(videoForFederation, true, t)
  167. // Update video import object
  168. videoImportToUpdate.state = VideoImportState.SUCCESS
  169. const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo
  170. videoImportUpdated.Video = video
  171. logger.info('Video %s imported.', video.uuid)
  172. return { videoImportUpdated, video: videoForFederation }
  173. })
  174. Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
  175. if (video.isBlacklisted()) {
  176. const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
  177. Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
  178. } else {
  179. Notifier.Instance.notifyOnNewVideoIfNeeded(video)
  180. }
  181. // Create transcoding jobs?
  182. if (video.state === VideoState.TO_TRANSCODE) {
  183. await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile)
  184. }
  185. } catch (err) {
  186. try {
  187. if (tempVideoPath) await remove(tempVideoPath)
  188. } catch (errUnlink) {
  189. logger.warn('Cannot cleanup files after a video import error.', { err: errUnlink })
  190. }
  191. videoImport.error = err.message
  192. if (videoImport.state !== VideoImportState.REJECTED) {
  193. videoImport.state = VideoImportState.FAILED
  194. }
  195. await videoImport.save()
  196. Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false)
  197. throw err
  198. }
  199. }