Refactor AP video create/update
This commit is contained in:
parent
69290ab37b
commit
08a47c75f9
|
@ -1,20 +1,19 @@
|
||||||
import { checkUrlsSameHost, getAPId } from "@server/helpers/activitypub"
|
import { checkUrlsSameHost, getAPId } from '@server/helpers/activitypub'
|
||||||
import { sanitizeAndCheckVideoTorrentObject } from "@server/helpers/custom-validators/activitypub/videos"
|
import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
|
||||||
import { retryTransactionWrapper } from "@server/helpers/database-utils"
|
import { retryTransactionWrapper } from '@server/helpers/database-utils'
|
||||||
import { logger } from "@server/helpers/logger"
|
import { logger } from '@server/helpers/logger'
|
||||||
import { doJSONRequest, PeerTubeRequestError } from "@server/helpers/requests"
|
import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
|
||||||
import { fetchVideoByUrl, VideoFetchByUrlType } from "@server/helpers/video"
|
import { fetchVideoByUrl, VideoFetchByUrlType } from '@server/helpers/video'
|
||||||
import { REMOTE_SCHEME } from "@server/initializers/constants"
|
import { REMOTE_SCHEME } from '@server/initializers/constants'
|
||||||
import { ActorFollowScoreCache } from "@server/lib/files-cache"
|
import { ActorFollowScoreCache } from '@server/lib/files-cache'
|
||||||
import { JobQueue } from "@server/lib/job-queue"
|
import { JobQueue } from '@server/lib/job-queue'
|
||||||
import { VideoModel } from "@server/models/video/video"
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { MVideoAccountLight, MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from "@server/types/models"
|
import { MVideoAccountLight, MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
|
||||||
import { HttpStatusCode } from "@shared/core-utils"
|
import { HttpStatusCode } from '@shared/core-utils'
|
||||||
import { VideoObject } from "@shared/models"
|
import { VideoObject } from '@shared/models'
|
||||||
import { getOrCreateActorAndServerAndModel } from "../actor"
|
import { getOrCreateActorAndServerAndModel } from '../actor'
|
||||||
import { SyncParam, syncVideoExternalAttributes } from "./shared"
|
import { APVideoCreator, SyncParam, syncVideoExternalAttributes } from './shared'
|
||||||
import { createVideo } from "./shared/video-create"
|
import { APVideoUpdater } from './updater'
|
||||||
import { APVideoUpdater } from "./update"
|
|
||||||
|
|
||||||
async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
|
async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
|
||||||
logger.info('Fetching remote video %s.', videoUrl)
|
logger.info('Fetching remote video %s.', videoUrl)
|
||||||
|
@ -115,16 +114,17 @@ async function getOrCreateVideoAndAccountAndChannel (
|
||||||
return { video: videoFromDatabase, created: false }
|
return { video: videoFromDatabase, created: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
|
const { videoObject } = await fetchRemoteVideo(videoUrl)
|
||||||
if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + 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
|
const videoChannel = actor.VideoChannel
|
||||||
|
|
||||||
try {
|
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 }
|
return { video: videoCreated, created: true, autoBlacklisted }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './federate'
|
export * from './federate'
|
||||||
export * from './fetch'
|
export * from './fetch'
|
||||||
export * from './update'
|
export * from './updater'
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
|
export * from './abstract-builder'
|
||||||
|
export * from './creator'
|
||||||
export * from './object-to-model-attributes'
|
export * from './object-to-model-attributes'
|
||||||
export * from './trackers'
|
export * from './trackers'
|
||||||
export * from './video-create'
|
|
||||||
export * from './video-sync-attributes'
|
export * from './video-sync-attributes'
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoStreamingPlaylistType
|
VideoStreamingPlaylistType
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
|
import { VideoCaptionModel } from '@server/models/video/video-caption'
|
||||||
|
|
||||||
function getThumbnailFromIcons (videoObject: VideoObject) {
|
function getThumbnailFromIcons (videoObject: VideoObject) {
|
||||||
let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
|
let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
|
||||||
|
@ -44,7 +45,7 @@ function getTagsFromObject (videoObject: VideoObject) {
|
||||||
.map(t => t.name)
|
.map(t => t.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoFileActivityUrlToDBAttributes (
|
function getFileAttributesFromUrl (
|
||||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
||||||
urls: (ActivityTagObject | ActivityUrlObject)[]
|
urls: (ActivityTagObject | ActivityUrlObject)[]
|
||||||
) {
|
) {
|
||||||
|
@ -109,7 +110,7 @@ function videoFileActivityUrlToDBAttributes (
|
||||||
return attributes
|
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[]
|
const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
|
||||||
if (playlistUrls.length === 0) return []
|
if (playlistUrls.length === 0) return []
|
||||||
|
|
||||||
|
@ -134,6 +135,7 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
|
||||||
p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
|
p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
|
||||||
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
|
p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
|
|
||||||
tagAPObject: playlistUrlObject.tag
|
tagAPObject: playlistUrlObject.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +145,24 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
|
||||||
return attributes
|
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)
|
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
|
||||||
? VideoPrivacy.PUBLIC
|
? VideoPrivacy.PUBLIC
|
||||||
: VideoPrivacy.UNLISTED
|
: VideoPrivacy.UNLISTED
|
||||||
|
@ -203,10 +222,13 @@ export {
|
||||||
|
|
||||||
getTagsFromObject,
|
getTagsFromObject,
|
||||||
|
|
||||||
videoActivityObjectToDBAttributes,
|
getFileAttributesFromUrl,
|
||||||
|
getStreamingPlaylistAttributesFromObject,
|
||||||
|
|
||||||
videoFileActivityUrlToDBAttributes,
|
getLiveAttributesFromObject,
|
||||||
streamingPlaylistActivityUrlToDBAttributes
|
getCaptionAttributesFromObject,
|
||||||
|
|
||||||
|
getVideoAttributesFromObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,293 +0,0 @@
|
||||||
import { Transaction } from 'sequelize/types'
|
|
||||||
import { deleteNonExistingModels, 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,
|
|
||||||
getThumbnailFromIcons,
|
|
||||||
getTrackerUrls,
|
|
||||||
setVideoTrackers,
|
|
||||||
streamingPlaylistActivityUrlToDBAttributes,
|
|
||||||
videoActivityObjectToDBAttributes,
|
|
||||||
videoFileActivityUrlToDBAttributes
|
|
||||||
} from './shared'
|
|
||||||
|
|
||||||
export class APVideoUpdater {
|
|
||||||
private readonly video: MVideoAccountLightBlacklistAllFiles
|
|
||||||
private readonly videoObject: VideoObject
|
|
||||||
private readonly channel: MChannelDefault
|
|
||||||
private readonly overrideTo: string[]
|
|
||||||
|
|
||||||
private readonly wasPrivateVideo: boolean
|
|
||||||
private readonly wasUnlistedVideo: boolean
|
|
||||||
|
|
||||||
private readonly videoFieldsSave: any
|
|
||||||
|
|
||||||
private readonly oldVideoChannel: MChannelAccountLight
|
|
||||||
|
|
||||||
constructor (options: {
|
|
||||||
video: MVideoAccountLightBlacklistAllFiles
|
|
||||||
videoObject: VideoObject
|
|
||||||
channel: MChannelDefault
|
|
||||||
overrideTo?: string[]
|
|
||||||
}) {
|
|
||||||
this.video = options.video
|
|
||||||
this.videoObject = options.videoObject
|
|
||||||
this.channel = options.channel
|
|
||||||
this.overrideTo = options.overrideTo
|
|
||||||
|
|
||||||
this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE
|
|
||||||
this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED
|
|
||||||
|
|
||||||
this.oldVideoChannel = this.video.VideoChannel
|
|
||||||
|
|
||||||
this.videoFieldsSave = this.video.toJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
async update () {
|
|
||||||
logger.debug('Updating remote video "%s".', this.videoObject.uuid, { videoObject: this.videoObject, channel: this.channel })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const thumbnailModel = await this.tryToGenerateThumbnail()
|
|
||||||
|
|
||||||
const videoUpdated = await sequelizeTypescript.transaction(async t => {
|
|
||||||
this.checkChannelUpdateOrThrow()
|
|
||||||
|
|
||||||
const videoUpdated = await this.updateVideo(t)
|
|
||||||
|
|
||||||
await this.processIcons(videoUpdated, thumbnailModel, t)
|
|
||||||
await this.processWebTorrentFiles(videoUpdated, t)
|
|
||||||
await this.processStreamingPlaylists(videoUpdated, t)
|
|
||||||
await this.processTags(videoUpdated, t)
|
|
||||||
await this.processTrackers(videoUpdated, t)
|
|
||||||
await this.processCaptions(videoUpdated, t)
|
|
||||||
await this.processLive(videoUpdated, t)
|
|
||||||
|
|
||||||
return videoUpdated
|
|
||||||
})
|
|
||||||
|
|
||||||
await autoBlacklistVideoIfNeeded({
|
|
||||||
video: videoUpdated,
|
|
||||||
user: undefined,
|
|
||||||
isRemote: true,
|
|
||||||
isNew: false,
|
|
||||||
transaction: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
// Notify our users?
|
|
||||||
if (this.wasPrivateVideo || this.wasUnlistedVideo) {
|
|
||||||
Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoUpdated.isLive) {
|
|
||||||
PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
|
|
||||||
PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Remote video with uuid %s updated', this.videoObject.uuid)
|
|
||||||
|
|
||||||
return videoUpdated
|
|
||||||
} catch (err) {
|
|
||||||
this.catchUpdateError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryToGenerateThumbnail (): Promise<MThumbnail> {
|
|
||||||
return createVideoMiniatureFromUrl({
|
|
||||||
downloadUrl: getThumbnailFromIcons(this.videoObject).url,
|
|
||||||
video: this.video,
|
|
||||||
type: ThumbnailType.MINIATURE
|
|
||||||
}).catch(err => {
|
|
||||||
logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err })
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check we can update the channel: we trust the remote server
|
|
||||||
private checkChannelUpdateOrThrow () {
|
|
||||||
if (!this.oldVideoChannel.Actor.serverId || !this.channel.Actor.serverId) {
|
|
||||||
throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.oldVideoChannel.Actor.serverId !== this.channel.Actor.serverId) {
|
|
||||||
throw new Error(`New channel ${this.channel.Actor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateVideo (transaction: Transaction) {
|
|
||||||
const to = this.overrideTo || this.videoObject.to
|
|
||||||
const videoData = videoActivityObjectToDBAttributes(this.channel, this.videoObject, to)
|
|
||||||
this.video.name = videoData.name
|
|
||||||
this.video.uuid = videoData.uuid
|
|
||||||
this.video.url = videoData.url
|
|
||||||
this.video.category = videoData.category
|
|
||||||
this.video.licence = videoData.licence
|
|
||||||
this.video.language = videoData.language
|
|
||||||
this.video.description = videoData.description
|
|
||||||
this.video.support = videoData.support
|
|
||||||
this.video.nsfw = videoData.nsfw
|
|
||||||
this.video.commentsEnabled = videoData.commentsEnabled
|
|
||||||
this.video.downloadEnabled = videoData.downloadEnabled
|
|
||||||
this.video.waitTranscoding = videoData.waitTranscoding
|
|
||||||
this.video.state = videoData.state
|
|
||||||
this.video.duration = videoData.duration
|
|
||||||
this.video.createdAt = videoData.createdAt
|
|
||||||
this.video.publishedAt = videoData.publishedAt
|
|
||||||
this.video.originallyPublishedAt = videoData.originallyPublishedAt
|
|
||||||
this.video.privacy = videoData.privacy
|
|
||||||
this.video.channelId = videoData.channelId
|
|
||||||
this.video.views = videoData.views
|
|
||||||
this.video.isLive = videoData.isLive
|
|
||||||
|
|
||||||
// Ensures we update the updated video attribute
|
|
||||||
this.video.changed('updatedAt', true)
|
|
||||||
|
|
||||||
return this.video.save({ transaction }) as Promise<MVideoFullLight>
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processIcons (videoUpdated: MVideoFullLight, thumbnailModel: MThumbnail, t: Transaction) {
|
|
||||||
if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
|
|
||||||
|
|
||||||
// Don't fetch the preview that could be big, create a placeholder instead
|
|
||||||
const previewIcon = getPreviewFromIcons(this.videoObject)
|
|
||||||
if (videoUpdated.getPreview() && previewIcon) {
|
|
||||||
const previewModel = createPlaceholderThumbnail({
|
|
||||||
fileUrl: previewIcon.url,
|
|
||||||
video: videoUpdated,
|
|
||||||
type: ThumbnailType.PREVIEW,
|
|
||||||
size: previewIcon
|
|
||||||
})
|
|
||||||
await videoUpdated.addAndSaveThumbnail(previewModel, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWebTorrentFiles (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, this.videoObject.url)
|
|
||||||
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
|
||||||
|
|
||||||
// Remove video files that do not exist anymore
|
|
||||||
const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t)
|
|
||||||
await Promise.all(destroyTasks)
|
|
||||||
|
|
||||||
// Update or add other one
|
|
||||||
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
|
|
||||||
videoUpdated.VideoFiles = await Promise.all(upsertTasks)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processStreamingPlaylists (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, this.videoObject, videoUpdated.VideoFiles)
|
|
||||||
const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
|
|
||||||
|
|
||||||
// Remove video playlists that do not exist anymore
|
|
||||||
const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t)
|
|
||||||
await Promise.all(destroyTasks)
|
|
||||||
|
|
||||||
let oldStreamingPlaylistFiles: MVideoFile[] = []
|
|
||||||
for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) {
|
|
||||||
oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
videoUpdated.VideoStreamingPlaylists = []
|
|
||||||
|
|
||||||
for (const playlistAttributes of streamingPlaylistAttributes) {
|
|
||||||
const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
|
|
||||||
.then(([ streamingPlaylist ]) => streamingPlaylist as MStreamingPlaylistFilesVideo)
|
|
||||||
streamingPlaylistModel.Video = videoUpdated
|
|
||||||
|
|
||||||
const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
|
|
||||||
.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))
|
|
||||||
streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks)
|
|
||||||
|
|
||||||
videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processTags (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
const tags = getTagsFromObject(this.videoObject)
|
|
||||||
await setVideoTags({ video: videoUpdated, tags, transaction: t })
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processTrackers (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
const trackers = getTrackerUrls(this.videoObject, videoUpdated)
|
|
||||||
await setVideoTrackers({ video: videoUpdated, trackers, transaction: t })
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processCaptions (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
// Update captions
|
|
||||||
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
|
|
||||||
|
|
||||||
const videoCaptionsPromises = this.videoObject.subtitleLanguage.map(c => {
|
|
||||||
const caption = new VideoCaptionModel({
|
|
||||||
videoId: videoUpdated.id,
|
|
||||||
filename: VideoCaptionModel.generateCaptionName(c.identifier),
|
|
||||||
language: c.identifier,
|
|
||||||
fileUrl: c.url
|
|
||||||
}) as MVideoCaption
|
|
||||||
|
|
||||||
return VideoCaptionModel.insertOrReplaceLanguage(caption, t)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(videoCaptionsPromises)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processLive (videoUpdated: MVideoFullLight, t: Transaction) {
|
|
||||||
// Create or update existing live
|
|
||||||
if (this.video.isLive) {
|
|
||||||
const [ videoLive ] = await VideoLiveModel.upsert({
|
|
||||||
saveReplay: this.videoObject.liveSaveReplay,
|
|
||||||
permanentLive: this.videoObject.permanentLive,
|
|
||||||
videoId: this.video.id
|
|
||||||
}, { transaction: t, returning: true })
|
|
||||||
|
|
||||||
videoUpdated.VideoLive = videoLive
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete existing live if it exists
|
|
||||||
await VideoLiveModel.destroy({
|
|
||||||
where: {
|
|
||||||
videoId: this.video.id
|
|
||||||
},
|
|
||||||
transaction: t
|
|
||||||
})
|
|
||||||
|
|
||||||
videoUpdated.VideoLive = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private catchUpdateError (err: Error) {
|
|
||||||
if (this.video !== undefined && this.videoFieldsSave !== undefined) {
|
|
||||||
resetSequelizeInstance(this.video, this.videoFieldsSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just a debug because we will retry the insert
|
|
||||||
logger.debug('Cannot update the remote video.', { err })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { Transaction } from 'sequelize/types'
|
||||||
|
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 { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
|
||||||
|
import { VideoCaptionModel } from '@server/models/video/video-caption'
|
||||||
|
import { VideoLiveModel } from '@server/models/video/video-live'
|
||||||
|
import { MChannelAccountLight, MChannelDefault, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
|
||||||
|
import { VideoObject, VideoPrivacy } from '@shared/models'
|
||||||
|
import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared'
|
||||||
|
|
||||||
|
export class APVideoUpdater extends APVideoAbstractBuilder {
|
||||||
|
protected readonly videoObject: VideoObject
|
||||||
|
|
||||||
|
private readonly video: MVideoAccountLightBlacklistAllFiles
|
||||||
|
private readonly channel: MChannelDefault
|
||||||
|
private readonly overrideTo: string[]
|
||||||
|
|
||||||
|
private readonly wasPrivateVideo: boolean
|
||||||
|
private readonly wasUnlistedVideo: boolean
|
||||||
|
|
||||||
|
private readonly videoFieldsSave: any
|
||||||
|
|
||||||
|
private readonly oldVideoChannel: MChannelAccountLight
|
||||||
|
|
||||||
|
constructor (options: {
|
||||||
|
video: MVideoAccountLightBlacklistAllFiles
|
||||||
|
videoObject: VideoObject
|
||||||
|
channel: MChannelDefault
|
||||||
|
overrideTo?: string[]
|
||||||
|
}) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.video = options.video
|
||||||
|
this.videoObject = options.videoObject
|
||||||
|
this.channel = options.channel
|
||||||
|
this.overrideTo = options.overrideTo
|
||||||
|
|
||||||
|
this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE
|
||||||
|
this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED
|
||||||
|
|
||||||
|
this.oldVideoChannel = this.video.VideoChannel
|
||||||
|
|
||||||
|
this.videoFieldsSave = this.video.toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
async update () {
|
||||||
|
logger.debug('Updating remote video "%s".', this.videoObject.uuid, { videoObject: this.videoObject, channel: this.channel })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const thumbnailModel = await this.tryToGenerateThumbnail(this.video)
|
||||||
|
|
||||||
|
const videoUpdated = await sequelizeTypescript.transaction(async t => {
|
||||||
|
this.checkChannelUpdateOrThrow()
|
||||||
|
|
||||||
|
const videoUpdated = await this.updateVideo(t)
|
||||||
|
|
||||||
|
if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
|
||||||
|
|
||||||
|
await this.setPreview(videoUpdated, t)
|
||||||
|
await this.setWebTorrentFiles(videoUpdated, t)
|
||||||
|
await this.setStreamingPlaylists(videoUpdated, t)
|
||||||
|
await this.setTags(videoUpdated, t)
|
||||||
|
await this.setTrackers(videoUpdated, t)
|
||||||
|
await this.setCaptions(videoUpdated, t)
|
||||||
|
await this.setOrDeleteLive(videoUpdated, t)
|
||||||
|
|
||||||
|
return videoUpdated
|
||||||
|
})
|
||||||
|
|
||||||
|
await autoBlacklistVideoIfNeeded({
|
||||||
|
video: videoUpdated,
|
||||||
|
user: undefined,
|
||||||
|
isRemote: true,
|
||||||
|
isNew: false,
|
||||||
|
transaction: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Notify our users?
|
||||||
|
if (this.wasPrivateVideo || this.wasUnlistedVideo) {
|
||||||
|
Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoUpdated.isLive) {
|
||||||
|
PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
|
||||||
|
PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Remote video with uuid %s updated', this.videoObject.uuid)
|
||||||
|
|
||||||
|
return videoUpdated
|
||||||
|
} catch (err) {
|
||||||
|
this.catchUpdateError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we can update the channel: we trust the remote server
|
||||||
|
private checkChannelUpdateOrThrow () {
|
||||||
|
if (!this.oldVideoChannel.Actor.serverId || !this.channel.Actor.serverId) {
|
||||||
|
throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.oldVideoChannel.Actor.serverId !== this.channel.Actor.serverId) {
|
||||||
|
throw new Error(`New channel ${this.channel.Actor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateVideo (transaction: Transaction) {
|
||||||
|
const to = this.overrideTo || this.videoObject.to
|
||||||
|
const videoData = getVideoAttributesFromObject(this.channel, this.videoObject, to)
|
||||||
|
this.video.name = videoData.name
|
||||||
|
this.video.uuid = videoData.uuid
|
||||||
|
this.video.url = videoData.url
|
||||||
|
this.video.category = videoData.category
|
||||||
|
this.video.licence = videoData.licence
|
||||||
|
this.video.language = videoData.language
|
||||||
|
this.video.description = videoData.description
|
||||||
|
this.video.support = videoData.support
|
||||||
|
this.video.nsfw = videoData.nsfw
|
||||||
|
this.video.commentsEnabled = videoData.commentsEnabled
|
||||||
|
this.video.downloadEnabled = videoData.downloadEnabled
|
||||||
|
this.video.waitTranscoding = videoData.waitTranscoding
|
||||||
|
this.video.state = videoData.state
|
||||||
|
this.video.duration = videoData.duration
|
||||||
|
this.video.createdAt = videoData.createdAt
|
||||||
|
this.video.publishedAt = videoData.publishedAt
|
||||||
|
this.video.originallyPublishedAt = videoData.originallyPublishedAt
|
||||||
|
this.video.privacy = videoData.privacy
|
||||||
|
this.video.channelId = videoData.channelId
|
||||||
|
this.video.views = videoData.views
|
||||||
|
this.video.isLive = videoData.isLive
|
||||||
|
|
||||||
|
// Ensures we update the updated video attribute
|
||||||
|
this.video.changed('updatedAt', true)
|
||||||
|
|
||||||
|
return this.video.save({ transaction }) as Promise<MVideoFullLight>
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setCaptions (videoUpdated: MVideoFullLight, t: Transaction) {
|
||||||
|
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
|
||||||
|
|
||||||
|
await this.insertOrReplaceCaptions(videoUpdated, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction: Transaction) {
|
||||||
|
if (this.video.isLive) return this.insertOrReplaceLive(videoUpdated, transaction)
|
||||||
|
|
||||||
|
// Delete existing live if it exists
|
||||||
|
await VideoLiveModel.destroy({
|
||||||
|
where: {
|
||||||
|
videoId: this.video.id
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
videoUpdated.VideoLive = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private catchUpdateError (err: Error) {
|
||||||
|
if (this.video !== undefined && this.videoFieldsSave !== undefined) {
|
||||||
|
resetSequelizeInstance(this.video, this.videoFieldsSave)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just a debug because we will retry the insert
|
||||||
|
logger.debug('Cannot update the remote video.', { err })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
|
@ -774,9 +774,11 @@ async function completeVideoCheck (
|
||||||
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
|
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(videoDetails.thumbnailPath).to.exist
|
||||||
await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
|
await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
|
||||||
|
|
||||||
if (attributes.previewfile) {
|
if (attributes.previewfile) {
|
||||||
|
expect(videoDetails.previewPath).to.exist
|
||||||
await testImage(url, attributes.previewfile, videoDetails.previewPath)
|
await testImage(url, attributes.previewfile, videoDetails.previewPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue