+async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
+ logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
+
+ const jobPayloads: ActivitypubHttpFetcherPayload[] = []
+
+ if (syncParam.likes === true) {
+ const handler = items => createRates(items, video, 'like')
+ const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
+
+ await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
+ .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
+ }
+
+ if (syncParam.dislikes === true) {
+ const handler = items => createRates(items, video, 'dislike')
+ const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
+
+ await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
+ .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
+ }
+
+ if (syncParam.shares === true) {
+ const handler = items => addVideoShares(items, video)
+ const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
+
+ await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
+ .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+ }
+
+ if (syncParam.comments === true) {
+ const handler = items => addVideoComments(items, video)
+ const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
+
+ await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
+ .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
+ }
+
+ await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
+}
+
+async function getOrCreateVideoAndAccountAndChannel (options: {
+ videoObject: { id: string } | string,
+ syncParam?: SyncParam,
+ fetchType?: VideoFetchByUrlType,
+ allowRefresh?: boolean // true by default
+}) {
+ // Default params
+ const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
+ const fetchType = options.fetchType || 'all'
+ const allowRefresh = options.allowRefresh !== false
+
+ // Get video url
+ const videoUrl = getAPId(options.videoObject)
+
+ let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
+ if (videoFromDatabase) {
+ if (videoFromDatabase.isOutdated() && allowRefresh === true) {
+ const refreshOptions = {
+ video: videoFromDatabase,
+ fetchedType: fetchType,
+ syncParam
+ }
+
+ if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
+ else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
+ }
+
+ return { video: videoFromDatabase, created: false }
+ }
+
+ const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
+ if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+
+ const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
+ const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
+
+ await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
+
+ return { video, created: true }
+}
+
+async function updateVideoFromAP (options: {
+ video: VideoModel,
+ videoObject: VideoTorrentObject,
+ account: AccountModel,
+ channel: VideoChannelModel,
+ overrideTo?: string[]
+}) {
+ logger.debug('Updating remote video "%s".', options.videoObject.uuid)
+
+ let videoFieldsSave: any
+ const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
+ const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
+
+ try {
+ await sequelizeTypescript.transaction(async t => {
+ const sequelizeOptions = { transaction: t }
+
+ videoFieldsSave = options.video.toJSON()
+
+ // Check actor has the right to update the video
+ const videoChannel = options.video.VideoChannel
+ if (videoChannel.Account.id !== options.account.id) {
+ throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
+ }
+
+ const to = options.overrideTo ? options.overrideTo : options.videoObject.to
+ const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
+ options.video.set('name', videoData.name)
+ options.video.set('uuid', videoData.uuid)
+ options.video.set('url', videoData.url)
+ options.video.set('category', videoData.category)
+ options.video.set('licence', videoData.licence)
+ options.video.set('language', videoData.language)
+ options.video.set('description', videoData.description)
+ options.video.set('support', videoData.support)
+ options.video.set('nsfw', videoData.nsfw)
+ options.video.set('commentsEnabled', videoData.commentsEnabled)
+ options.video.set('downloadEnabled', videoData.downloadEnabled)
+ options.video.set('waitTranscoding', videoData.waitTranscoding)
+ options.video.set('state', videoData.state)
+ options.video.set('duration', videoData.duration)
+ options.video.set('createdAt', videoData.createdAt)
+ options.video.set('publishedAt', videoData.publishedAt)
+ options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
+ options.video.set('privacy', videoData.privacy)
+ options.video.set('channelId', videoData.channelId)
+ options.video.set('views', videoData.views)
+
+ await options.video.save(sequelizeOptions)
+
+ {
+ const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
+ const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
+
+ // Remove video files that do not exist anymore
+ const destroyTasks = options.video.VideoFiles
+ .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
+ .map(f => f.destroy(sequelizeOptions))
+ await Promise.all(destroyTasks)
+
+ // Update or add other one
+ const upsertTasks = videoFileAttributes.map(a => {
+ return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
+ .then(([ file ]) => file)
+ })
+
+ options.video.VideoFiles = await Promise.all(upsertTasks)
+ }
+
+ {
+ const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject)
+ const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
+
+ // Remove video files that do not exist anymore
+ const destroyTasks = options.video.VideoStreamingPlaylists
+ .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
+ .map(f => f.destroy(sequelizeOptions))
+ await Promise.all(destroyTasks)
+
+ // Update or add other one
+ const upsertTasks = streamingPlaylistAttributes.map(a => {
+ return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
+ .then(([ streamingPlaylist ]) => streamingPlaylist)
+ })
+
+ options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
+ }
+
+ {
+ // Update Tags
+ const tags = options.videoObject.tag.map(tag => tag.name)
+ const tagInstances = await TagModel.findOrCreateTags(tags, t)
+ await options.video.$set('Tags', tagInstances, sequelizeOptions)
+ }
+
+ {
+ // Update captions
+ await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)