+ getOrCreateVideoChannelFromVideoObject
+}
+
+// ---------------------------------------------------------------------------
+
+function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
+ const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
+
+ const urlMediaType = url.mediaType || url.mimeType
+ return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
+}
+
+function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
+ const urlMediaType = url.mediaType || url.mimeType
+
+ return urlMediaType === 'application/x-mpegURL'
+}
+
+function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
+ const urlMediaType = tag.mediaType || tag.mimeType
+
+ return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
+}
+
+async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
+ logger.debug('Adding remote video %s.', videoObject.id)
+
+ const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
+ const video = VideoModel.build(videoData) as MVideoThumbnail
+
+ const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
+
+ let thumbnailModel: MThumbnail
+ if (waitThumbnail === true) {
+ thumbnailModel = await promiseThumbnail
+ }
+
+ const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
+ const sequelizeOptions = { transaction: t }
+
+ const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
+ videoCreated.VideoChannel = channel
+
+ if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
+
+ // FIXME: use icon URL instead
+ const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
+ const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
+ if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
+
+ // Process files
+ const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
+ if (videoFileAttributes.length === 0) {
+ throw new Error('Cannot find valid files for video %s ' + videoObject.url)
+ }
+
+ const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
+ const videoFiles = await Promise.all(videoFilePromises)
+
+ const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
+ const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
+ const streamingPlaylists = await Promise.all(playlistPromises)
+
+ // Process tags
+ const tags = videoObject.tag
+ .filter(t => t.type === 'Hashtag')
+ .map(t => t.name)
+ const tagInstances = await TagModel.findOrCreateTags(tags, t)
+ await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
+
+ // Process captions
+ const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
+ return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
+ })
+ await Promise.all(videoCaptionsPromises)
+
+ videoCreated.VideoFiles = videoFiles
+ videoCreated.VideoStreamingPlaylists = streamingPlaylists
+ videoCreated.Tags = tagInstances
+
+ const autoBlacklisted = await autoBlacklistVideoIfNeeded({
+ video: videoCreated,
+ user: undefined,
+ isRemote: true,
+ isNew: true,
+ transaction: t
+ })
+
+ logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
+
+ return { autoBlacklisted, videoCreated }
+ })
+
+ if (waitThumbnail === false) {
+ promiseThumbnail.then(thumbnailModel => {
+ thumbnailModel = videoCreated.id
+
+ return thumbnailModel.save()
+ })
+ }
+
+ return { autoBlacklisted, videoCreated }
+}
+
+async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
+ const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
+ const duration = videoObject.duration.replace(/[^\d]+/, '')
+
+ let language: string | undefined
+ if (videoObject.language) {
+ language = videoObject.language.identifier
+ }
+
+ let category: number | undefined
+ if (videoObject.category) {
+ category = parseInt(videoObject.category.identifier, 10)
+ }
+
+ let licence: number | undefined
+ if (videoObject.licence) {
+ licence = parseInt(videoObject.licence.identifier, 10)
+ }
+
+ const description = videoObject.content || null
+ const support = videoObject.support || null
+
+ return {
+ name: videoObject.name,
+ uuid: videoObject.uuid,
+ url: videoObject.id,
+ category,
+ licence,
+ language,
+ description,
+ support,
+ nsfw: videoObject.sensitive,
+ commentsEnabled: videoObject.commentsEnabled,
+ downloadEnabled: videoObject.downloadEnabled,
+ waitTranscoding: videoObject.waitTranscoding,
+ state: videoObject.state,
+ channelId: videoChannel.id,
+ duration: parseInt(duration, 10),
+ createdAt: new Date(videoObject.published),
+ publishedAt: new Date(videoObject.published),
+ originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null,
+ // FIXME: updatedAt does not seems to be considered by Sequelize
+ updatedAt: new Date(videoObject.updated),
+ views: videoObject.views,
+ likes: 0,
+ dislikes: 0,
+ remote: true,
+ privacy
+ }
+}
+
+function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) {
+ const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
+
+ if (fileUrls.length === 0) {
+ throw new Error('Cannot find video files for ' + video.url)
+ }
+
+ const attributes: FilteredModelAttributes<VideoFileModel>[] = []
+ for (const fileUrl of fileUrls) {
+ // Fetch associated magnet uri
+ const magnet = videoObject.url.find(u => {
+ const mediaType = u.mediaType || u.mimeType
+ return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
+ })
+
+ if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
+
+ const parsed = magnetUtil.decode(magnet.href)
+ if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
+ throw new Error('Cannot parse magnet URI ' + magnet.href)
+ }
+
+ const mediaType = fileUrl.mediaType || fileUrl.mimeType
+ const attribute = {
+ extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
+ infoHash: parsed.infoHash,
+ resolution: fileUrl.height,
+ size: fileUrl.size,
+ videoId: video.id,
+ fps: fileUrl.fps || -1
+ }
+
+ attributes.push(attribute)
+ }
+
+ return attributes
+}
+
+function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoTorrentObject, videoFiles: MVideoFile[]) {
+ const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
+ if (playlistUrls.length === 0) return []
+
+ const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
+ for (const playlistUrlObject of playlistUrls) {
+ const segmentsSha256UrlObject = playlistUrlObject.tag
+ .find(t => {
+ return isAPPlaylistSegmentHashesUrlObject(t)
+ }) as ActivityPlaylistSegmentHashesObject
+ if (!segmentsSha256UrlObject) {
+ logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
+ continue
+ }
+
+ const attribute = {
+ type: VideoStreamingPlaylistType.HLS,
+ playlistUrl: playlistUrlObject.href,
+ segmentsSha256Url: segmentsSha256UrlObject.href,
+ p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
+ p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
+ videoId: video.id
+ }
+
+ attributes.push(attribute)
+ }
+
+ return attributes