import.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import * as express from 'express'
  2. import * as magnetUtil from 'magnet-uri'
  3. import 'multer'
  4. import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
  5. import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
  6. import { MIMETYPES } from '../../../initializers/constants'
  7. import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
  8. import { createReqFiles } from '../../../helpers/express-utils'
  9. import { logger } from '../../../helpers/logger'
  10. import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
  11. import { VideoModel } from '../../../models/video/video'
  12. import { getVideoActivityPubUrl } from '../../../lib/activitypub'
  13. import { TagModel } from '../../../models/video/tag'
  14. import { VideoImportModel } from '../../../models/video/video-import'
  15. import { JobQueue } from '../../../lib/job-queue/job-queue'
  16. import { join } from 'path'
  17. import { isArray } from '../../../helpers/custom-validators/misc'
  18. import { VideoChannelModel } from '../../../models/video/video-channel'
  19. import * as Bluebird from 'bluebird'
  20. import * as parseTorrent from 'parse-torrent'
  21. import { getSecureTorrentName } from '../../../helpers/utils'
  22. import { move, readFile } from 'fs-extra'
  23. import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
  24. import { CONFIG } from '../../../initializers/config'
  25. import { sequelizeTypescript } from '../../../initializers/database'
  26. import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
  27. import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
  28. import { ThumbnailModel } from '../../../models/video/thumbnail'
  29. import { UserModel } from '../../../models/account/user'
  30. const auditLogger = auditLoggerFactory('video-imports')
  31. const videoImportsRouter = express.Router()
  32. const reqVideoFileImport = createReqFiles(
  33. [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
  34. Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
  35. {
  36. thumbnailfile: CONFIG.STORAGE.TMP_DIR,
  37. previewfile: CONFIG.STORAGE.TMP_DIR,
  38. torrentfile: CONFIG.STORAGE.TMP_DIR
  39. }
  40. )
  41. videoImportsRouter.post('/imports',
  42. authenticate,
  43. reqVideoFileImport,
  44. asyncMiddleware(videoImportAddValidator),
  45. asyncRetryTransactionMiddleware(addVideoImport)
  46. )
  47. // ---------------------------------------------------------------------------
  48. export {
  49. videoImportsRouter
  50. }
  51. // ---------------------------------------------------------------------------
  52. function addVideoImport (req: express.Request, res: express.Response) {
  53. if (req.body.targetUrl) return addYoutubeDLImport(req, res)
  54. const file = req.files && req.files['torrentfile'] ? req.files['torrentfile'][0] : undefined
  55. if (req.body.magnetUri || file) return addTorrentImport(req, res, file)
  56. }
  57. async function addTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
  58. const body: VideoImportCreate = req.body
  59. const user = res.locals.oauth.token.User
  60. let videoName: string
  61. let torrentName: string
  62. let magnetUri: string
  63. if (torrentfile) {
  64. torrentName = torrentfile.originalname
  65. // Rename the torrent to a secured name
  66. const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
  67. await move(torrentfile.path, newTorrentPath)
  68. torrentfile.path = newTorrentPath
  69. const buf = await readFile(torrentfile.path)
  70. const parsedTorrent = parseTorrent(buf)
  71. videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[ 0 ] : parsedTorrent.name as string
  72. } else {
  73. magnetUri = body.magnetUri
  74. const parsed = magnetUtil.decode(magnetUri)
  75. videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
  76. }
  77. const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
  78. const thumbnailModel = await processThumbnail(req, video)
  79. const previewModel = await processPreview(req, video)
  80. const tags = body.tags || undefined
  81. const videoImportAttributes = {
  82. magnetUri,
  83. torrentName,
  84. state: VideoImportState.PENDING,
  85. userId: user.id
  86. }
  87. const videoImport = await insertIntoDB({
  88. video,
  89. thumbnailModel,
  90. previewModel,
  91. videoChannel: res.locals.videoChannel,
  92. tags,
  93. videoImportAttributes,
  94. user
  95. })
  96. // Create job to import the video
  97. const payload = {
  98. type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri',
  99. videoImportId: videoImport.id,
  100. magnetUri
  101. }
  102. await JobQueue.Instance.createJob({ type: 'video-import', payload })
  103. auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
  104. return res.json(videoImport.toFormattedJSON()).end()
  105. }
  106. async function addYoutubeDLImport (req: express.Request, res: express.Response) {
  107. const body: VideoImportCreate = req.body
  108. const targetUrl = body.targetUrl
  109. const user = res.locals.oauth.token.User
  110. let youtubeDLInfo: YoutubeDLInfo
  111. try {
  112. youtubeDLInfo = await getYoutubeDLInfo(targetUrl)
  113. } catch (err) {
  114. logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
  115. return res.status(400).json({
  116. error: 'Cannot fetch remote information of this URL.'
  117. }).end()
  118. }
  119. const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
  120. const thumbnailModel = await processThumbnail(req, video)
  121. const previewModel = await processPreview(req, video)
  122. const tags = body.tags || youtubeDLInfo.tags
  123. const videoImportAttributes = {
  124. targetUrl,
  125. state: VideoImportState.PENDING,
  126. userId: user.id
  127. }
  128. const videoImport = await insertIntoDB({
  129. video,
  130. thumbnailModel,
  131. previewModel,
  132. videoChannel: res.locals.videoChannel,
  133. tags,
  134. videoImportAttributes,
  135. user
  136. })
  137. // Create job to import the video
  138. const payload = {
  139. type: 'youtube-dl' as 'youtube-dl',
  140. videoImportId: videoImport.id,
  141. thumbnailUrl: youtubeDLInfo.thumbnailUrl,
  142. downloadThumbnail: !thumbnailModel,
  143. downloadPreview: !previewModel
  144. }
  145. await JobQueue.Instance.createJob({ type: 'video-import', payload })
  146. auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
  147. return res.json(videoImport.toFormattedJSON()).end()
  148. }
  149. function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) {
  150. const videoData = {
  151. name: body.name || importData.name || 'Unknown name',
  152. remote: false,
  153. category: body.category || importData.category,
  154. licence: body.licence || importData.licence,
  155. language: body.language || undefined,
  156. commentsEnabled: body.commentsEnabled || true,
  157. downloadEnabled: body.downloadEnabled || true,
  158. waitTranscoding: body.waitTranscoding || false,
  159. state: VideoState.TO_IMPORT,
  160. nsfw: body.nsfw || importData.nsfw || false,
  161. description: body.description || importData.description,
  162. support: body.support || null,
  163. privacy: body.privacy || VideoPrivacy.PRIVATE,
  164. duration: 0, // duration will be set by the import job
  165. channelId: channelId,
  166. originallyPublishedAt: importData.originallyPublishedAt
  167. }
  168. const video = new VideoModel(videoData)
  169. video.url = getVideoActivityPubUrl(video)
  170. return video
  171. }
  172. async function processThumbnail (req: express.Request, video: VideoModel) {
  173. const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
  174. if (thumbnailField) {
  175. const thumbnailPhysicalFile = thumbnailField[ 0 ]
  176. return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE)
  177. }
  178. return undefined
  179. }
  180. async function processPreview (req: express.Request, video: VideoModel) {
  181. const previewField = req.files ? req.files['previewfile'] : undefined
  182. if (previewField) {
  183. const previewPhysicalFile = previewField[0]
  184. return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
  185. }
  186. return undefined
  187. }
  188. function insertIntoDB (parameters: {
  189. video: VideoModel,
  190. thumbnailModel: ThumbnailModel,
  191. previewModel: ThumbnailModel,
  192. videoChannel: VideoChannelModel,
  193. tags: string[],
  194. videoImportAttributes: Partial<VideoImportModel>,
  195. user: UserModel
  196. }): Bluebird<VideoImportModel> {
  197. const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
  198. return sequelizeTypescript.transaction(async t => {
  199. const sequelizeOptions = { transaction: t }
  200. // Save video object in database
  201. const videoCreated = await video.save(sequelizeOptions)
  202. videoCreated.VideoChannel = videoChannel
  203. if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
  204. if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
  205. await autoBlacklistVideoIfNeeded(video, user, t)
  206. // Set tags to the video
  207. if (tags) {
  208. const tagInstances = await TagModel.findOrCreateTags(tags, t)
  209. await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
  210. videoCreated.Tags = tagInstances
  211. } else {
  212. videoCreated.Tags = []
  213. }
  214. // Create video import object in database
  215. const videoImport = await VideoImportModel.create(
  216. Object.assign({ videoId: videoCreated.id }, videoImportAttributes),
  217. sequelizeOptions
  218. )
  219. videoImport.Video = videoCreated
  220. return videoImport
  221. })
  222. }