]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Refactor getOrCreateAPVideo
authorChocobozzz <me@florianbigard.com>
Wed, 2 Jun 2021 13:47:05 +0000 (15:47 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 2 Jun 2021 14:57:53 +0000 (16:57 +0200)
19 files changed:
server/controllers/api/search.ts
server/controllers/api/videos/index.ts
server/lib/activitypub/playlist.ts
server/lib/activitypub/process/process-announce.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/process/process-dislike.ts
server/lib/activitypub/process/process-like.ts
server/lib/activitypub/process/process-undo.ts
server/lib/activitypub/process/process-update.ts
server/lib/activitypub/process/process-view.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/videos/fetch.ts [deleted file]
server/lib/activitypub/videos/get.ts [new file with mode: 0644]
server/lib/activitypub/videos/index.ts
server/lib/activitypub/videos/refresh.ts [new file with mode: 0644]
server/lib/activitypub/videos/shared/index.ts
server/lib/activitypub/videos/shared/url-to-object.ts [new file with mode: 0644]
server/lib/schedulers/videos-redundancy-scheduler.ts
server/models/utils.ts

index 77e3a024dcabdd188eadc8ba1f4cc5574e98406b..0cb5674c2f313f40609f313c09d9ffd70e716a78 100644 (file)
@@ -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 })
index 47ab098ef02bdba670a8c040737d5cacd78da3f6..db23e563032b09be01a36b2fefbcf4c1bfb306a9 100644 (file)
@@ -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 || ''
+}
index 7166c68a6529451dbf94ef9f38fa6cfb506505f2..8fe6e79f2eac8feb59bf8bef8cf05431fbfcccf1 100644 (file)
@@ -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) {
index 63082466e6e923a7eb779263b2852a2bc3ccde6d..ec23c705e5e9d037a1ebb57461d085c9a1da6a96 100644 (file)
@@ -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) {
index 9cded4dec00a28a7571ee3e1ce019d0b53a68a8a..ef5a3100e8cd52271b82637d0a79586ced4e69d2 100644 (file)
@@ -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)
index 089c7b881917da342dbfaa33bc19871cd5c8b41d..ecc57cd10367cefb3838ee36a2decc39b4cad0a1 100644 (file)
@@ -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)
index 8688b3b47b7e9870f73f8b86afcba5fd164e11fe..cd4e86cbb7e89ca98fc8f1f24d4b852547f6c14b 100644 (file)
@@ -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)
index 9f031b5287f2cc7333aae15af3fef8ab5bc882de..fdb8dac24b4a679f579928ec03de845abffba908 100644 (file)
@@ -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)
index 516bd8d706818c37cb6b1331392bfc323ec3adef..be3f6acac85d5f35ee6b6f09e3b052c09e0dd7f0 100644 (file)
@@ -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)
index 84697673b6ee5f1359a0b3db9b17ae91a9e1aac2..c2d41dd2845fe2e629416795eb81863e07d3e91c 100644 (file)
@@ -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)
index e23e0c0e71f051f34e18fbed284bc795f6b37489..722147b69e15a4cea081c7f7b52ca17fc6e86ffb 100644 (file)
@@ -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')
diff --git a/server/lib/activitypub/videos/fetch.ts b/server/lib/activitypub/videos/fetch.ts
deleted file mode 100644 (file)
index 5113c9d..0000000
+++ /dev/null
@@ -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
-}
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts
new file mode 100644 (file)
index 0000000..a8c41e1
--- /dev/null
@@ -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
+}
index b560acb76396992818263b0b82076d05d4bad7cd..b220625980c90610b785757a1eb58fb6e823abb7 100644 (file)
@@ -1,3 +1,4 @@
 export * from './federate'
-export * from './fetch'
+export * from './get'
+export * from './refresh'
 export * from './updater'
diff --git a/server/lib/activitypub/videos/refresh.ts b/server/lib/activitypub/videos/refresh.ts
new file mode 100644 (file)
index 0000000..205a3cc
--- /dev/null
@@ -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
+}
index 208a437051a2e6cb83340b42b216afeb547195ee..951403493ed8cdd61559ac0794394572fdf52c5b 100644 (file)
@@ -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'
diff --git a/server/lib/activitypub/videos/shared/url-to-object.ts b/server/lib/activitypub/videos/shared/url-to-object.ts
new file mode 100644 (file)
index 0000000..b1ecac8
--- /dev/null
@@ -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
+}
index 59b55ccccdfee16a68cb253b70c8b0a4f0d21928..b5a5eb697400e178879b4a1a7db3e4f014634c07 100644 (file)
@@ -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
   }
index e27625bc862c8a20504b0e12c0224d9539c598b3..83b2b8f03cba2ac31607088073e5e449cacbd910 100644 (file)
@@ -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()