Refactor getOrCreateAPVideo

This commit is contained in:
Chocobozzz 2021-06-02 15:47:05 +02:00
parent c56faf0d94
commit 304a84d59c
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 243 additions and 209 deletions

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils' import { sanitizeUrl } from '@server/helpers/core-utils'
import { doJSONRequest } from '@server/helpers/requests' import { doJSONRequest } from '@server/helpers/requests'
import { CONFIG } from '@server/initializers/config' import { CONFIG } from '@server/initializers/config'
import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { AccountBlocklistModel } from '@server/models/account/account-blocklist' import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
@ -244,7 +244,7 @@ async function searchVideoURI (url: string, res: express.Response) {
refreshVideo: false refreshVideo: false
} }
const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam }) const result = await getOrCreateAPVideo({ videoObject: url, syncParam })
video = result ? result.video : undefined video = result ? result.video : undefined
} catch (err) { } catch (err) {
logger.info('Cannot search remote video %s.', url, { err }) logger.info('Cannot search remote video %s.', url, { err })

View File

@ -1,17 +1,18 @@
import * as express from 'express' import * as express from 'express'
import toInt from 'validator/lib/toInt' import toInt from 'validator/lib/toInt'
import { doJSONRequest } from '@server/helpers/requests'
import { LiveManager } from '@server/lib/live-manager' import { LiveManager } from '@server/lib/live-manager'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { MVideoAccountLight } from '@server/types/models'
import { VideosCommonQuery } from '../../../../shared' import { VideosCommonQuery } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs' import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils' import { getFormattedObjects } from '../../../helpers/utils'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { sendView } from '../../../lib/activitypub/send/send-view' import { sendView } from '../../../lib/activitypub/send/send-view'
import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
import { JobQueue } from '../../../lib/job-queue' import { JobQueue } from '../../../lib/job-queue'
import { Hooks } from '../../../lib/plugins/hooks' import { Hooks } from '../../../lib/plugins/hooks'
import { Redis } from '../../../lib/redis' import { Redis } from '../../../lib/redis'
@ -245,3 +246,15 @@ async function removeVideo (_req: express.Request, res: express.Response) {
.status(HttpStatusCode.NO_CONTENT_204) .status(HttpStatusCode.NO_CONTENT_204)
.end() .end()
} }
// ---------------------------------------------------------------------------
// FIXME: Should not exist, we rely on specific API
async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
const host = video.VideoChannel.Account.Actor.Server.host
const path = video.getDescriptionAPIPath()
const url = REMOTE_SCHEME.HTTP + '://' + host + path
const { body } = await doJSONRequest<any>(url)
return body.description || ''
}

View File

