Refactor getOrCreateAPVideo
This commit is contained in:
parent
c56faf0d94
commit
304a84d59c
19 changed files with 243 additions and 209 deletions
|
@ -2,7 +2,7 @@ import * as express from 'express'
|
|||
import { sanitizeUrl } from '@server/helpers/core-utils'
|
||||
import { doJSONRequest } from '@server/helpers/requests'
|
||||
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 { AccountBlocklistModel } from '@server/models/account/account-blocklist'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
|
@ -244,7 +244,7 @@ async function searchVideoURI (url: string, res: express.Response) {
|
|||
refreshVideo: false
|
||||
}
|
||||
|
||||
const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
|
||||
const result = await getOrCreateAPVideo({ videoObject: url, syncParam })
|
||||
video = result ? result.video : undefined
|
||||
} catch (err) {
|
||||
logger.info('Cannot search remote video %s.', url, { err })
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import * as express from 'express'
|
||||
import toInt from 'validator/lib/toInt'
|
||||
import { doJSONRequest } from '@server/helpers/requests'
|
||||
import { LiveManager } from '@server/lib/live-manager'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { MVideoAccountLight } from '@server/types/models'
|
||||
import { VideosCommonQuery } from '../../../../shared'
|
||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
||||
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
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 { sendView } from '../../../lib/activitypub/send/send-view'
|
||||
import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
|
||||
import { JobQueue } from '../../../lib/job-queue'
|
||||
import { Hooks } from '../../../lib/plugins/hooks'
|
||||
import { Redis } from '../../../lib/redis'
|
||||
|
@ -245,3 +246,15 @@ async function removeVideo (_req: express.Request, res: express.Response) {
|
|||
.status(HttpStatusCode.NO_CONTENT_204)
|
||||
.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 || ''
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { FilteredModelAttributes } from '../../types/sequelize'
|
|||
import { createPlaylistMiniatureFromUrl } from '../thumbnail'
|
||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { crawlCollectionPage } from './crawl'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from './videos'
|
||||
import { getOrCreateAPVideo } from './videos'
|
||||
|
||||
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
|
||||
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}`)
|
||||
}
|
||||
|
||||
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))
|
||||
} catch (err) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
|||
import { sequelizeTypescript } from '../../../initializers/database'
|
||||
import { VideoShareModel } from '../../../models/video/video-share'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
import { Notifier } from '../../notifier'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
|
@ -32,7 +32,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act
|
|||
let videoCreated: boolean
|
||||
|
||||
try {
|
||||
const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri })
|
||||
const result = await getOrCreateAPVideo({ videoObject: objectUri })
|
||||
video = result.video
|
||||
videoCreated = result.created
|
||||
} catch (err) {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { createOrUpdateCacheFile } from '../cache-file'
|
|||
import { createOrUpdateVideoPlaylist } from '../playlist'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { resolveThread } from '../video-comments'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
|
||||
|
||||
async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
|
||||
|
@ -55,7 +55,7 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
|
|||
const videoToCreateData = activity.object as VideoObject
|
||||
|
||||
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)
|
||||
|
||||
|
@ -67,7 +67,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor
|
|||
|
||||
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 => {
|
||||
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
|
|||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
import { MActorSignature } from '../../../types/models'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
|
||||
async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
|
||||
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)
|
||||
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject })
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject })
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
|
|||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
import { MActorSignature } from '../../../types/models'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
|
||||
async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -27,7 +27,7 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
|
|||
const byAccount = byActor.Account
|
||||
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 => {
|
||||
const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
|
||||
|
|
|
@ -11,7 +11,7 @@ import { VideoShareModel } from '../../../models/video/video-share'
|
|||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
import { MActorSignature } from '../../../types/models'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
|
||||
async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -55,7 +55,7 @@ export {
|
|||
async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
|
||||
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 => {
|
||||
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.object as DislikeObject
|
||||
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object })
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: dislike.object })
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
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) {
|
||||
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 => {
|
||||
const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id)
|
||||
|
|
|
@ -17,7 +17,7 @@ import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } f
|
|||
import { createOrUpdateCacheFile } from '../cache-file'
|
||||
import { createOrUpdateVideoPlaylist } from '../playlist'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { APVideoUpdater, getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
|
||||
|
||||
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -63,7 +63,7 @@ async function processUpdateVideo (activity: ActivityUpdate) {
|
|||
return undefined
|
||||
}
|
||||
|
||||
const { video, created } = await getOrCreateVideoAndAccountAndChannel({
|
||||
const { video, created } = await getOrCreateAPVideo({
|
||||
videoObject: videoObject.id,
|
||||
allowRefresh: false,
|
||||
fetchType: 'all'
|
||||
|
@ -85,7 +85,7 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ
|
|||
return undefined
|
||||
}
|
||||
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object })
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { getOrCreateAPVideo } from '../videos'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { Redis } from '../../redis'
|
||||
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',
|
||||
allowRefresh: false as false
|
||||
}
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel(options)
|
||||
const { video } = await getOrCreateAPVideo(options)
|
||||
|
||||
if (!video.isLive) {
|
||||
await Redis.Instance.addVideoView(video.id)
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/cons
|
|||
import { VideoCommentModel } from '../../models/video/video-comment'
|
||||
import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
|
||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from './videos'
|
||||
import { getOrCreateAPVideo } from './videos'
|
||||
|
||||
type ResolveThreadParams = {
|
||||
url: string
|
||||
|
@ -89,7 +89,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
|
|||
// Maybe it's a reply to a video?
|
||||
// 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 { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
|
||||
|
||||
if (video.isOwned() && !video.hasPrivacyForFederation()) {
|
||||
throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')
|
||||
|
|
|
@ -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
|
||||
}
|
109
server/lib/activitypub/videos/get.ts
Normal file
109
server/lib/activitypub/videos/get.ts
Normal 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
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './federate'
|
||||
export * from './fetch'
|
||||
export * from './get'
|
||||
export * from './refresh'
|
||||
export * from './updater'
|
||||
|
|
64
server/lib/activitypub/videos/refresh.ts
Normal file
64
server/lib/activitypub/videos/refresh.ts
Normal 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
|
||||
}
|
|
@ -2,4 +2,5 @@ export * from './abstract-builder'
|
|||
export * from './creator'
|
||||
export * from './object-to-model-attributes'
|
||||
export * from './trackers'
|
||||
export * from './url-to-object'
|
||||
export * from './video-sync-attributes'
|
||||
|
|
22
server/lib/activitypub/videos/shared/url-to-object.ts
Normal file
22
server/lib/activitypub/videos/shared/url-to-object.ts
Normal 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
|
||||
}
|
|
@ -23,7 +23,7 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../.
|
|||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
|
||||
import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
|
||||
import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos'
|
||||
import { getOrCreateAPVideo } from '../activitypub/videos'
|
||||
import { downloadPlaylistSegments } from '../hls'
|
||||
import { removeVideoRedundancy } from '../redundancy'
|
||||
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 },
|
||||
fetchType: 'all' as 'all'
|
||||
}
|
||||
const { video } = await getOrCreateVideoAndAccountAndChannel(getVideoOptions)
|
||||
const { video } = await getOrCreateAPVideo(getVideoOptions)
|
||||
|
||||
return video
|
||||
}
|
||||
|
|
|
@ -102,6 +102,10 @@ function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]):
|
|||
}
|
||||
|
||||
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 createdAtTime = model.createdAt.getTime()
|
||||
const updatedAtTime = model.updatedAt.getTime()
|
||||
|
|
Loading…
Reference in a new issue