]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add refresh video on search
authorChocobozzz <me@florianbigard.com>
Wed, 22 Aug 2018 14:15:35 +0000 (16:15 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 27 Aug 2018 07:41:54 +0000 (09:41 +0200)
29 files changed:
.travis.yml
config/default.yaml
config/production.yaml.example
scripts/travis.sh
server/controllers/api/search.ts
server/initializers/constants.ts
server/lib/activitypub/process/process-announce.ts
server/lib/activitypub/process/process-create.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/share.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/video-rates.ts
server/lib/activitypub/videos.ts
server/models/video/video.ts
server/tests/api/index-1.ts [new file with mode: 0644]
server/tests/api/index-2.ts [new file with mode: 0644]
server/tests/api/index-3.ts [new file with mode: 0644]
server/tests/api/index-fast.ts [deleted file]
server/tests/api/index-slow.ts [deleted file]
server/tests/api/index.ts
server/tests/api/search/index.ts [new file with mode: 0644]
server/tests/api/search/search-activitypub-videos.ts [new file with mode: 0644]
server/tests/api/server/index.ts [new file with mode: 0644]
server/tests/api/users/index.ts [new file with mode: 0644]
server/tests/api/videos/index.ts [new file with mode: 0644]
server/tests/feeds/index.ts [new file with mode: 0644]
server/tests/index.ts

index ecb44c514dae9daf6e97fc041a21084a0d1a9f98..78e25cf4548870a14b36492b9f174865cc05a491 100644 (file)
@@ -36,8 +36,9 @@ before_script:
 matrix:
   include:
   - env: TEST_SUITE=misc
-  - env: TEST_SUITE=api-fast
-  - env: TEST_SUITE=api-slow
+  - env: TEST_SUITE=api-1
+  - env: TEST_SUITE=api-2
+  - env: TEST_SUITE=api-3
   - env: TEST_SUITE=cli
   - env: TEST_SUITE=lint
 
index 60da192b4f8f64d0fcbc77f6e12ee50ff6d6b320..6a02f254dc43650e77c64c60cf0ff45b7c4b9e1f 100644 (file)
@@ -57,6 +57,11 @@ storage:
 log:
   level: 'info' # debug/info/warning/error
 
+search:
+  remote_uri: # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
+    users: true
+    anonymous: false
+
 cache:
   previews:
     size: 500 # Max number of previews you want to cache
index 9e8b578292351986e90f045a325d5f64bc236637..272a3cb4676121af242300423eae5a25371a3a55 100644 (file)
@@ -58,6 +58,10 @@ storage:
 log:
   level: 'info' # debug/info/warning/error
 
+search:
+  remote_uri: # Add ability to search remote videos/actors by URI, that may not be federated with your instance
+    users: true
+    anonymous: false
 
 ###############################################################################
 #
index 390500ed451549e64ee7e469ea7d7e2f98e9ba0f..c2785ffa76bcd8adf797a70d35041900962367ea 100755 (executable)
@@ -12,19 +12,22 @@ killall -q peertube || true
 if [ "$1" = "misc" ]; then
     npm run build
     mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/client.ts server/tests/activitypub.ts \
-        server/tests/feeds/feeds.ts
+        server/tests/feeds/index.ts
 elif [ "$1" = "api" ]; then
     npm run build:server
     mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index.ts
 elif [ "$1" = "cli" ]; then
     npm run build:server
     mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/cli/index.ts
-elif [ "$1" = "api-fast" ]; then
+elif [ "$1" = "api-1" ]; then
     npm run build:server
-    mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-fast.ts
-elif [ "$1" = "api-slow" ]; then
+    mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-1.ts
+elif [ "$1" = "api-2" ]; then
     npm run build:server
-    mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-slow.ts
+    mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-2.ts
+elif [ "$1" = "api-3" ]; then
+    npm run build:server
+    mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/api/index-3.ts
 elif [ "$1" = "lint" ]; then
     ( cd client
       npm run lint
index 9c2c7d6c1f3b44b6088382643c7ae82e9bf06df4..d95e7cac9dd8ef6a0d44a25b6dc99eb6074548f2 100644 (file)
@@ -13,8 +13,10 @@ import {
   videosSearchSortValidator
 } from '../../middlewares'
 import { VideosSearchQuery } from '../../../shared/models/search'
-import { getOrCreateAccountAndVideoAndChannel } from '../../lib/activitypub'
+import { getOrCreateVideoAndAccountAndChannel } from '../../lib/activitypub'
 import { logger } from '../../helpers/logger'
+import { User } from '../../../shared/models/users'
+import { CONFIG } from '../../initializers/constants'
 
 const searchRouter = express.Router()
 
@@ -56,20 +58,30 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
 
 async function searchVideoUrl (url: string, res: express.Response) {
   let video: VideoModel
+  const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
 
-  try {
-    const syncParam = {
-      likes: false,
-      dislikes: false,
-      shares: false,
-      comments: false,
-      thumbnail: true
-    }
+  // Check if we can fetch a remote video with the URL
+  if (
+    CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
+    (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
+  ) {
+    try {
+      const syncParam = {
+        likes: false,
+        dislikes: false,
+        shares: false,
+        comments: false,
+        thumbnail: true,
+        refreshVideo: false
+      }
 
-    const res = await getOrCreateAccountAndVideoAndChannel(url, syncParam)
-    video = res ? res.video : undefined
-  } catch (err) {
-    logger.info('Cannot search remote video %s.', url)
+      const res = await getOrCreateVideoAndAccountAndChannel(url, syncParam)
+      video = res ? res.video : undefined
+    } catch (err) {
+      logger.info('Cannot search remote video %s.', url)
+    }
+  } else {
+    video = await VideoModel.loadByUrlAndPopulateAccount(url)
   }
 
   return res.json({
index 99b10a7fc90156a28f47f2e2052fe0a76ff6d62c..cd709cd3f29069625d9b19325d8ef81c658792d7 100644 (file)
@@ -181,6 +181,12 @@ const CONFIG = {
   LOG: {
     LEVEL: config.get<string>('log.level')
   },
+  SEARCH: {
+    REMOTE_URI: {
+      USERS: config.get<boolean>('search.remote_uri.users'),
+      ANONYMOUS: config.get<boolean>('search.remote_uri.anonymous')
+    }
+  },
   ADMIN: {
     get EMAIL () { return config.get<string>('admin.email') }
   },
@@ -462,7 +468,8 @@ const ACTIVITY_PUB = {
     MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
   },
   MAX_RECURSION_COMMENTS: 100,
-  ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 // 1 day
+  ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000, // 1 day
+  VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 // 1 day
 }
 
 const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
@@ -574,6 +581,7 @@ if (isTestInstance() === true) {
 
   ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
   ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
+  ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
 
   CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
 
index b08156aa165ad7cdd67d6b0d633c7fe8a12dca65..814556817314410652cf8e43e89d12cdc3422339 100644 (file)
@@ -6,7 +6,7 @@ import { VideoModel } from '../../../models/video/video'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { getOrCreateActorAndServerAndModel } from '../actor'
 import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAccountAndVideoAndChannel } from '../videos'
+import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 
 async function processAnnounceActivity (activity: ActivityAnnounce) {
   const actorAnnouncer = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -25,7 +25,7 @@ export {
 async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
   const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(objectUri)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(objectUri)
 
   return sequelizeTypescript.transaction(async t => {
     // Add share entry
index 9655d015ffd5abea095ed47cc9dd150e4abcc3af..e8f5ade065b19c7bf25c7d265a6f738894af3d74 100644 (file)
@@ -10,7 +10,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { getOrCreateActorAndServerAndModel } from '../actor'
 import { resolveThread } from '../video-comments'
-import { getOrCreateAccountAndVideoAndChannel } from '../videos'
+import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { forwardActivity, forwardVideoRelatedActivity } from '../send/utils'
 
 async function processCreateActivity (activity: ActivityCreate) {
@@ -45,7 +45,7 @@ export {
 async function processCreateVideo (activity: ActivityCreate) {
   const videoToCreateData = activity.object as VideoTorrentObject
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(videoToCreateData)
 
   return video
 }
@@ -56,7 +56,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
 
   if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(dislike.object)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(dislike.object)
 
   return sequelizeTypescript.transaction(async t => {
     const rate = {
@@ -83,7 +83,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
 async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
   const view = activity.object as ViewObject
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(view.object)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(view.object)
 
   const actor = await ActorModel.loadByUrl(view.actor)
   if (!actor) throw new Error('Unknown actor ' + view.actor)
@@ -103,7 +103,7 @@ async function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateDat
   const account = actor.Account
   if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url)
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(videoAbuseToCreateData.object)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(videoAbuseToCreateData.object)
 
   return sequelizeTypescript.transaction(async t => {
     const videoAbuseData = {
index d0865b78c6f34b83532bcf71c6f88e7517d07bb6..9e1664fd8ed8040d1343251b32dd7510ad923a70 100644 (file)
@@ -5,7 +5,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
 import { ActorModel } from '../../../models/activitypub/actor'
 import { getOrCreateActorAndServerAndModel } from '../actor'
 import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAccountAndVideoAndChannel } from '../videos'
+import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 
 async function processLikeActivity (activity: ActivityLike) {
   const actor = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -27,7 +27,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
   const byAccount = byActor.Account
   if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(videoUrl)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(videoUrl)
 
   return sequelizeTypescript.transaction(async t => {
     const rate = {
index b6de107ad9a3250f00e5beefdbe6294f2e667da2..eab9e3d61500241ff499f20e4eb6669e6216ad8b 100644 (file)
@@ -9,7 +9,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
 import { ActorModel } from '../../../models/activitypub/actor'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { forwardVideoRelatedActivity } from '../send/utils'
-import { getOrCreateAccountAndVideoAndChannel } from '../videos'
+import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { VideoShareModel } from '../../../models/video/video-share'
 
 async function processUndoActivity (activity: ActivityUndo) {
@@ -43,7 +43,7 @@ export {
 async function processUndoLike (actorUrl: string, activity: ActivityUndo) {
   const likeActivity = activity.object as ActivityLike
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(likeActivity.object)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(likeActivity.object)
 
   return sequelizeTypescript.transaction(async t => {
     const byAccount = await AccountModel.loadByUrl(actorUrl, t)
@@ -67,7 +67,7 @@ async function processUndoLike (actorUrl: string, activity: ActivityUndo) {
 async function processUndoDislike (actorUrl: string, activity: ActivityUndo) {
   const dislike = activity.object.object as DislikeObject
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(dislike.object)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(dislike.object)
 
   return sequelizeTypescript.transaction(async t => {
     const byAccount = await AccountModel.loadByUrl(actorUrl, t)
index 11226e2753661637ba9031c3fa352e2eb2abaa6d..07a5ff92f1227936fb848e40d59c7461646b8d86 100644 (file)
@@ -1,4 +1,3 @@
-import * as Bluebird from 'bluebird'
 import { ActivityUpdate, VideoTorrentObject } from '../../../../shared/models/activitypub'
 import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
 import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -6,19 +5,10 @@ import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers'
 import { AccountModel } from '../../../models/account/account'
 import { ActorModel } from '../../../models/activitypub/actor'
-import { TagModel } from '../../../models/video/tag'
 import { VideoChannelModel } from '../../../models/video/video-channel'
-import { VideoFileModel } from '../../../models/video/video-file'
 import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
-import {
-  generateThumbnailFromUrl,
-  getOrCreateAccountAndVideoAndChannel,
-  getOrCreateVideoChannel,
-  videoActivityObjectToDBAttributes,
-  videoFileActivityUrlToDBAttributes
-} from '../videos'
+import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannel, updateVideoFromAP } from '../videos'
 import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
-import { VideoCaptionModel } from '../../../models/video/video-caption'
 
 async function processUpdateActivity (activity: ActivityUpdate) {
   const actor = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -49,91 +39,10 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
     return undefined
   }
 
-  const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
+  const { video } = await getOrCreateVideoAndAccountAndChannel(videoObject.id)
+  const channelActor = await getOrCreateVideoChannel(videoObject)
 
-  // Fetch video channel outside the transaction
-  const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
-  const newVideoChannel = newVideoChannelActor.VideoChannel
-
-  logger.debug('Updating remote video "%s".', videoObject.uuid)
-  let videoInstance = res.video
-  let videoFieldsSave: any
-
-  try {
-    await sequelizeTypescript.transaction(async t => {
-      const sequelizeOptions = {
-        transaction: t
-      }
-
-      videoFieldsSave = videoInstance.toJSON()
-
-      // Check actor has the right to update the video
-      const videoChannel = videoInstance.VideoChannel
-      if (videoChannel.Account.Actor.id !== actor.id) {
-        throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
-      }
-
-      const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
-      videoInstance.set('name', videoData.name)
-      videoInstance.set('uuid', videoData.uuid)
-      videoInstance.set('url', videoData.url)
-      videoInstance.set('category', videoData.category)
-      videoInstance.set('licence', videoData.licence)
-      videoInstance.set('language', videoData.language)
-      videoInstance.set('description', videoData.description)
-      videoInstance.set('support', videoData.support)
-      videoInstance.set('nsfw', videoData.nsfw)
-      videoInstance.set('commentsEnabled', videoData.commentsEnabled)
-      videoInstance.set('waitTranscoding', videoData.waitTranscoding)
-      videoInstance.set('state', videoData.state)
-      videoInstance.set('duration', videoData.duration)
-      videoInstance.set('createdAt', videoData.createdAt)
-      videoInstance.set('updatedAt', videoData.updatedAt)
-      videoInstance.set('views', videoData.views)
-      videoInstance.set('privacy', videoData.privacy)
-      videoInstance.set('channelId', videoData.channelId)
-
-      await videoInstance.save(sequelizeOptions)
-
-      // Don't block on request
-      generateThumbnailFromUrl(videoInstance, videoObject.icon)
-        .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
-
-      // Remove old video files
-      const videoFileDestroyTasks: Bluebird<void>[] = []
-      for (const videoFile of videoInstance.VideoFiles) {
-        videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
-      }
-      await Promise.all(videoFileDestroyTasks)
-
-      const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
-      const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, sequelizeOptions))
-      await Promise.all(tasks)
-
-      // Update Tags
-      const tags = videoObject.tag.map(tag => tag.name)
-      const tagInstances = await TagModel.findOrCreateTags(tags, t)
-      await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
-
-      // Update captions
-      await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoInstance.id, t)
-
-      const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
-        return VideoCaptionModel.insertOrReplaceLanguage(videoInstance.id, c.identifier, t)
-      })
-      await Promise.all(videoCaptionsPromises)
-    })
-
-    logger.info('Remote video with uuid %s updated', videoObject.uuid)
-  } catch (err) {
-    if (videoInstance !== undefined && videoFieldsSave !== undefined) {
-      resetSequelizeInstance(videoInstance, videoFieldsSave)
-    }
-
-    // This is just a debug because we will retry the insert
-    logger.debug('Cannot update the remote video.', { err })
-    throw err
-  }
+  return updateVideoFromAP(video, videoObject, actor, channelActor, activity.to)
 }
 
 async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) {
index 69841486765b16df1404bab64360713282de1e0d..fe3d73e9b7db6139ce7aebd6a0bd182945b64ac8 100644 (file)
@@ -6,6 +6,11 @@ import { VideoShareModel } from '../../models/video/video-share'
 import { sendUndoAnnounce, sendVideoAnnounce } from './send'
 import { getAnnounceActivityPubUrl } from './url'
 import { VideoChannelModel } from '../../models/video/video-channel'
+import * as Bluebird from 'bluebird'
+import { doRequest } from '../../helpers/requests'
+import { getOrCreateActorAndServerAndModel } from './actor'
+import { logger } from '../../helpers/logger'
+import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
 
 async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
   if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -22,8 +27,41 @@ async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: Vide
   await shareByVideoChannel(video, t)
 }
 
+async function addVideoShares (shareUrls: string[], instance: VideoModel) {
+  await Bluebird.map(shareUrls, async shareUrl => {
+    try {
+      // Fetch url
+      const { body } = await doRequest({
+        uri: shareUrl,
+        json: true,
+        activityPub: true
+      })
+      if (!body || !body.actor) throw new Error('Body of body actor is invalid')
+
+      const actorUrl = body.actor
+      const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+
+      const entry = {
+        actorId: actor.id,
+        videoId: instance.id,
+        url: shareUrl
+      }
+
+      await VideoShareModel.findOrCreate({
+        where: {
+          url: shareUrl
+        },
+        defaults: entry
+      })
+    } catch (err) {
+      logger.warn('Cannot add share %s.', shareUrl, { err })
+    }
+  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
+}
+
 export {
   changeVideoChannelShare,
+  addVideoShares,
   shareVideoByServerAndChannel
 }
 
index 14c7fde69150b6eda3afdd589c25a9e3cf015117..beff557bcc6999304d5caa188cf30f6654a6f309 100644 (file)
@@ -7,7 +7,7 @@ import { ActorModel } from '../../models/activitypub/actor'
 import { VideoModel } from '../../models/video/video'
 import { VideoCommentModel } from '../../models/video/video-comment'
 import { getOrCreateActorAndServerAndModel } from './actor'
-import { getOrCreateAccountAndVideoAndChannel } from './videos'
+import { getOrCreateVideoAndAccountAndChannel } from './videos'
 import * as Bluebird from 'bluebird'
 
 async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
@@ -91,7 +91,7 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
 
   try {
     // Maybe it's a reply to a video?
-    const { video } = await getOrCreateAccountAndVideoAndChannel(url)
+    const { video } = await getOrCreateVideoAndAccountAndChannel(url)
 
     if (comments.length !== 0) {
       const firstReply = comments[ comments.length - 1 ]
index 19011b4ab835efb47a10515dcdc2d5ad4887846d..1619251c3b4f424f107f4203e1ccb79e00cb6535 100644 (file)
@@ -2,6 +2,45 @@ import { Transaction } from 'sequelize'
 import { AccountModel } from '../../models/account/account'
 import { VideoModel } from '../../models/video/video'
 import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send'
+import { VideoRateType } from '../../../shared/models/videos'
+import * as Bluebird from 'bluebird'
+import { getOrCreateActorAndServerAndModel } from './actor'
+import { AccountVideoRateModel } from '../../models/account/account-video-rate'
+import { logger } from '../../helpers/logger'
+import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
+
+async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
+  let rateCounts = 0
+
+  await Bluebird.map(actorUrls, async actorUrl => {
+    try {
+      const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+      const [ , created ] = await AccountVideoRateModel
+        .findOrCreate({
+          where: {
+            videoId: video.id,
+            accountId: actor.Account.id
+          },
+          defaults: {
+            videoId: video.id,
+            accountId: actor.Account.id,
+            type: rate
+          }
+        })
+
+      if (created) rateCounts += 1
+    } catch (err) {
+      logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err })
+    }
+  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
+
+  logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
+
+  // This is "likes" and "dislikes"
+  if (rateCounts !== 0) await video.increment(rate + 's', { by: rateCounts })
+
+  return
+}
 
 async function sendVideoRateChange (account: AccountModel,
                               video: VideoModel,
@@ -24,5 +63,6 @@ async function sendVideoRateChange (account: AccountModel,
 }
 
 export {
+  createRates,
   sendVideoRateChange
 }
index fac1d3fc74b67d68e604a318784a51362a8b38f8..388c31fe5d6d6dfba80a9e4ef6631e2fb8d0cf5b 100644 (file)
@@ -5,29 +5,30 @@ import { join } from 'path'
 import * as request from 'request'
 import { ActivityIconObject, VideoState } from '../../../shared/index'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
-import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
+import { VideoPrivacy } from '../../../shared/models/videos'
 import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
-import { retryTransactionWrapper } from '../../helpers/database-utils'
+import { resetSequelizeInstance, retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
-import { AccountVideoRateModel } from '../../models/account/account-video-rate'
+import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
 import { ActorModel } from '../../models/activitypub/actor'
 import { TagModel } from '../../models/video/tag'
 import { VideoModel } from '../../models/video/video'
 import { VideoChannelModel } from '../../models/video/video-channel'
 import { VideoFileModel } from '../../models/video/video-file'
-import { VideoShareModel } from '../../models/video/video-share'
-import { getOrCreateActorAndServerAndModel } from './actor'
+import { getOrCreateActorAndServerAndModel, updateActorAvatarInstance } from './actor'
 import { addVideoComments } from './video-comments'
 import { crawlCollectionPage } from './crawl'
 import { sendCreateVideo, sendUpdateVideo } from './send'
-import { shareVideoByServerAndChannel } from './index'
 import { isArray } from '../../helpers/custom-validators/misc'
 import { VideoCaptionModel } from '../../models/video/video-caption'
 import { JobQueue } from '../job-queue'
 import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
+import { getUrlFromWebfinger } from '../../helpers/webfinger'
+import { createRates } from './video-rates'
+import { addVideoShares, shareVideoByServerAndChannel } from './share'
+import { AccountModel } from '../../models/account/account'
 
 async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   // If the video is not private and published, we federate it
@@ -180,15 +181,11 @@ function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
   return getOrCreateActorAndServerAndModel(channel.id)
 }
 
-async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
+async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
   logger.debug('Adding remote video %s.', videoObject.id)
 
   const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
-    const sequelizeOptions = {
-      transaction: t
-    }
-    const videoFromDatabase = await VideoModel.loadByUUIDOrURLAndPopulateAccount(videoObject.uuid, videoObject.id, t)
-    if (videoFromDatabase) return videoFromDatabase
+    const sequelizeOptions = { transaction: t }
 
     const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
     const video = VideoModel.build(videoData)
@@ -230,26 +227,32 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
 }
 
 type SyncParam = {
-  likes: boolean,
-  dislikes: boolean,
-  shares: boolean,
-  comments: boolean,
+  likes: boolean
+  dislikes: boolean
+  shares: boolean
+  comments: boolean
   thumbnail: boolean
+  refreshVideo: boolean
 }
-async function getOrCreateAccountAndVideoAndChannel (
+async function getOrCreateVideoAndAccountAndChannel (
   videoObject: VideoTorrentObject | string,
-  syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
+  syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
 ) {
   const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
 
-  const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
-  if (videoFromDatabase) return { video: videoFromDatabase }
+  let videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
+  if (videoFromDatabase) {
+    const p = retryTransactionWrapper(refreshVideoIfNeeded, videoFromDatabase)
+    if (syncParam.refreshVideo === true) videoFromDatabase = await p
+
+    return { video: videoFromDatabase }
+  }
 
-  const fetchedVideo = await fetchRemoteVideo(videoUrl)
+  const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
   if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
 
   const channelActor = await getOrCreateVideoChannel(fetchedVideo)
-  const video = await retryTransactionWrapper(getOrCreateVideo, fetchedVideo, channelActor, syncParam.thumbnail)
+  const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
 
   // Process outside the transaction because we could fetch remote data
 
@@ -290,101 +293,153 @@ async function getOrCreateAccountAndVideoAndChannel (
   return { video }
 }
 
-async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
-  let rateCounts = 0
-
-  await Bluebird.map(actorUrls, async actorUrl => {
-    try {
-      const actor = await getOrCreateActorAndServerAndModel(actorUrl)
-      const [ , created ] = await AccountVideoRateModel
-        .findOrCreate({
-          where: {
-            videoId: video.id,
-            accountId: actor.Account.id
-          },
-          defaults: {
-            videoId: video.id,
-            accountId: actor.Account.id,
-            type: rate
-          }
-        })
-
-      if (created) rateCounts += 1
-    } catch (err) {
-      logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err })
+async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoTorrentObject }> {
+  const options = {
+    uri: videoUrl,
+    method: 'GET',
+    json: true,
+    activityPub: true
+  }
+
+  logger.info('Fetching remote video %s.', videoUrl)
+
+  const { response, body } = await doRequest(options)
+
+  if (sanitizeAndCheckVideoTorrentObject(body) === false) {
+    logger.debug('Remote video JSON is not valid.', { body })
+    return { response, videoObject: undefined }
+  }
+
+  return { response, videoObject: body }
+}
+
+async function refreshVideoIfNeeded (video: VideoModel): Promise<VideoModel> {
+  if (!video.isOutdated()) return video
+
+  try {
+    const { response, videoObject } = await fetchRemoteVideo(video.url)
+    if (response.statusCode === 404) {
+      // Video does not exist anymore
+      await video.destroy()
+      return undefined
     }
-  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 
-  logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
+    if (videoObject === undefined) {
+      logger.warn('Cannot refresh remote video: invalid body.')
+      return video
+    }
 
-  // This is "likes" and "dislikes"
-  if (rateCounts !== 0) await video.increment(rate + 's', { by: rateCounts })
+    const channelActor = await getOrCreateVideoChannel(videoObject)
+    const account = await AccountModel.load(channelActor.VideoChannel.accountId)
+    return updateVideoFromAP(video, videoObject, account.Actor, channelActor)
 
-  return
+  } catch (err) {
+    logger.warn('Cannot refresh video.', { err })
+    return video
+  }
 }
 
-async function addVideoShares (shareUrls: string[], instance: VideoModel) {
-  await Bluebird.map(shareUrls, async shareUrl => {
-    try {
-      // Fetch url
-      const { body } = await doRequest({
-        uri: shareUrl,
-        json: true,
-        activityPub: true
-      })
-      if (!body || !body.actor) throw new Error('Body of body actor is invalid')
+async function updateVideoFromAP (
+  video: VideoModel,
+  videoObject: VideoTorrentObject,
+  accountActor: ActorModel,
+  channelActor: ActorModel,
+  overrideTo?: string[]
+) {
+  logger.debug('Updating remote video "%s".', videoObject.uuid)
+  let videoFieldsSave: any
+
+  try {
+    const updatedVideo: VideoModel = await sequelizeTypescript.transaction(async t => {
+      const sequelizeOptions = {
+        transaction: t
+      }
 
-      const actorUrl = body.actor
-      const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+      videoFieldsSave = video.toJSON()
 
-      const entry = {
-        actorId: actor.id,
-        videoId: instance.id,
-        url: shareUrl
+      // Check actor has the right to update the video
+      const videoChannel = video.VideoChannel
+      if (videoChannel.Account.Actor.id !== accountActor.id) {
+        throw new Error('Account ' + accountActor.url + ' does not own video channel ' + videoChannel.Actor.url)
       }
 
-      await VideoShareModel.findOrCreate({
-        where: {
-          url: shareUrl
-        },
-        defaults: entry
-      })
-    } catch (err) {
-      logger.warn('Cannot add share %s.', shareUrl, { err })
-    }
-  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
-}
+      const to = overrideTo ? overrideTo : videoObject.to
+      const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, to)
+      video.set('name', videoData.name)
+      video.set('uuid', videoData.uuid)
+      video.set('url', videoData.url)
+      video.set('category', videoData.category)
+      video.set('licence', videoData.licence)
+      video.set('language', videoData.language)
+      video.set('description', videoData.description)
+      video.set('support', videoData.support)
+      video.set('nsfw', videoData.nsfw)
+      video.set('commentsEnabled', videoData.commentsEnabled)
+      video.set('waitTranscoding', videoData.waitTranscoding)
+      video.set('state', videoData.state)
+      video.set('duration', videoData.duration)
+      video.set('createdAt', videoData.createdAt)
+      video.set('publishedAt', videoData.publishedAt)
+      video.set('views', videoData.views)
+      video.set('privacy', videoData.privacy)
+      video.set('channelId', videoData.channelId)
+
+      await video.save(sequelizeOptions)
+
+      // Don't block on request
+      generateThumbnailFromUrl(video, videoObject.icon)
+        .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
+
+      // Remove old video files
+      const videoFileDestroyTasks: Bluebird<void>[] = []
+      for (const videoFile of video.VideoFiles) {
+        videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
+      }
+      await Promise.all(videoFileDestroyTasks)
 
-async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
-  const options = {
-    uri: videoUrl,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
+      const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject)
+      const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, sequelizeOptions))
+      await Promise.all(tasks)
 
-  logger.info('Fetching remote video %s.', videoUrl)
+      // Update Tags
+      const tags = videoObject.tag.map(tag => tag.name)
+      const tagInstances = await TagModel.findOrCreateTags(tags, t)
+      await video.$set('Tags', tagInstances, sequelizeOptions)
 
-  const { body } = await doRequest(options)
+      // Update captions
+      await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
 
-  if (sanitizeAndCheckVideoTorrentObject(body) === false) {
-    logger.debug('Remote video JSON is not valid.', { body })
-    return undefined
-  }
+      const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
+        return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
+      })
+      await Promise.all(videoCaptionsPromises)
+    })
+
+    logger.info('Remote video with uuid %s updated', videoObject.uuid)
 
-  return body
+    return updatedVideo
+  } catch (err) {
+    if (video !== undefined && videoFieldsSave !== undefined) {
+      resetSequelizeInstance(video, videoFieldsSave)
+    }
+
+    // This is just a debug because we will retry the insert
+    logger.debug('Cannot update the remote video.', { err })
+    throw err
+  }
 }
 
 export {
+  updateVideoFromAP,
   federateVideoIfNeeded,
   fetchRemoteVideo,
-  getOrCreateAccountAndVideoAndChannel,
+  getOrCreateVideoAndAccountAndChannel,
   fetchRemoteVideoStaticFile,
   fetchRemoteVideoDescription,
   generateThumbnailFromUrl,
   videoActivityObjectToDBAttributes,
   videoFileActivityUrlToDBAttributes,
-  getOrCreateVideo,
+  createVideo,
   getOrCreateVideoChannel,
   addVideoShares,
   createRates
index 25a1cd177dbeb2ff00d9b54ae1452ac91cec0ec1..7acbc60f7cd19d06c2572f91134345ddea89aa7e 100644 (file)
@@ -56,6 +56,7 @@ import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, tr
 import { logger } from '../../helpers/logger'
 import { getServerActor } from '../../helpers/utils'
 import {
+  ACTIVITY_PUB,
   API_VERSION,
   CONFIG,
   CONSTRAINTS_FIELDS,
@@ -1004,21 +1005,6 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
   }
 
-  static loadByUUIDOrURLAndPopulateAccount (uuid: string, url: string, t?: Sequelize.Transaction) {
-    const query: IFindOptions<VideoModel> = {
-      where: {
-        [Sequelize.Op.or]: [
-          { uuid },
-          { url }
-        ]
-      }
-    }
-
-    if (t !== undefined) query.transaction = t
-
-    return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
-  }
-
   static loadAndPopulateAccountAndServerAndTags (id: number) {
     const options = {
       order: [ [ 'Tags', 'name', 'ASC' ] ]
@@ -1646,6 +1632,17 @@ export class VideoModel extends Model<VideoModel> {
     return 'PT' + this.duration + 'S'
   }
 
+  isOutdated () {
+    if (this.isOwned()) return false
+
+    const now = Date.now()
+    const createdAtTime = this.createdAt.getTime()
+    const updatedAtTime = this.updatedAt.getTime()
+
+    return (now - createdAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL &&
+      (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
+  }
+
   private getBaseUrls () {
     let baseUrlHttp
     let baseUrlWs
diff --git a/server/tests/api/index-1.ts b/server/tests/api/index-1.ts
new file mode 100644 (file)
index 0000000..80d752f
--- /dev/null
@@ -0,0 +1,2 @@
+import './check-params'
+import './search'
diff --git a/server/tests/api/index-2.ts b/server/tests/api/index-2.ts
new file mode 100644 (file)
index 0000000..ed93faa
--- /dev/null
@@ -0,0 +1,2 @@
+import './server'
+import './users'
diff --git a/server/tests/api/index-3.ts b/server/tests/api/index-3.ts
new file mode 100644 (file)
index 0000000..39823b8
--- /dev/null
@@ -0,0 +1 @@
+import './videos'
diff --git a/server/tests/api/index-fast.ts b/server/tests/api/index-fast.ts
deleted file mode 100644 (file)
index 02ffdd4..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-// Order of the tests we want to execute
-import './server/stats'
-import './check-params'
-import './users/users'
-import './videos/single-server'
-import './videos/video-abuse'
-import './videos/video-captions'
-import './videos/video-blacklist'
-import './videos/video-blacklist-management'
-import './videos/video-description'
-import './videos/video-nsfw'
-import './videos/video-privacy'
-import './videos/services'
-import './server/email'
-import './server/config'
-import './server/reverse-proxy'
-import './search/search-videos'
-import './server/tracker'
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts
deleted file mode 100644 (file)
index e24a7b6..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-// Order of the tests we want to execute
-import './videos/video-channels'
-import './videos/video-transcoder'
-import './videos/multiple-servers'
-import './server/follows'
-import './server/jobs'
-import './videos/video-comments'
-import './users/users-multiple-servers'
-import './users/user-subscriptions'
-import './server/handle-down'
-import './videos/video-schedule-update'
-import './videos/video-imports'
index 258502d26067d2777feb34507fd80ee9c6096c87..2d996dbf952c1e2a4201ddfc2ef384e809e2ca2f 100644 (file)
@@ -1,3 +1,4 @@
 // Order of the tests we want to execute
-import './index-fast'
-import './index-slow'
+import './index-1'
+import './index-2'
+import './index-3'
diff --git a/server/tests/api/search/index.ts b/server/tests/api/search/index.ts
new file mode 100644 (file)
index 0000000..64b3d09
--- /dev/null
@@ -0,0 +1,2 @@
+import './search-activitypub-videos'
+import './search-videos'
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts
new file mode 100644 (file)
index 0000000..6dc7926
--- /dev/null
@@ -0,0 +1,161 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+  addVideoChannel,
+  flushAndRunMultipleServers,
+  flushTests,
+  getVideosList,
+  killallServers,
+  removeVideo,
+  searchVideoWithToken,
+  ServerInfo,
+  setAccessTokensToServers,
+  updateVideo,
+  uploadVideo,
+  wait,
+  searchVideo
+} from '../../utils'
+import { waitJobs } from '../../utils/server/jobs'
+import { Video, VideoPrivacy } from '../../../../shared/models/videos'
+
+const expect = chai.expect
+
+describe('Test a ActivityPub videos search', function () {
+  let servers: ServerInfo[]
+  let videoServer1UUID: string
+  let videoServer2UUID: string
+
+  before(async function () {
+    this.timeout(120000)
+
+    await flushTests()
+
+    servers = await flushAndRunMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+
+    {
+      const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1 on server 1' })
+      videoServer1UUID = res.body.video.uuid
+    }
+
+    {
+      const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 on server 2' })
+      videoServer2UUID = res.body.video.uuid
+    }
+
+    await waitJobs(servers)
+  })
+
+  it('Should not find a remote video', async function () {
+    {
+      const res = await searchVideoWithToken(servers[ 0 ].url, 'http://localhost:9002/videos/watch/43', servers[ 0 ].accessToken)
+
+      expect(res.body.total).to.equal(0)
+      expect(res.body.data).to.be.an('array')
+      expect(res.body.data).to.have.lengthOf(0)
+    }
+
+    {
+      const res = await searchVideo(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID)
+
+      expect(res.body.total).to.equal(0)
+      expect(res.body.data).to.be.an('array')
+      expect(res.body.data).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should search a local video', async function () {
+    const res = await searchVideo(servers[0].url, 'http://localhost:9001/videos/watch/' + videoServer1UUID)
+
+    expect(res.body.total).to.equal(1)
+    expect(res.body.data).to.be.an('array')
+    expect(res.body.data).to.have.lengthOf(1)
+    expect(res.body.data[0].name).to.equal('video 1 on server 1')
+  })
+
+  it('Should search a remote video', async function () {
+    const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
+
+    expect(res.body.total).to.equal(1)
+    expect(res.body.data).to.be.an('array')
+    expect(res.body.data).to.have.lengthOf(1)
+    expect(res.body.data[0].name).to.equal('video 1 on server 2')
+  })
+
+  it('Should not list this remote video', async function () {
+    const res = await getVideosList(servers[0].url)
+    expect(res.body.total).to.equal(1)
+    expect(res.body.data).to.have.lengthOf(1)
+    expect(res.body.data[0].name).to.equal('video 1 on server 1')
+  })
+
+  it('Should update video of server 2, and refresh it on server 1', async function () {
+    this.timeout(60000)
+
+    const channelAttributes = {
+      name: 'super_channel',
+      displayName: 'super channel'
+    }
+    const resChannel = await addVideoChannel(servers[1].url, servers[1].accessToken, channelAttributes)
+    const videoChannelId = resChannel.body.videoChannel.id
+
+    const attributes = {
+      name: 'updated',
+      tag: [ 'tag1', 'tag2' ],
+      privacy: VideoPrivacy.UNLISTED,
+      channelId: videoChannelId
+    }
+    await updateVideo(servers[1].url, servers[1].accessToken, videoServer2UUID, attributes)
+
+    await waitJobs(servers)
+    // Expire video
+    await wait(10000)
+
+    // Will run refresh async
+    await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
+
+    // Wait refresh
+    await wait(5000)
+
+    const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
+    expect(res.body.total).to.equal(1)
+    expect(res.body.data).to.have.lengthOf(1)
+
+    const video: Video = res.body.data[0]
+    expect(video.name).to.equal('updated')
+    expect(video.channel.name).to.equal('super_channel')
+    expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
+  })
+
+  it('Should delete video of server 2, and delete it on server 1', async function () {
+    this.timeout(60000)
+
+    await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID)
+
+    await waitJobs(servers)
+    // Expire video
+    await wait(10000)
+
+    // Will run refresh async
+    await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
+
+    // Wait refresh
+    await wait(5000)
+
+    const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
+    expect(res.body.total).to.equal(0)
+    expect(res.body.data).to.have.lengthOf(0)
+  })
+
+  after(async function () {
+    killallServers(servers)
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
new file mode 100644 (file)
index 0000000..eeb8b7a
--- /dev/null
@@ -0,0 +1,8 @@
+import './config'
+import './email'
+import './follows'
+import './handle-down'
+import './jobs'
+import './reverse-proxy'
+import './stats'
+import './tracker'
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
new file mode 100644 (file)
index 0000000..4ce87fb
--- /dev/null
@@ -0,0 +1,3 @@
+import './user-subscriptions'
+import './users'
+import './users-multiple-servers'
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
new file mode 100644 (file)
index 0000000..9f12307
--- /dev/null
@@ -0,0 +1,15 @@
+import './multiple-servers'
+import './services'
+import './single-server'
+import './video-abuse'
+import './video-blacklist'
+import './video-blacklist-management'
+import './video-captions'
+import './video-channels'
+import './video-comme'
+import './video-description'
+import './video-impo'
+import './video-nsfw'
+import './video-privacy'
+import './video-schedule-update'
+import './video-transcoder'
diff --git a/server/tests/feeds/index.ts b/server/tests/feeds/index.ts
new file mode 100644 (file)
index 0000000..aa6236a
--- /dev/null
@@ -0,0 +1 @@
+import './feeds'
index 755fb26046e1263049aff1b1148c4803c4246a4c..e659fd3df9bb4998241257771d1e4f52d108f4db 100644 (file)
@@ -1,5 +1,6 @@
 // Order of the tests we want to execute
 import './client'
 import './activitypub'
-import './api/'
+import './feeds/'
 import './cli/'
+import './api/'