@ -18,7 +18,7 @@ import { FilteredModelAttributes } from '../../types/sequelize'
import { createPlaylistMiniatureFromUrl } from '../thumbnail' import { createPlaylistMiniatureFromUrl } from '../thumbnail'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { crawlCollectionPage } from './crawl' import { crawlCollectionPage } from './crawl'
import { getOrCreateVideoAndAccountAndChannel } from './videos' import { getOrCreateAPVideo } from './videos'
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC) const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@ -169,7 +169,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`)
} }
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: { id: body.url }, fetchType: 'only-video' }) const { video } = await getOrCreateAPVideo({ videoObject: { id: body.url }, fetchType: 'only-video' })
elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video)) elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video))
} catch (err) { } catch (err) {

View File

@ -3,7 +3,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
import { Notifier } from '../../notifier' import { Notifier } from '../../notifier'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
@ -32,7 +32,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act
let videoCreated: boolean let videoCreated: boolean
try { try {
const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) const result = await getOrCreateAPVideo({ videoObject: objectUri })
video = result.video video = result.video
videoCreated = result.created videoCreated = result.created
} catch (err) { } catch (err) {

View File

@ -12,7 +12,7 @@ import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlist' import { createOrUpdateVideoPlaylist } from '../playlist'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { resolveThread } from '../video-comments' import { resolveThread } from '../video-comments'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
import { isBlockedByServerOrAccount } from '@server/lib/blocklist' import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
@ -55,7 +55,7 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
const videoToCreateData = activity.object as VideoObject const videoToCreateData = activity.object as VideoObject
const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData, syncParam }) const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video)
@ -67,7 +67,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor
const cacheFile = activity.object as CacheFileObject const cacheFile = activity.object as CacheFileObject
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
return createOrUpdateCacheFile(cacheFile, video, byActor, t) return createOrUpdateCacheFile(cacheFile, video, byActor, t)

View File

@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -30,7 +30,7 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject }) const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject })
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)

View File

@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -27,7 +27,7 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
const byAccount = byActor.Account const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoUrl }) const { video } = await getOrCreateAPVideo({ videoObject: videoUrl })
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)

View File

@ -11,7 +11,7 @@ import { VideoShareModel } from '../../../models/video/video-share'
import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorSignature } from '../../../types/models' import { MActorSignature } from '../../../types/models'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -55,7 +55,7 @@ export {
async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
const likeActivity = activity.object as ActivityLike const likeActivity = activity.object as ActivityLike
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: likeActivity.object }) const { video } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
@ -80,7 +80,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
? activity.object ? activity.object
: activity.object.object as DislikeObject : activity.object.object as DislikeObject
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) const { video } = await getOrCreateAPVideo({ videoObject: dislike.object })
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
@ -103,7 +103,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
const cacheFileObject = activity.object.object as CacheFileObject const cacheFileObject = activity.object.object as CacheFileObject
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object }) const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id) const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id)

View File

@ -17,7 +17,7 @@ import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } f
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { createOrUpdateVideoPlaylist } from '../playlist' import { createOrUpdateVideoPlaylist } from '../playlist'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { APVideoUpdater, getOrCreateVideoAndAccountAndChannel } from '../videos' import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -63,7 +63,7 @@ async function processUpdateVideo (activity: ActivityUpdate) {
return undefined return undefined
} }
const { video, created } = await getOrCreateVideoAndAccountAndChannel({ const { video, created } = await getOrCreateAPVideo({
videoObject: videoObject.id, videoObject: videoObject.id,
allowRefresh: false, allowRefresh: false,
fetchType: 'all' fetchType: 'all'
@ -85,7 +85,7 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ
return undefined return undefined
} }
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object }) const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)

View File

@ -1,4 +1,4 @@
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateAPVideo } from '../videos'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { Redis } from '../../redis' import { Redis } from '../../redis'
import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub'
@ -29,7 +29,7 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct
fetchType: 'only-video' as 'only-video', fetchType: 'only-video' as 'only-video',
allowRefresh: false as false allowRefresh: false as false
} }
const { video } = await getOrCreateVideoAndAccountAndChannel(options) const { video } = await getOrCreateAPVideo(options)
if (!video.isLive) { if (!video.isLive) {
await Redis.Instance.addVideoView(video.id) await Redis.Instance.addVideoView(video.id)

View File

@ -7,7 +7,7 @@ import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/cons
import { VideoCommentModel } from '../../models/video/video-comment' import { VideoCommentModel } from '../../models/video/video-comment'
import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { getOrCreateVideoAndAccountAndChannel } from './videos' import { getOrCreateAPVideo } from './videos'
type ResolveThreadParams = { type ResolveThreadParams = {
url: string url: string
@ -89,7 +89,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
// Maybe it's a reply to a video? // Maybe it's a reply to a video?
// If yes, it's done: we resolved all the thread // If yes, it's done: we resolved all the thread
const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam }) const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !video.hasPrivacyForFederation()) { if (video.isOwned() && !video.hasPrivacyForFederation()) {
throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')

View File

@ -1,180 +0,0 @@
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 { 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)
const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body })
return { statusCode, videoObject: undefined }
}
return { statusCode, videoObject: body }
}
async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
const host = video.VideoChannel.Account.Actor.Server.host
const path = video.getDescriptionAPIPath()
const url = REMOTE_SCHEME.HTTP + '://' + host + path
const { body } = await doJSONRequest<any>(url)
return body.description || ''
}
type GetVideoResult <T> = Promise<{
video: T
created: boolean
autoBlacklisted?: boolean
}>
type GetVideoParamAll = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType?: 'all'
allowRefresh?: boolean
}
type GetVideoParamImmutable = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType: 'only-immutable-attributes'
allowRefresh: false
}
type GetVideoParamOther = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType?: 'all' | 'only-video'
allowRefresh?: boolean
}
function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
function getOrCreateVideoAndAccountAndChannel (
options: GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
async function getOrCreateVideoAndAccountAndChannel (
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
// 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 allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
const refreshOptions = {
video: videoFromDatabase as MVideoThumbnail,
fetchedType: fetchType,
syncParam
}
if (syncParam.refreshVideo === true) {
videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
} else {
await JobQueue.Instance.createJobWithPromise({
type: 'activitypub-refresher',
payload: { type: 'video', url: videoFromDatabase.url }
})
}
}
return { video: videoFromDatabase, created: false }
}
const { videoObject } = await fetchRemoteVideo(videoUrl)
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
try {
const creator = new APVideoCreator(videoObject)
const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator), syncParam.thumbnail)
await syncVideoExternalAttributes(videoCreated, videoObject, syncParam)
return { video: videoCreated, created: true, autoBlacklisted }
} catch (err) {
// Maybe a concurrent getOrCreateVideoAndAccountAndChannel call created this video
if (err.name === 'SequelizeUniqueConstraintError') {
const fallbackVideo = await fetchVideoByUrl(videoUrl, fetchType)
if (fallbackVideo) return { video: fallbackVideo, created: false }
}
throw err
}
}
async function refreshVideoIfNeeded (options: {
video: MVideoThumbnail
fetchedType: VideoFetchByUrlType
syncParam: SyncParam
}): Promise<MVideoThumbnail> {
if (!options.video.isOutdated()) return options.video
// We need more attributes if the argument video was fetched with not enough joints
const video = options.fetchedType === 'all'
? options.video as MVideoAccountLightBlacklistAllFiles
: await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
try {
const { videoObject } = await fetchRemoteVideo(video.url)
if (videoObject === undefined) {
logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
await video.setAsRefreshed()
return video
}
const videoUpdater = new APVideoUpdater(videoObject, video)
await videoUpdater.update()
await syncVideoExternalAttributes(video, videoObject, options.syncParam)
ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId)
return video
} catch (err) {
if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
// Video does not exist anymore
await video.destroy()
return undefined
}
logger.warn('Cannot refresh video %s.', options.video.url, { err })
ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
// Don't refresh in loop
await video.setAsRefreshed()
return video
}
}
export {
fetchRemoteVideo,
fetchRemoteVideoDescription,
refreshVideoIfNeeded,
getOrCreateVideoAndAccountAndChannel
}

View File

@ -0,0 +1,109 @@
import { getAPId } from '@server/helpers/activitypub'
import { retryTransactionWrapper } from '@server/helpers/database-utils'
import { fetchVideoByUrl, VideoFetchByUrlType } from '@server/helpers/video'
import { JobQueue } from '@server/lib/job-queue'
import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
import { refreshVideoIfNeeded } from './refresh'
import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
type GetVideoResult <T> = Promise<{
video: T
created: boolean
autoBlacklisted?: boolean
}>
type GetVideoParamAll = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType?: 'all'
allowRefresh?: boolean
}
type GetVideoParamImmutable = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType: 'only-immutable-attributes'
allowRefresh: false
}
type GetVideoParamOther = {
videoObject: { id: string } | string
syncParam?: SyncParam
fetchType?: 'all' | 'only-video'
allowRefresh?: boolean
}
function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
async function getOrCreateAPVideo (
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
// 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 (allowRefresh === true) {
// Typings ensure allowRefresh === false in only-immutable-attributes fetch type
videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
}
return { video: videoFromDatabase, created: false }
}
const { videoObject } = await fetchRemoteVideo(videoUrl)
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
try {
const creator = new APVideoCreator(videoObject)
const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator), syncParam.thumbnail)
await syncVideoExternalAttributes(videoCreated, videoObject, syncParam)
return { video: videoCreated, created: true, autoBlacklisted }
} catch (err) {
// Maybe a concurrent getOrCreateAPVideo call created this video
if (err.name === 'SequelizeUniqueConstraintError') {
const alreadyCreatedVideo = await fetchVideoByUrl(videoUrl, fetchType)
if (alreadyCreatedVideo) return { video: alreadyCreatedVideo, created: false }
}
throw err
}
}
// ---------------------------------------------------------------------------
export {
getOrCreateAPVideo
}
// ---------------------------------------------------------------------------
async function scheduleRefresh (video: MVideoThumbnail, fetchType: VideoFetchByUrlType, syncParam: SyncParam) {
if (!video.isOutdated()) return video
const refreshOptions = {
video,
fetchedType: fetchType,
syncParam
}
if (syncParam.refreshVideo === true) {
return refreshVideoIfNeeded(refreshOptions)
}
await JobQueue.Instance.createJobWithPromise({
type: 'activitypub-refresher',
payload: { type: 'video', url: video.url }
})
return video
}

View File

@ -1,3 +1,4 @@
export * from './federate' export * from './federate'
export * from './fetch' export * from './get'
export * from './refresh'
export * from './updater' export * from './updater'

View File

@ -0,0 +1,64 @@
import { logger } from '@server/helpers/logger'
import { PeerTubeRequestError } from '@server/helpers/requests'
import { VideoFetchByUrlType } from '@server/helpers/video'
import { ActorFollowScoreCache } from '@server/lib/files-cache'
import { VideoModel } from '@server/models/video/video'
import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models'
import { HttpStatusCode } from '@shared/core-utils'
import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
import { APVideoUpdater } from './updater'
async function refreshVideoIfNeeded (options: {
video: MVideoThumbnail
fetchedType: VideoFetchByUrlType
syncParam: SyncParam
}): Promise<MVideoThumbnail> {
if (!options.video.isOutdated()) return options.video
// We need more attributes if the argument video was fetched with not enough joints
const video = options.fetchedType === 'all'
? options.video as MVideoAccountLightBlacklistAllFiles
: await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
try {
const { videoObject } = await fetchRemoteVideo(video.url)
if (videoObject === undefined) {
logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
await video.setAsRefreshed()
return video
}
const videoUpdater = new APVideoUpdater(videoObject, video)
await videoUpdater.update()
await syncVideoExternalAttributes(video, videoObject, options.syncParam)
ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId)
return video
} catch (err) {
if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
// Video does not exist anymore
await video.destroy()
return undefined
}
logger.warn('Cannot refresh video %s.', options.video.url, { err })
ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
// Don't refresh in loop
await video.setAsRefreshed()
return video
}
}
// ---------------------------------------------------------------------------
export {
refreshVideoIfNeeded
}

View File

@ -2,4 +2,5 @@ export * from './abstract-builder'
export * from './creator' export * from './creator'
export * from './object-to-model-attributes' export * from './object-to-model-attributes'
export * from './trackers' export * from './trackers'
export * from './url-to-object'
export * from './video-sync-attributes' export * from './video-sync-attributes'

View File

@ -0,0 +1,22 @@
import { checkUrlsSameHost } from '@server/helpers/activitypub'
import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
import { logger } from '@server/helpers/logger'
import { doJSONRequest } from '@server/helpers/requests'
import { VideoObject } from '@shared/models'
async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
logger.info('Fetching remote video %s.', videoUrl)
const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body })
return { statusCode, videoObject: undefined }
}
return { statusCode, videoObject: body }
}
export {
fetchRemoteVideo
}

View File

@ -23,7 +23,7 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../.
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos' import { getOrCreateAPVideo } from '../activitypub/videos'
import { downloadPlaylistSegments } from '../hls' import { downloadPlaylistSegments } from '../hls'
import { removeVideoRedundancy } from '../redundancy' import { removeVideoRedundancy } from '../redundancy'
import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths' import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths'
@ -351,7 +351,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true }, syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true },
fetchType: 'all' as 'all' fetchType: 'all' as 'all'
} }
const { video } = await getOrCreateVideoAndAccountAndChannel(getVideoOptions) const { video } = await getOrCreateAPVideo(getVideoOptions)
return video return video
} }

View File

@ -102,6 +102,10 @@ function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]):
} }
function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
if (!model.createdAt || !model.updatedAt) {
throw new Error('Miss createdAt & updatedAt attribuets to model')
}
const now = Date.now() const now = Date.now()
const createdAtTime = model.createdAt.getTime() const createdAtTime = model.createdAt.getTime()
const updatedAtTime = model.updatedAt.getTime() const updatedAtTime = model.updatedAt.getTime()