Verified Commit 08a47c75 authored by Chocobozzz's avatar Chocobozzz
Browse files

Refactor AP video create/update

parent 69290ab3
import { checkUrlsSameHost, getAPId } from "@server/helpers/activitypub"
import { sanitizeAndCheckVideoTorrentObject } from "@server/helpers/custom-validators/activitypub/videos"
import { retryTransactionWrapper } from "@server/helpers/database-utils"
import { logger } from "@server/helpers/logger"
import { doJSONRequest, PeerTubeRequestError } from "@server/helpers/requests"
import { fetchVideoByUrl, VideoFetchByUrlType } from "@server/helpers/video"
import { REMOTE_SCHEME } from "@server/initializers/constants"
import { ActorFollowScoreCache } from "@server/lib/files-cache"
import { JobQueue } from "@server/lib/job-queue"
import { VideoModel } from "@server/models/video/video"
import { MVideoAccountLight, MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from "@server/types/models"
import { HttpStatusCode } from "@shared/core-utils"
import { VideoObject } from "@shared/models"
import { getOrCreateActorAndServerAndModel } from "../actor"
import { SyncParam, syncVideoExternalAttributes } from "./shared"
import { createVideo } from "./shared/video-create"
import { APVideoUpdater } from "./update"
import { checkUrlsSameHost, getAPId } from '@server/helpers/activitypub'
import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
import { retryTransactionWrapper } from '@server/helpers/database-utils'
import { logger } from '@server/helpers/logger'
import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
import { fetchVideoByUrl, VideoFetchByUrlType } from '@server/helpers/video'
import { REMOTE_SCHEME } from '@server/initializers/constants'
import { ActorFollowScoreCache } from '@server/lib/files-cache'
import { JobQueue } from '@server/lib/job-queue'
import { VideoModel } from '@server/models/video/video'
import { MVideoAccountLight, MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
import { HttpStatusCode } from '@shared/core-utils'
import { VideoObject } from '@shared/models'
import { getOrCreateActorAndServerAndModel } from '../actor'
import { APVideoCreator, SyncParam, syncVideoExternalAttributes } from './shared'
import { APVideoUpdater } from './updater'
async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
logger.info('Fetching remote video %s.', videoUrl)
......@@ -115,16 +114,17 @@ async function getOrCreateVideoAndAccountAndChannel (
return { video: videoFromDatabase, created: false }
}
const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
const { videoObject } = await fetchRemoteVideo(videoUrl)
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
const actor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
const actor = await getOrCreateVideoChannelFromVideoObject(videoObject)
const videoChannel = actor.VideoChannel
try {
const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, videoChannel, syncParam.thumbnail)
const creator = new APVideoCreator({ videoObject, channel: videoChannel })
const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator), syncParam.thumbnail)
await syncVideoExternalAttributes(videoCreated, fetchedVideo, syncParam)
await syncVideoExternalAttributes(videoCreated, videoObject, syncParam)
return { video: videoCreated, created: true, autoBlacklisted }
} catch (err) {
......
export * from './federate'
export * from './fetch'
export * from './update'
export * from './updater'
import { Transaction } from 'sequelize/types'
import { deleteNonExistingModels } from '@server/helpers/database-utils'
import { logger } from '@server/helpers/logger'
import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail'
import { setVideoTags } from '@server/lib/video'
import { VideoCaptionModel } from '@server/models/video/video-caption'
import { VideoFileModel } from '@server/models/video/video-file'
import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
import {
getCaptionAttributesFromObject,
getFileAttributesFromUrl,
getLiveAttributesFromObject,
getPreviewFromIcons,
getStreamingPlaylistAttributesFromObject,
getTagsFromObject,
getThumbnailFromIcons
} from './object-to-model-attributes'
import { getTrackerUrls, setVideoTrackers } from './trackers'
export abstract class APVideoAbstractBuilder {
protected abstract videoObject: VideoObject
protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> {
return createVideoMiniatureFromUrl({
downloadUrl: getThumbnailFromIcons(this.videoObject).url,
video,
type: ThumbnailType.MINIATURE
}).catch(err => {
logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err })
return undefined
})
}
protected async setPreview (video: MVideoFullLight, t: Transaction) {
// Don't fetch the preview that could be big, create a placeholder instead
const previewIcon = getPreviewFromIcons(this.videoObject)
if (!previewIcon) return
const previewModel = createPlaceholderThumbnail({
fileUrl: previewIcon.url,
video,
type: ThumbnailType.PREVIEW,
size: previewIcon
})
await video.addAndSaveThumbnail(previewModel, t)
}
protected async setTags (video: MVideoFullLight, t: Transaction) {
const tags = getTagsFromObject(this.videoObject)
await setVideoTags({ video, tags, transaction: t })
}
protected async setTrackers (video: MVideoFullLight, t: Transaction) {
const trackers = getTrackerUrls(this.videoObject, video)
await setVideoTrackers({ video, trackers, transaction: t })
}
protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
const videoCaptionsPromises = getCaptionAttributesFromObject(video, this.videoObject)
.map(a => new VideoCaptionModel(a) as MVideoCaption)
.map(c => VideoCaptionModel.insertOrReplaceLanguage(c, t))
await Promise.all(videoCaptionsPromises)
}
protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
const attributes = getLiveAttributesFromObject(video, this.videoObject)
const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
video.VideoLive = videoLive
}
protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) {
const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
// Remove video files that do not exist anymore
const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t)
await Promise.all(destroyTasks)
// Update or add other one
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
video.VideoFiles = await Promise.all(upsertTasks)
}
protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject, video.VideoFiles || [])
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
// Remove video playlists that do not exist anymore
const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t)
await Promise.all(destroyTasks)
video.VideoStreamingPlaylists = []
for (const playlistAttributes of streamingPlaylistAttributes) {
const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
streamingPlaylistModel.Video = video
await this.setStreamingPlaylistFiles(video, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
video.VideoStreamingPlaylists.push(streamingPlaylistModel)
}
}
private async insertOrReplaceStreamingPlaylist (attributes: VideoStreamingPlaylistModel['_creationAttributes'], t: Transaction) {
const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
return streamingPlaylist as MStreamingPlaylistFilesVideo
}
private getStreamingPlaylistFiles (video: MVideoFullLight, type: VideoStreamingPlaylistType) {
const playlist = video.VideoStreamingPlaylists.find(s => s.type === type)
if (!playlist) return []
return playlist.VideoFiles
}
private async setStreamingPlaylistFiles (
video: MVideoFullLight,
playlistModel: MStreamingPlaylistFilesVideo,
tagObjects: ActivityTagObject[],
t: Transaction
) {
const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(video, playlistModel.type)
const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
await Promise.all(destroyTasks)
// Update or add other one
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
playlistModel.VideoFiles = await Promise.all(upsertTasks)
}
}
import { logger } from '@server/helpers/logger'
import { sequelizeTypescript } from '@server/initializers/database'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
import { VideoModel } from '@server/models/video/video'
import { MChannelAccountLight, MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
import { VideoObject } from '@shared/models'
import { APVideoAbstractBuilder } from './abstract-builder'
import { getVideoAttributesFromObject } from './object-to-model-attributes'
export class APVideoCreator extends APVideoAbstractBuilder {
protected readonly videoObject: VideoObject
private readonly channel: MChannelAccountLight
constructor (options: {
videoObject: VideoObject
channel: MChannelAccountLight
}) {
super()
this.videoObject = options.videoObject
this.channel = options.channel
}
async create (waitThumbnail = false) {
logger.debug('Adding remote video %s.', this.videoObject.id)
const videoData = await getVideoAttributesFromObject(this.channel, this.videoObject, this.videoObject.to)
const video = VideoModel.build(videoData) as MVideoThumbnail
const promiseThumbnail = this.tryToGenerateThumbnail(video)
let thumbnailModel: MThumbnail
if (waitThumbnail === true) {
thumbnailModel = await promiseThumbnail
}
const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
try {
const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
videoCreated.VideoChannel = this.channel
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
await this.setPreview(videoCreated, t)
await this.setWebTorrentFiles(videoCreated, t)
await this.setStreamingPlaylists(videoCreated, t)
await this.setTags(videoCreated, t)
await this.setTrackers(videoCreated, t)
await this.insertOrReplaceCaptions(videoCreated, t)
await this.insertOrReplaceLive(videoCreated, t)
// We added a video in this channel, set it as updated
await this.channel.setAsUpdated(t)
const autoBlacklisted = await autoBlacklistVideoIfNeeded({
video: videoCreated,
user: undefined,
isRemote: true,
isNew: true,
transaction: t
})
logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid)
return { autoBlacklisted, videoCreated }
} catch (err) {
// FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
// Remove thumbnail
if (thumbnailModel) await thumbnailModel.removeThumbnail()
throw err
}
})
if (waitThumbnail === false) {
// Error is already caught above
// eslint-disable-next-line @typescript-eslint/no-floating-promises
promiseThumbnail.then(thumbnailModel => {
if (!thumbnailModel) return
thumbnailModel = videoCreated.id
return thumbnailModel.save()
})
}
return { autoBlacklisted, videoCreated }
}
}
export * from './abstract-builder'
export * from './creator'
export * from './object-to-model-attributes'
export * from './trackers'
export * from './video-create'
export * from './video-sync-attributes'
......@@ -23,6 +23,7 @@ import {
VideoPrivacy,
VideoStreamingPlaylistType
} from '@shared/models'
import { VideoCaptionModel } from '@server/models/video/video-caption'
function getThumbnailFromIcons (videoObject: VideoObject) {
let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
......@@ -44,7 +45,7 @@ function getTagsFromObject (videoObject: VideoObject) {
.map(t => t.name)
}
function videoFileActivityUrlToDBAttributes (
function getFileAttributesFromUrl (
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
urls: (ActivityTagObject | ActivityUrlObject)[]
) {
......@@ -109,7 +110,7 @@ function videoFileActivityUrlToDBAttributes (
return attributes
}
function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) {
function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) {
const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
if (playlistUrls.length === 0) return []
......@@ -134,6 +135,7 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
videoId: video.id,
tagAPObject: playlistUrlObject.tag
}
......@@ -143,7 +145,24 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
return attributes
}
function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
return {
saveReplay: videoObject.liveSaveReplay,
permanentLive: videoObject.permanentLive,
videoId: video.id
}
}
function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
return videoObject.subtitleLanguage.map(c => ({
videoId: video.id,
filename: VideoCaptionModel.generateCaptionName(c.identifier),
language: c.identifier,
fileUrl: c.url
}))
}
function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
? VideoPrivacy.PUBLIC
: VideoPrivacy.UNLISTED
......@@ -203,10 +222,13 @@ export {
getTagsFromObject,
videoActivityObjectToDBAttributes,
getFileAttributesFromUrl,
getStreamingPlaylistAttributesFromObject,
getLiveAttributesFromObject,
getCaptionAttributesFromObject,
videoFileActivityUrlToDBAttributes,
streamingPlaylistActivityUrlToDBAttributes
getVideoAttributesFromObject
}
// ---------------------------------------------------------------------------
......
import { logger } from '@server/helpers/logger'
import { sequelizeTypescript } from '@server/initializers/database'
import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail'
import { setVideoTags } from '@server/lib/video'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
import { VideoModel } from '@server/models/video/video'
import { VideoCaptionModel } from '@server/models/video/video-caption'
import { VideoFileModel } from '@server/models/video/video-file'
import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
import {
MChannelAccountLight,
MStreamingPlaylistFilesVideo,
MThumbnail,
MVideoCaption,
MVideoFullLight,
MVideoThumbnail
} from '@server/types/models'
import { ThumbnailType, VideoObject } from '@shared/models'
import {
getPreviewFromIcons,
getTagsFromObject,
getThumbnailFromIcons,
streamingPlaylistActivityUrlToDBAttributes,
videoActivityObjectToDBAttributes,
videoFileActivityUrlToDBAttributes
} from './object-to-model-attributes'
import { getTrackerUrls, setVideoTrackers } from './trackers'
async function createVideo (videoObject: VideoObject, 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({
downloadUrl: getThumbnailFromIcons(videoObject).url,
video,
type: ThumbnailType.MINIATURE
}).catch(err => {
logger.error('Cannot create miniature from url.', { err })
return undefined
})
let thumbnailModel: MThumbnail
if (waitThumbnail === true) {
thumbnailModel = await promiseThumbnail
}
const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
try {
const sequelizeOptions = { transaction: t }
const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
videoCreated.VideoChannel = channel
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
const previewIcon = getPreviewFromIcons(videoObject)
if (previewIcon) {
const previewModel = createPlaceholderThumbnail({
fileUrl: previewIcon.url,
video: videoCreated,
type: ThumbnailType.PREVIEW,
size: previewIcon
})
await videoCreated.addAndSaveThumbnail(previewModel, t)
}
// Process files
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
const videoFiles = await Promise.all(videoFilePromises)
const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
videoCreated.VideoStreamingPlaylists = []
for (const playlistAttributes of streamingPlaylistsAttributes) {
const playlist = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) as MStreamingPlaylistFilesVideo
playlist.Video = videoCreated
const playlistFiles = videoFileActivityUrlToDBAttributes(playlist, playlistAttributes.tagAPObject)
const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
playlist.VideoFiles = await Promise.all(videoFilePromises)
videoCreated.VideoStreamingPlaylists.push(playlist)
}
// Process tags
const tags = getTagsFromObject(videoObject)
await setVideoTags({ video: videoCreated, tags, transaction: t })
// Process captions
const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
const caption = new VideoCaptionModel({
videoId: videoCreated.id,
filename: VideoCaptionModel.generateCaptionName(c.identifier),
language: c.identifier,
fileUrl: c.url
}) as MVideoCaption
return VideoCaptionModel.insertOrReplaceLanguage(caption, t)
})
await Promise.all(videoCaptionsPromises)
// Process trackers
{
const trackers = getTrackerUrls(videoObject, videoCreated)
await setVideoTrackers({ video: videoCreated, trackers, transaction: t })
}
videoCreated.VideoFiles = videoFiles
if (videoCreated.isLive) {
const videoLive = new VideoLiveModel({
streamKey: null,
saveReplay: videoObject.liveSaveReplay,
permanentLive: videoObject.permanentLive,
videoId: videoCreated.id
})
videoCreated.VideoLive = await videoLive.save({ transaction: t })
}
// We added a video in this channel, set it as updated
await channel.setAsUpdated(t)
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 }
} catch (err) {
// FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released
// Remove thumbnail
if (thumbnailModel) await thumbnailModel.removeThumbnail()
throw err
}
})
if (waitThumbnail === false) {
// Error is already caught above
// eslint-disable-next-line @typescript-eslint/no-floating-promises
promiseThumbnail.then(thumbnailModel => {
if (!thumbnailModel) return
thumbnailModel = videoCreated.id
return thumbnailModel.save()
})
}
return { autoBlacklisted, videoCreated }
}
export {
createVideo
}
import { Transaction } from 'sequelize/types'
import { deleteNonExistingModels, resetSequelizeInstance } from '@server/helpers/database-utils'
import { resetSequelizeInstance } from '@server/helpers/database-utils'
import { logger } from '@server/helpers/logger'
import { sequelizeTypescript } from '@server/initializers/database'
import { Notifier } from '@server/lib/notifier'
import { PeerTubeSocket } from '@server/lib/peertube-socket'
import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail'
import { setVideoTags } from '@server/lib/video'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
import { VideoCaptionModel } from '@server/models/video/video-caption'
import { VideoFileModel } from '@server/models/video/video-file'
import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
import {
MChannelAccountLight,
MChannelDefault,
MStreamingPlaylistFilesVideo,
MThumbnail,
MVideoAccountLightBlacklistAllFiles,
MVideoCaption,
MVideoFile,
MVideoFullLight
} from '@server/types/models'
import { ThumbnailType, VideoObject, VideoPrivacy } from '@shared/models'
import {
getPreviewFromIcons,
getTagsFromObject,