Add ability to search a video with an URL
authorChocobozzz <me@florianbigard.com>
Wed, 22 Aug 2018 09:51:39 +0000 (11:51 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 27 Aug 2018 07:41:54 +0000 (09:41 +0200)
19 files changed:
client/src/app/header/header.component.scss
client/src/app/shared/user-subscription/index.ts
client/src/app/shared/video-channel/video-channel.service.ts
server/controllers/api/search.ts
server/initializers/constants.ts
server/lib/activitypub/actor.ts
server/lib/activitypub/crawl.ts
server/lib/activitypub/process/process-announce.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/videos.ts
server/lib/job-queue/handlers/activitypub-http-fetcher.ts
server/tests/api/check-params/videos.ts
server/tests/api/server/follows.ts
server/tests/api/server/handle-down.ts
server/tests/api/users/user-subscriptions.ts
server/tests/api/videos/multiple-servers.ts
server/tests/api/videos/single-server.ts
server/tests/utils/videos/videos.ts

index d79e6274b28f5107788d33af4d44e92dd25d7332..6ce92fc22b9a0443fa1501a622878d7551dbcbe1 100644 (file)
@@ -4,7 +4,7 @@
 #search-video {
   @include peertube-input-text($search-input-width);
   margin-right: 15px;
-  padding-right: 25px; // For the search icon
+  padding-right: 40px; // For the search icon
 
   &::placeholder {
     color: #000;
index 024b36a41ad8b45b3dcf7e0256e9fb3de092b512..faddae66a0b28f256f008408c80f74528a68a7d3 100644 (file)
@@ -1,2 +1,2 @@
 export * from './user-subscription.service'
-export * from './subscribe-button.component'
\ No newline at end of file
+export * from './subscribe-button.component'
index 46b1217904d45ef3f7a89786f9f9891f4f180fa0..c94411146bdc898e0ff96332f66cbc76a380a439 100644 (file)
@@ -17,11 +17,6 @@ export class VideoChannelService {
 
   videoChannelLoaded = new ReplaySubject<VideoChannel>(1)
 
-  constructor (
-    private authHttp: HttpClient,
-    private restExtractor: RestExtractor
-  ) {}
-
   static extractVideoChannels (result: ResultList<VideoChannelServer>) {
     const videoChannels: VideoChannel[] = []
 
@@ -32,6 +27,11 @@ export class VideoChannelService {
     return { data: videoChannels, total: result.total }
   }
 
+  constructor (
+    private authHttp: HttpClient,
+    private restExtractor: RestExtractor
+  ) { }
+
   getVideoChannel (videoChannelName: string) {
     return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
                .pipe(
index 7a7504b7d5c0e58987eeee64d7bd6d54cd2c26bf..9c2c7d6c1f3b44b6088382643c7ae82e9bf06df4 100644 (file)
@@ -13,6 +13,8 @@ import {
   videosSearchSortValidator
 } from '../../middlewares'
 import { VideosSearchQuery } from '../../../shared/models/search'
+import { getOrCreateAccountAndVideoAndChannel } from '../../lib/activitypub'
+import { logger } from '../../helpers/logger'
 
 const searchRouter = express.Router()
 
@@ -33,9 +35,16 @@ export { searchRouter }
 
 // ---------------------------------------------------------------------------
 
-async function searchVideos (req: express.Request, res: express.Response) {
+function searchVideos (req: express.Request, res: express.Response) {
   const query: VideosSearchQuery = req.query
+  if (query.search.startsWith('http://') || query.search.startsWith('https://')) {
+    return searchVideoUrl(query.search, res)
+  }
 
+  return searchVideosDB(query, res)
+}
+
+async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
   const options = Object.assign(query, {
     includeLocalVideos: true,
     nsfw: buildNSFWFilter(res, query.nsfw)
@@ -44,3 +53,27 @@ async function searchVideos (req: express.Request, res: express.Response) {
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
+
+async function searchVideoUrl (url: string, res: express.Response) {
+  let video: VideoModel
+
+  try {
+    const syncParam = {
+      likes: false,
+      dislikes: false,
+      shares: false,
+      comments: false,
+      thumbnail: true
+    }
+
+    const res = await getOrCreateAccountAndVideoAndChannel(url, syncParam)
+    video = res ? res.video : undefined
+  } catch (err) {
+    logger.info('Cannot search remote video %s.', url)
+  }
+
+  return res.json({
+    total: video ? 1 : 0,
+    data: video ? [ video.toFormattedJSON() ] : []
+  })
+}
index 7e865fe3bb4287cee489dd1c5b789492c2f79521..99b10a7fc90156a28f47f2e2052fe0a76ff6d62c 100644 (file)
@@ -112,6 +112,7 @@ const JOB_TTL: { [ id in JobType ]: number } = {
   'email': 60000 * 10 // 10 minutes
 }
 const BROADCAST_CONCURRENCY = 10 // How many requests in parallel we do in activitypub-http-broadcast job
+const CRAWL_REQUEST_CONCURRENCY = 5 // How many requests in parallel to fetch remote data (likes, shares...)
 const JOB_REQUEST_TIMEOUT = 3000 // 3 seconds
 const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
 
@@ -643,6 +644,7 @@ export {
   STATIC_DOWNLOAD_PATHS,
   RATES_LIMIT,
   VIDEO_EXT_MIMETYPE,
+  CRAWL_REQUEST_CONCURRENCY,
   JOB_COMPLETED_LIFETIME,
   VIDEO_IMPORT_STATES,
   VIDEO_VIEW_LIFETIME,
index d84b465b2afe045d671a2e2b6ae5ecee5df3c9c2..9922229d2b26e38ea010a2b73c68c22777be1910 100644 (file)
@@ -177,7 +177,8 @@ async function addFetchOutboxJob (actor: ActorModel) {
   }
 
   const payload = {
-    uris: [ actor.outboxUrl ]
+    uri: actor.outboxUrl,
+    type: 'activity' as 'activity'
   }
 
   return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
@@ -248,6 +249,7 @@ function saveActorAndServerAndModelIfNotExist (
     } else if (actorCreated.type === 'Group') { // Video channel
       actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
       actorCreated.VideoChannel.Actor = actorCreated
+      actorCreated.VideoChannel.Account = ownerActor.Account
     }
 
     return actorCreated
index d4fc786f71b973d69965981954d88342ad12c90e..55912341cb82aaba55c43fa27ad8d502d2771543 100644 (file)
@@ -1,8 +1,9 @@
 import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
 import { doRequest } from '../../helpers/requests'
 import { logger } from '../../helpers/logger'
+import Bluebird = require('bluebird')
 
-async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any>) {
+async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
   logger.info('Crawling ActivityPub data on %s.', uri)
 
   const options = {
index d8ca594259b6d0bff65cef7114dd0c9e5b747f3a..b08156aa165ad7cdd67d6b0d633c7fe8a12dca65 100644 (file)
@@ -24,10 +24,8 @@ export {
 
 async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
   const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
-  let video: VideoModel
 
-  const res = await getOrCreateAccountAndVideoAndChannel(objectUri)
-  video = res.video
+  const { video } = await getOrCreateAccountAndVideoAndChannel(objectUri)
 
   return sequelizeTypescript.transaction(async t => {
     // Add share entry
index 791148919f2958c2e1a6e9528543cbb478d2962d..9655d015ffd5abea095ed47cc9dd150e4abcc3af 100644 (file)
@@ -23,7 +23,7 @@ async function processCreateActivity (activity: ActivityCreate) {
   } else if (activityType === 'Dislike') {
     return retryTransactionWrapper(processCreateDislike, actor, activity)
   } else if (activityType === 'Video') {
-    return processCreateVideo(actor, activity)
+    return processCreateVideo(activity)
   } else if (activityType === 'Flag') {
     return retryTransactionWrapper(processCreateVideoAbuse, actor, activityObject as VideoAbuseObject)
   } else if (activityType === 'Note') {
@@ -42,13 +42,10 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processCreateVideo (
-  actor: ActorModel,
-  activity: ActivityCreate
-) {
+async function processCreateVideo (activity: ActivityCreate) {
   const videoToCreateData = activity.object as VideoTorrentObject
 
-  const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData, actor)
+  const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData)
 
   return video
 }
index fd03710c2c4aea8aa8584f539886007bdfb14416..14c7fde69150b6eda3afdd589c25a9e3cf015117 100644 (file)
@@ -2,12 +2,13 @@ import { VideoCommentObject } from '../../../shared/models/activitypub/objects/v
 import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
 import { logger } from '../../helpers/logger'
 import { doRequest } from '../../helpers/requests'
-import { ACTIVITY_PUB } from '../../initializers'
+import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
 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 * as Bluebird from 'bluebird'
 
 async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
   let originCommentId: number = null
@@ -38,9 +39,9 @@ async function videoCommentActivityObjectToDBAttributes (video: VideoModel, acto
 }
 
 async function addVideoComments (commentUrls: string[], instance: VideoModel) {
-  for (const commentUrl of commentUrls) {
-    await addVideoComment(instance, commentUrl)
-  }
+  return Bluebird.map(commentUrls, commentUrl => {
+    return addVideoComment(instance, commentUrl)
+  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 }
 
 async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
index d1888556c0df182cc7c5d8c571533834acca2921..fac1d3fc74b67d68e604a318784a51362a8b38f8 100644 (file)
@@ -11,7 +11,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos
 import { retryTransactionWrapper } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
+import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
 import { AccountVideoRateModel } from '../../models/account/account-video-rate'
 import { ActorModel } from '../../models/activitypub/actor'
 import { TagModel } from '../../models/video/tag'
@@ -26,6 +26,8 @@ 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'
 
 async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   // If the video is not private and published, we federate it
@@ -178,10 +180,10 @@ function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
   return getOrCreateActorAndServerAndModel(channel.id)
 }
 
-async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) {
+async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
   logger.debug('Adding remote video %s.', videoObject.id)
 
-  return sequelizeTypescript.transaction(async t => {
+  const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
     const sequelizeOptions = {
       transaction: t
     }
@@ -191,10 +193,6 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
     const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
     const video = VideoModel.build(videoData)
 
-    // Don't block on remote HTTP request (we are in a transaction!)
-    generateThumbnailFromUrl(video, videoObject.icon)
-      .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
-
     const videoCreated = await video.save(sequelizeOptions)
 
     // Process files
@@ -222,68 +220,100 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
     videoCreated.VideoChannel = channelActor.VideoChannel
     return videoCreated
   })
+
+  const p = generateThumbnailFromUrl(videoCreated, videoObject.icon)
+    .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
+
+  if (waitThumbnail === true) await p
+
+  return videoCreated
 }
 
-async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
+type SyncParam = {
+  likes: boolean,
+  dislikes: boolean,
+  shares: boolean,
+  comments: boolean,
+  thumbnail: boolean
+}
+async function getOrCreateAccountAndVideoAndChannel (
+  videoObject: VideoTorrentObject | string,
+  syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
+) {
   const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
 
   const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
-  if (videoFromDatabase) {
-    return {
-      video: videoFromDatabase,
-      actor: videoFromDatabase.VideoChannel.Account.Actor,
-      channelActor: videoFromDatabase.VideoChannel.Actor
-    }
-  }
+  if (videoFromDatabase) return { video: videoFromDatabase }
 
-  videoObject = await fetchRemoteVideo(videoUrl)
-  if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+  const fetchedVideo = await fetchRemoteVideo(videoUrl)
+  if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
 
-  if (!actor) {
-    const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
-    if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
+  const channelActor = await getOrCreateVideoChannel(fetchedVideo)
+  const video = await retryTransactionWrapper(getOrCreateVideo, fetchedVideo, channelActor, syncParam.thumbnail)
 
-    actor = await getOrCreateActorAndServerAndModel(actorObj.id)
-  }
+  // Process outside the transaction because we could fetch remote data
 
-  const channelActor = await getOrCreateVideoChannel(videoObject)
+  logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
 
-  const video = await retryTransactionWrapper(getOrCreateVideo, videoObject, channelActor)
+  const jobPayloads: ActivitypubHttpFetcherPayload[] = []
 
-  // Process outside the transaction because we could fetch remote data
-  logger.info('Adding likes of video %s.', video.uuid)
-  await crawlCollectionPage<string>(videoObject.likes, (items) => createRates(items, video, 'like'))
+  if (syncParam.likes === true) {
+    await crawlCollectionPage<string>(fetchedVideo.likes, items => createRates(items, video, 'like'))
+      .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
+  } else {
+    jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
+  }
 
-  logger.info('Adding dislikes of video %s.', video.uuid)
-  await crawlCollectionPage<string>(videoObject.dislikes, (items) => createRates(items, video, 'dislike'))
+  if (syncParam.dislikes === true) {
+    await crawlCollectionPage<string>(fetchedVideo.dislikes, items => createRates(items, video, 'dislike'))
+      .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
+  } else {
+    jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
+  }
+
+  if (syncParam.shares === true) {
+    await crawlCollectionPage<string>(fetchedVideo.shares, items => addVideoShares(items, video))
+      .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
+  } else {
+    jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+  }
 
-  logger.info('Adding shares of video %s.', video.uuid)
-  await crawlCollectionPage<string>(videoObject.shares, (items) => addVideoShares(items, video))
+  if (syncParam.comments === true) {
+    await crawlCollectionPage<string>(fetchedVideo.comments, items => addVideoComments(items, video))
+      .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
+  } else {
+    jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+  }
 
-  logger.info('Adding comments of video %s.', video.uuid)
-  await crawlCollectionPage<string>(videoObject.comments, (items) => addVideoComments(items, video))
+  await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
 
-  return { actor, channelActor, video }
+  return { video }
 }
 
 async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
   let rateCounts = 0
-  const tasks: Bluebird<number>[] = []
-
-  for (const actorUrl of actorUrls) {
-    const actor = await getOrCreateActorAndServerAndModel(actorUrl)
-    const p = AccountVideoRateModel
-      .create({
-        videoId: video.id,
-        accountId: actor.Account.id,
-        type: rate
-      })
-      .then(() => rateCounts += 1)
-
-    tasks.push(p)
-  }
 
-  await Promise.all(tasks)
+  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)
 
@@ -294,34 +324,35 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
 }
 
 async function addVideoShares (shareUrls: string[], instance: VideoModel) {
-  for (const shareUrl of shareUrls) {
-    // Fetch url
-    const { body } = await doRequest({
-      uri: shareUrl,
-      json: true,
-      activityPub: true
-    })
-    if (!body || !body.actor) {
-      logger.warn('Cannot add remote share with url: %s, skipping...', shareUrl)
-      continue
-    }
-
-    const actorUrl = body.actor
-    const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+  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 entry = {
-      actorId: actor.id,
-      videoId: instance.id,
-      url: shareUrl
-    }
+      const actorUrl = body.actor
+      const actor = await getOrCreateActorAndServerAndModel(actorUrl)
 
-    await VideoShareModel.findOrCreate({
-      where: {
+      const entry = {
+        actorId: actor.id,
+        videoId: instance.id,
         url: shareUrl
-      },
-      defaults: entry
-    })
-  }
+      }
+
+      await VideoShareModel.findOrCreate({
+        where: {
+          url: shareUrl
+        },
+        defaults: entry
+      })
+    } catch (err) {
+      logger.warn('Cannot add share %s.', shareUrl, { err })
+    }
+  }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 }
 
 async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
@@ -355,5 +386,6 @@ export {
   videoFileActivityUrlToDBAttributes,
   getOrCreateVideo,
   getOrCreateVideoChannel,
-  addVideoShares
+  addVideoShares,
+  createRates
 }
index f21da087e9b80e127c4aac21eded92a1b4071a52..72d670277a3752c90276e736e74db4c9b9f7313a 100644 (file)
@@ -1,22 +1,36 @@
 import * as Bull from 'bull'
 import { logger } from '../../../helpers/logger'
 import { processActivities } from '../../activitypub/process'
-import { ActivitypubHttpBroadcastPayload } from './activitypub-http-broadcast'
+import { VideoModel } from '../../../models/video/video'
+import { addVideoShares, createRates } from '../../activitypub/videos'
+import { addVideoComments } from '../../activitypub/video-comments'
 import { crawlCollectionPage } from '../../activitypub/crawl'
-import { Activity } from '../../../../shared/models/activitypub'
+
+type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments'
 
 export type ActivitypubHttpFetcherPayload = {
-  uris: string[]
+  uri: string
+  type: FetchType
+  videoId?: number
 }
 
 async function processActivityPubHttpFetcher (job: Bull.Job) {
   logger.info('Processing ActivityPub fetcher in job %d.', job.id)
 
-  const payload = job.data as ActivitypubHttpBroadcastPayload
+  const payload = job.data as ActivitypubHttpFetcherPayload
+
+  let video: VideoModel
+  if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
 
-  for (const uri of payload.uris) {
-    await crawlCollectionPage<Activity>(uri, (items) => processActivities(items))
+  const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
+    'activity': items => processActivities(items),
+    'video-likes': items => createRates(items, video, 'like'),
+    'video-dislikes': items => createRates(items, video, 'dislike'),
+    'video-shares': items => addVideoShares(items, video),
+    'video-comments': items => addVideoComments(items, video)
   }
+
+  return crawlCollectionPage(payload.uri, fetcherType[payload.type])
 }
 
 // ---------------------------------------------------------------------------
index 7fce8ba7c71c81125c4f6b80817c751a6612062f..904d2287090b12dc3859c4779541d99b1d3ce80a 100644 (file)
@@ -20,7 +20,7 @@ describe('Test videos API validator', function () {
   let userAccessToken = ''
   let accountName: string
   let channelId: number
-  let channelUUID: string
+  let channelName: string
   let videoId
 
   // ---------------------------------------------------------------
@@ -42,7 +42,7 @@ describe('Test videos API validator', function () {
     {
       const res = await getMyUserInformation(server.url, server.accessToken)
       channelId = res.body.videoChannels[ 0 ].id
-      channelUUID = res.body.videoChannels[ 0 ].uuid
+      channelName = res.body.videoChannels[ 0 ].name
       accountName = res.body.account.name + '@' + res.body.account.host
     }
   })
@@ -140,7 +140,7 @@ describe('Test videos API validator', function () {
     let path: string
 
     before(async function () {
-      path = '/api/v1/video-channels/' + channelUUID + '/videos'
+      path = '/api/v1/video-channels/' + channelName + '/videos'
     })
 
     it('Should fail with a bad start pagination', async function () {
index 243fcd4e702136e99427b8479674e1af30659b47..310c291bf946e0f4e5015347ea470f68ce4c99bc 100644 (file)
@@ -311,7 +311,8 @@ describe('Test follows', function () {
         likes: 1,
         dislikes: 1,
         channel: {
-          name: 'Main root channel',
+          displayName: 'Main root channel',
+          name: 'root_channel',
           description: '',
           isLocal
         },
index df35b36ebb491793720ed03e2b879b372c495e6c..ed15c8090659ec0cc095e781bcefdfd17556b229 100644 (file)
@@ -71,7 +71,8 @@ describe('Test handle downs', function () {
     privacy: VideoPrivacy.PUBLIC,
     commentsEnabled: true,
     channel: {
-      name: 'Main root channel',
+      name: 'root_channel',
+      displayName: 'Main root channel',
       description: '',
       isLocal: false
     },
index 2fbda6828d1ca158e9240d7d00be16e491e4d592..cb7d94b0b9aff80a983cc93f83626c672945802e 100644 (file)
@@ -2,7 +2,7 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, userLogin } from '../../utils'
+import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, updateVideo, userLogin } from '../../utils'
 import { killallServers, ServerInfo, uploadVideo } from '../../utils/index'
 import { setAccessTokensToServers } from '../../utils/users/login'
 import { Video, VideoChannel } from '../../../../shared/models/videos'
@@ -20,6 +20,7 @@ const expect = chai.expect
 describe('Test users subscriptions', function () {
   let servers: ServerInfo[] = []
   const users: { accessToken: string }[] = []
+  let video3UUID: string
 
   before(async function () {
     this.timeout(120000)
@@ -65,7 +66,8 @@ describe('Test users subscriptions', function () {
 
     await waitJobs(servers)
 
-    await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
+    const res = await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
+    video3UUID = res.body.video.uuid
 
     await waitJobs(servers)
   })
@@ -247,7 +249,21 @@ describe('Test users subscriptions', function () {
     }
   })
 
+  it('Should update a video of server 3 and see the updated video on server 1', async function () {
+    this.timeout(30000)
+
+    await updateVideo(servers[2].url, users[2].accessToken, video3UUID, { name: 'video server 3 added after follow updated' })
+
+    await waitJobs(servers)
+
+    const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
+    const videos: Video[] = res.body.data
+    expect(videos[2].name).to.equal('video server 3 added after follow updated')
+  })
+
   it('Should remove user of server 3 subscription', async function () {
+    this.timeout(30000)
+
     await removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003')
 
     await waitJobs(servers)
@@ -267,6 +283,8 @@ describe('Test users subscriptions', function () {
   })
 
   it('Should remove the root subscription and not display the videos anymore', async function () {
+    this.timeout(30000)
+
     await removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:9001')
 
     await waitJobs(servers)
@@ -288,7 +306,7 @@ describe('Test users subscriptions', function () {
     for (const video of res.body.data) {
       expect(video.name).to.not.contain('1-3')
       expect(video.name).to.not.contain('2-3')
-      expect(video.name).to.not.contain('video server 3 added after follow')
+      expect(video.name).to.not.contain('video server 3 added after follow updated')
     }
   })
 
@@ -309,7 +327,7 @@ describe('Test users subscriptions', function () {
 
       expect(videos[0].name).to.equal('video 1-3')
       expect(videos[1].name).to.equal('video 2-3')
-      expect(videos[2].name).to.equal('video server 3 added after follow')
+      expect(videos[2].name).to.equal('video server 3 added after follow updated')
     }
 
     {
@@ -319,7 +337,7 @@ describe('Test users subscriptions', function () {
       for (const video of res.body.data) {
         expect(video.name).to.not.contain('1-3')
         expect(video.name).to.not.contain('2-3')
-        expect(video.name).to.not.contain('video server 3 added after follow')
+        expect(video.name).to.not.contain('video server 3 added after follow updated')
       }
     }
   })
index 3c383933864c2436195fd7bb07e11685dd93cca4..c551ccc597e853806ab331723957aa8079a78512 100644 (file)
@@ -128,7 +128,8 @@ describe('Test multiple servers', function () {
           privacy: VideoPrivacy.PUBLIC,
           commentsEnabled: true,
           channel: {
-            name: 'my channel',
+            displayName: 'my channel',
+            name: 'super_channel_name',
             description: 'super channel',
             isLocal
           },
@@ -201,7 +202,8 @@ describe('Test multiple servers', function () {
           tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
-            name: 'Main user1 channel',
+            displayName: 'Main user1 channel',
+            name: 'user1_channel',
             description: 'super channel',
             isLocal
           },
@@ -307,7 +309,8 @@ describe('Test multiple servers', function () {
           tags: [ 'tag1p3' ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
-            name: 'Main root channel',
+            displayName: 'Main root channel',
+            name: 'root_channel',
             description: '',
             isLocal
           },
@@ -339,7 +342,8 @@ describe('Test multiple servers', function () {
           tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
-            name: 'Main root channel',
+            displayName: 'Main root channel',
+            name: 'root_channel',
             description: '',
             isLocal
           },
@@ -647,7 +651,8 @@ describe('Test multiple servers', function () {
           tags: [ 'tag_up_1', 'tag_up_2' ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
-            name: 'Main root channel',
+            displayName: 'Main root channel',
+            name: 'root_channel',
             description: '',
             isLocal
           },
@@ -967,7 +972,8 @@ describe('Test multiple servers', function () {
           tags: [ ],
           privacy: VideoPrivacy.PUBLIC,
           channel: {
-            name: 'Main root channel',
+            displayName: 'Main root channel',
+            name: 'root_channel',
             description: '',
             isLocal
           },
index 12181ad671033992836844b4c9c1580e3f6dbc98..a757ad9da809fb19b913072b05e5941b0c9633d3 100644 (file)
@@ -56,7 +56,8 @@ describe('Test a single server', function () {
     privacy: VideoPrivacy.PUBLIC,
     commentsEnabled: true,
     channel: {
-      name: 'Main root channel',
+      displayName: 'Main root channel',
+      name: 'root_channel',
       description: '',
       isLocal: true
     },
@@ -87,7 +88,8 @@ describe('Test a single server', function () {
     duration: 5,
     commentsEnabled: false,
     channel: {
-      name: 'Main root channel',
+      name: 'root_channel',
+      displayName: 'Main root channel',
       description: '',
       isLocal: true
     },
index 59224814425f8221b7cd031bb0f6318b8e8d6cfa..674a92df93441bff816d67dad7774d192cd5d9ba 100644 (file)
@@ -438,18 +438,19 @@ async function completeVideoCheck (
       name: string
       host: string
     }
-    isLocal: boolean,
-    tags: string[],
-    privacy: number,
-    likes?: number,
-    dislikes?: number,
-    duration: number,
+    isLocal: boolean
+    tags: string[]
+    privacy: number
+    likes?: number
+    dislikes?: number
+    duration: number
     channel: {
-      name: string,
+      displayName: string
+      name: string
       description
       isLocal: boolean
     }
-    fixture: string,
+    fixture: string
     files: {
       resolution: number
       size: number
@@ -476,8 +477,8 @@ async function completeVideoCheck (
   expect(video.account.uuid).to.be.a('string')
   expect(video.account.host).to.equal(attributes.account.host)
   expect(video.account.name).to.equal(attributes.account.name)
-  expect(video.channel.displayName).to.equal(attributes.channel.name)
-  expect(video.channel.name).to.have.lengthOf(36)
+  expect(video.channel.displayName).to.equal(attributes.channel.displayName)
+  expect(video.channel.name).to.equal(attributes.channel.name)
   expect(video.likes).to.equal(attributes.likes)
   expect(video.dislikes).to.equal(attributes.dislikes)
   expect(video.isLocal).to.equal(attributes.isLocal)
@@ -497,8 +498,8 @@ async function completeVideoCheck (
   expect(videoDetails.tags).to.deep.equal(attributes.tags)
   expect(videoDetails.account.name).to.equal(attributes.account.name)
   expect(videoDetails.account.host).to.equal(attributes.account.host)
-  expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
-  expect(videoDetails.channel.name).to.have.lengthOf(36)
+  expect(video.channel.displayName).to.equal(attributes.channel.displayName)
+  expect(video.channel.name).to.equal(attributes.channel.name)
   expect(videoDetails.channel.host).to.equal(attributes.account.host)
   expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
   expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true