videos.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. import * as Bluebird from 'bluebird'
  2. import * as sequelize from 'sequelize'
  3. import * as magnetUtil from 'magnet-uri'
  4. import * as request from 'request'
  5. import {
  6. ActivityPlaylistSegmentHashesObject,
  7. ActivityPlaylistUrlObject,
  8. ActivityUrlObject,
  9. ActivityVideoUrlObject,
  10. VideoState
  11. } from '../../../shared/index'
  12. import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
  13. import { VideoPrivacy } from '../../../shared/models/videos'
  14. import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
  15. import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
  16. import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
  17. import { logger } from '../../helpers/logger'
  18. import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
  19. import {
  20. ACTIVITY_PUB,
  21. MIMETYPES,
  22. P2P_MEDIA_LOADER_PEER_VERSION,
  23. PREVIEWS_SIZE,
  24. REMOTE_SCHEME,
  25. STATIC_PATHS
  26. } from '../../initializers/constants'
  27. import { ActorModel } from '../../models/activitypub/actor'
  28. import { TagModel } from '../../models/video/tag'
  29. import { VideoModel } from '../../models/video/video'
  30. import { VideoChannelModel } from '../../models/video/video-channel'
  31. import { VideoFileModel } from '../../models/video/video-file'
  32. import { getOrCreateActorAndServerAndModel } from './actor'
  33. import { addVideoComments } from './video-comments'
  34. import { crawlCollectionPage } from './crawl'
  35. import { sendCreateVideo, sendUpdateVideo } from './send'
  36. import { isArray } from '../../helpers/custom-validators/misc'
  37. import { VideoCaptionModel } from '../../models/video/video-caption'
  38. import { JobQueue } from '../job-queue'
  39. import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
  40. import { createRates } from './video-rates'
  41. import { addVideoShares, shareVideoByServerAndChannel } from './share'
  42. import { AccountModel } from '../../models/account/account'
  43. import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
  44. import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
  45. import { Notifier } from '../notifier'
  46. import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
  47. import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
  48. import { AccountVideoRateModel } from '../../models/account/account-video-rate'
  49. import { VideoShareModel } from '../../models/video/video-share'
  50. import { VideoCommentModel } from '../../models/video/video-comment'
  51. import { sequelizeTypescript } from '../../initializers/database'
  52. import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
  53. import { ThumbnailModel } from '../../models/video/thumbnail'
  54. import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
  55. import { join } from 'path'
  56. import { FilteredModelAttributes } from '../../typings/sequelize'
  57. async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
  58. // If the video is not private and is published, we federate it
  59. if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
  60. // Fetch more attributes that we will need to serialize in AP object
  61. if (isArray(video.VideoCaptions) === false) {
  62. video.VideoCaptions = await video.$get('VideoCaptions', {
  63. attributes: [ 'language' ],
  64. transaction
  65. }) as VideoCaptionModel[]
  66. }
  67. if (isNewVideo) {
  68. // Now we'll add the video's meta data to our followers
  69. await sendCreateVideo(video, transaction)
  70. await shareVideoByServerAndChannel(video, transaction)
  71. } else {
  72. await sendUpdateVideo(video, transaction)
  73. }
  74. }
  75. }
  76. async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoTorrentObject }> {
  77. const options = {
  78. uri: videoUrl,
  79. method: 'GET',
  80. json: true,
  81. activityPub: true
  82. }
  83. logger.info('Fetching remote video %s.', videoUrl)
  84. const { response, body } = await doRequest(options)
  85. if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
  86. logger.debug('Remote video JSON is not valid.', { body })
  87. return { response, videoObject: undefined }
  88. }
  89. return { response, videoObject: body }
  90. }
  91. async function fetchRemoteVideoDescription (video: VideoModel) {
  92. const host = video.VideoChannel.Account.Actor.Server.host
  93. const path = video.getDescriptionAPIPath()
  94. const options = {
  95. uri: REMOTE_SCHEME.HTTP + '://' + host + path,
  96. json: true
  97. }
  98. const { body } = await doRequest(options)
  99. return body.description ? body.description : ''
  100. }
  101. function fetchRemoteVideoStaticFile (video: VideoModel, path: string, destPath: string) {
  102. const url = buildRemoteBaseUrl(video, path)
  103. // We need to provide a callback, if no we could have an uncaught exception
  104. return doRequestAndSaveToFile({ uri: url }, destPath)
  105. }
  106. function buildRemoteBaseUrl (video: VideoModel, path: string) {
  107. const host = video.VideoChannel.Account.Actor.Server.host
  108. return REMOTE_SCHEME.HTTP + '://' + host + path
  109. }
  110. function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
  111. const channel = videoObject.attributedTo.find(a => a.type === 'Group')
  112. if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
  113. if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
  114. throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
  115. }
  116. return getOrCreateActorAndServerAndModel(channel.id, 'all')
  117. }
  118. type SyncParam = {
  119. likes: boolean
  120. dislikes: boolean
  121. shares: boolean
  122. comments: boolean
  123. thumbnail: boolean
  124. refreshVideo?: boolean
  125. }
  126. async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
  127. logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
  128. const jobPayloads: ActivitypubHttpFetcherPayload[] = []
  129. if (syncParam.likes === true) {
  130. const handler = items => createRates(items, video, 'like')
  131. const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
  132. await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
  133. .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
  134. } else {
  135. jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
  136. }
  137. if (syncParam.dislikes === true) {
  138. const handler = items => createRates(items, video, 'dislike')
  139. const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
  140. await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
  141. .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
  142. } else {
  143. jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
  144. }
  145. if (syncParam.shares === true) {
  146. const handler = items => addVideoShares(items, video)
  147. const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
  148. await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
  149. .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
  150. } else {
  151. jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
  152. }
  153. if (syncParam.comments === true) {
  154. const handler = items => addVideoComments(items, video)
  155. const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
  156. await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
  157. .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
  158. } else {
  159. jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
  160. }
  161. await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
  162. }
  163. async function getOrCreateVideoAndAccountAndChannel (options: {
  164. videoObject: { id: string } | string,
  165. syncParam?: SyncParam,
  166. fetchType?: VideoFetchByUrlType,
  167. allowRefresh?: boolean // true by default
  168. }) {
  169. // Default params
  170. const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
  171. const fetchType = options.fetchType || 'all'
  172. const allowRefresh = options.allowRefresh !== false
  173. // Get video url
  174. const videoUrl = getAPId(options.videoObject)
  175. let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
  176. if (videoFromDatabase) {
  177. if (videoFromDatabase.isOutdated() && allowRefresh === true) {
  178. const refreshOptions = {
  179. video: videoFromDatabase,
  180. fetchedType: fetchType,
  181. syncParam
  182. }
  183. if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
  184. else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
  185. }
  186. return { video: videoFromDatabase, created: false }
  187. }
  188. const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
  189. if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
  190. const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
  191. const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
  192. await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
  193. return { video, created: true }
  194. }
  195. async function updateVideoFromAP (options: {
  196. video: VideoModel,
  197. videoObject: VideoTorrentObject,
  198. account: AccountModel,
  199. channel: VideoChannelModel,
  200. overrideTo?: string[]
  201. }) {
  202. logger.debug('Updating remote video "%s".', options.videoObject.uuid)
  203. let videoFieldsSave: any
  204. const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
  205. const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
  206. try {
  207. let thumbnailModel: ThumbnailModel
  208. try {
  209. thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
  210. } catch (err) {
  211. logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
  212. }
  213. await sequelizeTypescript.transaction(async t => {
  214. const sequelizeOptions = { transaction: t }
  215. videoFieldsSave = options.video.toJSON()
  216. // Check actor has the right to update the video
  217. const videoChannel = options.video.VideoChannel
  218. if (videoChannel.Account.id !== options.account.id) {
  219. throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
  220. }
  221. const to = options.overrideTo ? options.overrideTo : options.videoObject.to
  222. const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
  223. options.video.set('name', videoData.name)
  224. options.video.set('uuid', videoData.uuid)
  225. options.video.set('url', videoData.url)
  226. options.video.set('category', videoData.category)
  227. options.video.set('licence', videoData.licence)
  228. options.video.set('language', videoData.language)
  229. options.video.set('description', videoData.description)
  230. options.video.set('support', videoData.support)
  231. options.video.set('nsfw', videoData.nsfw)
  232. options.video.set('commentsEnabled', videoData.commentsEnabled)
  233. options.video.set('downloadEnabled', videoData.downloadEnabled)
  234. options.video.set('waitTranscoding', videoData.waitTranscoding)
  235. options.video.set('state', videoData.state)
  236. options.video.set('duration', videoData.duration)
  237. options.video.set('createdAt', videoData.createdAt)
  238. options.video.set('publishedAt', videoData.publishedAt)
  239. options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
  240. options.video.set('privacy', videoData.privacy)
  241. options.video.set('channelId', videoData.channelId)
  242. options.video.set('views', videoData.views)
  243. await options.video.save(sequelizeOptions)
  244. if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
  245. // FIXME: use icon URL instead
  246. const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
  247. const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
  248. await options.video.addAndSaveThumbnail(previewModel, t)
  249. {
  250. const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
  251. const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
  252. // Remove video files that do not exist anymore
  253. const destroyTasks = options.video.VideoFiles
  254. .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
  255. .map(f => f.destroy(sequelizeOptions))
  256. await Promise.all(destroyTasks)
  257. // Update or add other one
  258. const upsertTasks = videoFileAttributes.map(a => {
  259. return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
  260. .then(([ file ]) => file)
  261. })
  262. options.video.VideoFiles = await Promise.all(upsertTasks)
  263. }
  264. {
  265. const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(
  266. options.video,
  267. options.videoObject,
  268. options.video.VideoFiles
  269. )
  270. const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
  271. // Remove video files that do not exist anymore
  272. const destroyTasks = options.video.VideoStreamingPlaylists
  273. .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
  274. .map(f => f.destroy(sequelizeOptions))
  275. await Promise.all(destroyTasks)
  276. // Update or add other one
  277. const upsertTasks = streamingPlaylistAttributes.map(a => {
  278. return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
  279. .then(([ streamingPlaylist ]) => streamingPlaylist)
  280. })
  281. options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
  282. }
  283. {
  284. // Update Tags
  285. const tags = options.videoObject.tag.map(tag => tag.name)
  286. const tagInstances = await TagModel.findOrCreateTags(tags, t)
  287. await options.video.$set('Tags', tagInstances, sequelizeOptions)
  288. }
  289. {
  290. // Update captions
  291. await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)
  292. const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => {
  293. return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t)
  294. })
  295. options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
  296. }
  297. })
  298. // Notify our users?
  299. if (wasPrivateVideo || wasUnlistedVideo) {
  300. Notifier.Instance.notifyOnNewVideo(options.video)
  301. }
  302. logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
  303. } catch (err) {
  304. if (options.video !== undefined && videoFieldsSave !== undefined) {
  305. resetSequelizeInstance(options.video, videoFieldsSave)
  306. }
  307. // This is just a debug because we will retry the insert
  308. logger.debug('Cannot update the remote video.', { err })
  309. throw err
  310. }
  311. }
  312. async function refreshVideoIfNeeded (options: {
  313. video: VideoModel,
  314. fetchedType: VideoFetchByUrlType,
  315. syncParam: SyncParam
  316. }): Promise<VideoModel> {
  317. if (!options.video.isOutdated()) return options.video
  318. // We need more attributes if the argument video was fetched with not enough joints
  319. const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
  320. try {
  321. const { response, videoObject } = await fetchRemoteVideo(video.url)
  322. if (response.statusCode === 404) {
  323. logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
  324. // Video does not exist anymore
  325. await video.destroy()
  326. return undefined
  327. }
  328. if (videoObject === undefined) {
  329. logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
  330. await video.setAsRefreshed()
  331. return video
  332. }
  333. const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
  334. const account = await AccountModel.load(channelActor.VideoChannel.accountId)
  335. const updateOptions = {
  336. video,
  337. videoObject,
  338. account,
  339. channel: channelActor.VideoChannel
  340. }
  341. await retryTransactionWrapper(updateVideoFromAP, updateOptions)
  342. await syncVideoExternalAttributes(video, videoObject, options.syncParam)
  343. return video
  344. } catch (err) {
  345. logger.warn('Cannot refresh video %s.', options.video.url, { err })
  346. // Don't refresh in loop
  347. await video.setAsRefreshed()
  348. return video
  349. }
  350. }
  351. export {
  352. updateVideoFromAP,
  353. refreshVideoIfNeeded,
  354. federateVideoIfNeeded,
  355. fetchRemoteVideo,
  356. getOrCreateVideoAndAccountAndChannel,
  357. fetchRemoteVideoStaticFile,
  358. fetchRemoteVideoDescription,
  359. getOrCreateVideoChannelFromVideoObject
  360. }
  361. // ---------------------------------------------------------------------------
  362. function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
  363. const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
  364. const urlMediaType = url.mediaType || url.mimeType
  365. return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
  366. }
  367. function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
  368. const urlMediaType = url.mediaType || url.mimeType
  369. return urlMediaType === 'application/x-mpegURL'
  370. }
  371. function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
  372. const urlMediaType = tag.mediaType || tag.mimeType
  373. return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
  374. }
  375. async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
  376. logger.debug('Adding remote video %s.', videoObject.id)
  377. const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
  378. const video = VideoModel.build(videoData)
  379. const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
  380. let thumbnailModel: ThumbnailModel
  381. if (waitThumbnail === true) {
  382. thumbnailModel = await promiseThumbnail
  383. }
  384. const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
  385. const sequelizeOptions = { transaction: t }
  386. const videoCreated = await video.save(sequelizeOptions)
  387. videoCreated.VideoChannel = channelActor.VideoChannel
  388. if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
  389. // FIXME: use icon URL instead
  390. const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
  391. const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
  392. if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
  393. // Process files
  394. const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
  395. if (videoFileAttributes.length === 0) {
  396. throw new Error('Cannot find valid files for video %s ' + videoObject.url)
  397. }
  398. const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
  399. const videoFiles = await Promise.all(videoFilePromises)
  400. const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
  401. const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
  402. await Promise.all(playlistPromises)
  403. // Process tags
  404. const tags = videoObject.tag
  405. .filter(t => t.type === 'Hashtag')
  406. .map(t => t.name)
  407. const tagInstances = await TagModel.findOrCreateTags(tags, t)
  408. await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
  409. // Process captions
  410. const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
  411. return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
  412. })
  413. await Promise.all(videoCaptionsPromises)
  414. logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
  415. return videoCreated
  416. })
  417. if (waitThumbnail === false) {
  418. promiseThumbnail.then(thumbnailModel => {
  419. thumbnailModel = videoCreated.id
  420. return thumbnailModel.save()
  421. })
  422. }
  423. return videoCreated
  424. }
  425. async function videoActivityObjectToDBAttributes (
  426. videoChannel: VideoChannelModel,
  427. videoObject: VideoTorrentObject,
  428. to: string[] = []
  429. ) {
  430. const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
  431. const duration = videoObject.duration.replace(/[^\d]+/, '')
  432. let language: string | undefined
  433. if (videoObject.language) {
  434. language = videoObject.language.identifier
  435. }
  436. let category: number | undefined
  437. if (videoObject.category) {
  438. category = parseInt(videoObject.category.identifier, 10)
  439. }
  440. let licence: number | undefined
  441. if (videoObject.licence) {
  442. licence = parseInt(videoObject.licence.identifier, 10)
  443. }
  444. const description = videoObject.content || null
  445. const support = videoObject.support || null
  446. return {
  447. name: videoObject.name,
  448. uuid: videoObject.uuid,
  449. url: videoObject.id,
  450. category,
  451. licence,
  452. language,
  453. description,
  454. support,
  455. nsfw: videoObject.sensitive,
  456. commentsEnabled: videoObject.commentsEnabled,
  457. downloadEnabled: videoObject.downloadEnabled,
  458. waitTranscoding: videoObject.waitTranscoding,
  459. state: videoObject.state,
  460. channelId: videoChannel.id,
  461. duration: parseInt(duration, 10),
  462. createdAt: new Date(videoObject.published),
  463. publishedAt: new Date(videoObject.published),
  464. originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null,
  465. // FIXME: updatedAt does not seems to be considered by Sequelize
  466. updatedAt: new Date(videoObject.updated),
  467. views: videoObject.views,
  468. likes: 0,
  469. dislikes: 0,
  470. remote: true,
  471. privacy
  472. }
  473. }
  474. function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
  475. const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
  476. if (fileUrls.length === 0) {
  477. throw new Error('Cannot find video files for ' + video.url)
  478. }
  479. const attributes: FilteredModelAttributes<VideoFileModel>[] = []
  480. for (const fileUrl of fileUrls) {
  481. // Fetch associated magnet uri
  482. const magnet = videoObject.url.find(u => {
  483. const mediaType = u.mediaType || u.mimeType
  484. return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
  485. })
  486. if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
  487. const parsed = magnetUtil.decode(magnet.href)
  488. if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
  489. throw new Error('Cannot parse magnet URI ' + magnet.href)
  490. }
  491. const mediaType = fileUrl.mediaType || fileUrl.mimeType
  492. const attribute = {
  493. extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
  494. infoHash: parsed.infoHash,
  495. resolution: fileUrl.height,
  496. size: fileUrl.size,
  497. videoId: video.id,
  498. fps: fileUrl.fps || -1
  499. }
  500. attributes.push(attribute)
  501. }
  502. return attributes
  503. }
  504. function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject, videoFiles: VideoFileModel[]) {
  505. const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
  506. if (playlistUrls.length === 0) return []
  507. const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
  508. for (const playlistUrlObject of playlistUrls) {
  509. const segmentsSha256UrlObject = playlistUrlObject.tag
  510. .find(t => {
  511. return isAPPlaylistSegmentHashesUrlObject(t)
  512. }) as ActivityPlaylistSegmentHashesObject
  513. if (!segmentsSha256UrlObject) {
  514. logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
  515. continue
  516. }
  517. const attribute = {
  518. type: VideoStreamingPlaylistType.HLS,
  519. playlistUrl: playlistUrlObject.href,
  520. segmentsSha256Url: segmentsSha256UrlObject.href,
  521. p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
  522. p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
  523. videoId: video.id
  524. }
  525. attributes.push(attribute)
  526. }
  527. return attributes
  528. }