aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts38
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/ffmpeg-utils.ts2
-rw-r--r--server/lib/activitypub/videos.ts7
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts12
-rw-r--r--server/lib/live-manager.ts8
-rw-r--r--server/lib/video-paths.ts4
-rw-r--r--server/lib/video-transcoding.ts17
-rw-r--r--server/lib/video.ts2
-rw-r--r--server/models/video/video-file.ts4
-rw-r--r--server/tests/api/live/live.ts24
11 files changed, 57 insertions, 63 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index df2a01d2c..d85d0aa5f 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -1,11 +1,22 @@
1import * as express from 'express'
2import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express'
3import { getRateUrl } from '@server/lib/activitypub/video-rates'
4import { getServerActor } from '@server/models/application/application'
5import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 6import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
7import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 8import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' 9import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants'
6import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send'
7import { audiencify, getAudience } from '../../lib/activitypub/audience' 10import { audiencify, getAudience } from '../../lib/activitypub/audience'
11import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send'
8import { buildCreateActivity } from '../../lib/activitypub/send/send-create' 12import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
13import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
14import {
15 getVideoCommentsActivityPubUrl,
16 getVideoDislikesActivityPubUrl,
17 getVideoLikesActivityPubUrl,
18 getVideoSharesActivityPubUrl
19} from '../../lib/activitypub/url'
9import { 20import {
10 asyncMiddleware, 21 asyncMiddleware,
11 executeIfActivityPub, 22 executeIfActivityPub,
@@ -14,30 +25,19 @@ import {
14 videosCustomGetValidator, 25 videosCustomGetValidator,
15 videosShareValidator 26 videosShareValidator
16} from '../../middlewares' 27} from '../../middlewares'
28import { cacheRoute } from '../../middlewares/cache'
17import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators' 29import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators'
30import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
31import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
18import { AccountModel } from '../../models/account/account' 32import { AccountModel } from '../../models/account/account'
33import { AccountVideoRateModel } from '../../models/account/account-video-rate'
19import { ActorFollowModel } from '../../models/activitypub/actor-follow' 34import { ActorFollowModel } from '../../models/activitypub/actor-follow'
20import { VideoModel } from '../../models/video/video' 35import { VideoModel } from '../../models/video/video'
36import { VideoCaptionModel } from '../../models/video/video-caption'
21import { VideoCommentModel } from '../../models/video/video-comment' 37import { VideoCommentModel } from '../../models/video/video-comment'
38import { VideoPlaylistModel } from '../../models/video/video-playlist'
22import { VideoShareModel } from '../../models/video/video-share' 39import { VideoShareModel } from '../../models/video/video-share'
23import { cacheRoute } from '../../middlewares/cache'
24import { activityPubResponse } from './utils' 40import { activityPubResponse } from './utils'
25import { AccountVideoRateModel } from '../../models/account/account-video-rate'
26import {
27 getVideoCommentsActivityPubUrl,
28 getVideoDislikesActivityPubUrl,
29 getVideoLikesActivityPubUrl,
30 getVideoSharesActivityPubUrl
31} from '../../lib/activitypub/url'
32import { VideoCaptionModel } from '../../models/video/video-caption'
33import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
34import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
35import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
36import { VideoPlaylistModel } from '../../models/video/video-playlist'
37import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
38import { MAccountId, MActorId, MVideoAPWithoutCaption, MVideoId, MChannelId } from '@server/types/models'
39import { getServerActor } from '@server/models/application/application'
40import { getRateUrl } from '@server/lib/activitypub/video-rates'
41 41
42const activityPubClientRouter = express.Router() 42const activityPubClientRouter = express.Router()
43activityPubClientRouter.use(cors()) 43activityPubClientRouter.use(cors())
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 50e769e77..ff29e584b 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -9,7 +9,7 @@ import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
9import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 9import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
10import { getVideoFilePath } from '@server/lib/video-paths' 10import { getVideoFilePath } from '@server/lib/video-paths'
11import { getServerActor } from '@server/models/application/application' 11import { getServerActor } from '@server/models/application/application'
12import { MVideoDetails, MVideoFullLight } from '@server/types/models' 12import { MVideoFullLight } from '@server/types/models'
13import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' 13import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared'
14import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 14import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
15import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 15import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 7a54b4642..b6f4097aa 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,5 +1,5 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { outputFile, readFile, remove, writeFile } from 'fs-extra' 2import { readFile, remove, writeFile } from 'fs-extra'
3import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' 4import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
5import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos' 5import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index ab4aac0a1..4053f487c 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,10 +1,10 @@
1import { VideoLiveModel } from '@server/models/video/video-live'
2import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
3import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
4import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
5import { join } from 'path' 4import { join } from 'path'
6import * as request from 'request' 5import * as request from 'request'
7import * as sequelize from 'sequelize' 6import * as sequelize from 'sequelize'
7import { VideoLiveModel } from '@server/models/video/video-live'
8import { 8import {
9 ActivityHashTagObject, 9 ActivityHashTagObject,
10 ActivityMagnetUrlObject, 10 ActivityMagnetUrlObject,
@@ -13,8 +13,7 @@ import {
13 ActivitypubHttpFetcherPayload, 13 ActivitypubHttpFetcherPayload,
14 ActivityTagObject, 14 ActivityTagObject,
15 ActivityUrlObject, 15 ActivityUrlObject,
16 ActivityVideoUrlObject, 16 ActivityVideoUrlObject
17 VideoState
18} from '../../../shared/index' 17} from '../../../shared/index'
19import { VideoObject } from '../../../shared/models/activitypub/objects' 18import { VideoObject } from '../../../shared/models/activitypub/objects'
20import { VideoPrivacy } from '../../../shared/models/videos' 19import { VideoPrivacy } from '../../../shared/models/videos'
@@ -562,8 +561,6 @@ function isAPHashTagObject (url: any): url is ActivityHashTagObject {
562 return url && url.type === 'Hashtag' 561 return url && url.type === 'Hashtag'
563} 562}
564 563
565
566
567async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { 564async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) {
568 logger.debug('Adding remote video %s.', videoObject.id) 565 logger.debug('Adding remote video %s.', videoObject.id)
569 566
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 3892260c4..3d9341738 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -10,8 +10,9 @@ import { VideoFileModel } from '@server/models/video/video-file'
10import { VideoLiveModel } from '@server/models/video/video-live' 10import { VideoLiveModel } from '@server/models/video/video-live'
11import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 11import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
12import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 12import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
13import { VideoLiveEndingPayload, VideoState } from '@shared/models' 13import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
14import { logger } from '../../../helpers/logger' 14import { logger } from '../../../helpers/logger'
15import { generateVideoMiniature } from '@server/lib/thumbnail'
15 16
16async function processVideoLiveEnding (job: Bull.Job) { 17async function processVideoLiveEnding (job: Bull.Job) {
17 const payload = job.data as VideoLiveEndingPayload 18 const payload = job.data as VideoLiveEndingPayload
@@ -109,6 +110,15 @@ async function saveLive (video: MVideo, live: MVideoLive) {
109 await remove(videoInputPath) 110 await remove(videoInputPath)
110 } 111 }
111 112
113 // Regenerate the thumbnail & preview?
114 if (videoWithFiles.getMiniature().automaticallyGenerated === true) {
115 await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.MINIATURE)
116 }
117
118 if (videoWithFiles.getPreview().automaticallyGenerated === true) {
119 await generateVideoMiniature(videoWithFiles, videoWithFiles.getMaxQualityFile(), ThumbnailType.PREVIEW)
120 }
121
112 await publishAndFederateIfNeeded(video, true) 122 await publishAndFederateIfNeeded(video, true)
113} 123}
114 124
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index d253d06fc..9a2914cc5 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -4,7 +4,13 @@ import * as chokidar from 'chokidar'
4import { FfmpegCommand } from 'fluent-ffmpeg' 4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { ensureDir, stat } from 'fs-extra' 5import { ensureDir, stat } from 'fs-extra'
6import { basename } from 'path' 6import { basename } from 'path'
7import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution, getVideoStreamCodec, getVideoStreamSize, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' 7import {
8 computeResolutionsToTranscode,
9 getVideoFileFPS,
10 getVideoFileResolution,
11 runLiveMuxing,
12 runLiveTranscoding
13} from '@server/helpers/ffmpeg-utils'
8import { logger } from '@server/helpers/logger' 14import { logger } from '@server/helpers/logger'
9import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' 15import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
10import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' 16import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants'
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts
index b6cb39d25..53fc8e81d 100644
--- a/server/lib/video-paths.ts
+++ b/server/lib/video-paths.ts
@@ -9,7 +9,7 @@ import { extractVideo } from '@server/helpers/video'
9function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 9function getVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
10 const video = extractVideo(videoOrPlaylist) 10 const video = extractVideo(videoOrPlaylist)
11 11
12 if (isStreamingPlaylist(videoOrPlaylist)) { 12 if (videoFile.isHLS()) {
13 return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution) 13 return generateVideoStreamingPlaylistName(video.uuid, videoFile.resolution)
14 } 14 }
15 15
@@ -25,7 +25,7 @@ function generateWebTorrentVideoName (uuid: string, resolution: number, extname:
25} 25}
26 26
27function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { 27function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) {
28 if (isStreamingPlaylist(videoOrPlaylist)) { 28 if (videoFile.isHLS()) {
29 const video = extractVideo(videoOrPlaylist) 29 const video = extractVideo(videoOrPlaylist)
30 30
31 return join(getHLSDirectory(video), getVideoFilename(videoOrPlaylist, videoFile)) 31 return join(getHLSDirectory(video), getVideoFilename(videoOrPlaylist, videoFile))
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index c62b3c1ce..e267b1397 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,5 +1,9 @@
1import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' 1import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
2import { basename, extname as extnameUtil, join } from 'path' 2import { basename, extname as extnameUtil, join } from 'path'
3import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
5import { VideoResolution } from '../../shared/models/videos'
6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
3import { 7import {
4 canDoQuickTranscode, 8 canDoQuickTranscode,
5 getDurationFromVideoFile, 9 getDurationFromVideoFile,
@@ -9,18 +13,13 @@ import {
9 TranscodeOptions, 13 TranscodeOptions,
10 TranscodeOptionsType 14 TranscodeOptionsType
11} from '../helpers/ffmpeg-utils' 15} from '../helpers/ffmpeg-utils'
12import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
13import { logger } from '../helpers/logger' 16import { logger } from '../helpers/logger'
14import { VideoResolution } from '../../shared/models/videos' 17import { CONFIG } from '../initializers/config'
18import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
15import { VideoFileModel } from '../models/video/video-file' 19import { VideoFileModel } from '../models/video/video-file'
16import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
17import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
18import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 21import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
19import { CONFIG } from '../initializers/config'
20import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
21import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
22import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths' 22import { generateVideoStreamingPlaylistName, getVideoFilename, getVideoFilePath } from './video-paths'
23import { spawn } from 'child_process'
24 23
25/** 24/**
26 * Optimize the original video file and replace it. The resolution is not changed. 25 * Optimize the original video file and replace it. The resolution is not changed.
diff --git a/server/lib/video.ts b/server/lib/video.ts
index 8d9918b2d..d03ab0452 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -4,7 +4,7 @@ import { TagModel } from '@server/models/video/tag'
4import { VideoModel } from '@server/models/video/video' 4import { VideoModel } from '@server/models/video/video'
5import { FilteredModelAttributes } from '@server/types' 5import { FilteredModelAttributes } from '@server/types'
6import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' 6import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
7import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' 7import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models'
8import { federateVideoIfNeeded } from './activitypub/videos' 8import { federateVideoIfNeeded } from './activitypub/videos'
9import { Notifier } from './notifier' 9import { Notifier } from './notifier'
10import { createVideoMiniatureFromExisting } from './thumbnail' 10import { createVideoMiniatureFromExisting } from './thumbnail'
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 5048cf9b7..0e834aee0 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -333,6 +333,10 @@ export class VideoFileModel extends Model<VideoFileModel> {
333 return this.size === -1 333 return this.size === -1
334 } 334 }
335 335
336 isHLS () {
337 return this.videoStreamingPlaylistId !== null
338 }
339
336 hasSameUniqueKeysThan (other: MVideoFile) { 340 hasSameUniqueKeysThan (other: MVideoFile) {
337 return this.fps === other.fps && 341 return this.fps === other.fps &&
338 this.resolution === other.resolution && 342 this.resolution === other.resolution &&
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index c795f201a..b41b5fc2e 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -3,18 +3,16 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io' 5import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
6import { LiveVideo, LiveVideoCreate, User, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' 6import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
7import { 7import {
8 addVideoToBlacklist, 8 addVideoToBlacklist,
9 checkLiveCleanup, 9 checkLiveCleanup,
10 checkResolutionsInMasterPlaylist, 10 checkResolutionsInMasterPlaylist,
11 cleanupTests, 11 cleanupTests,
12 createLive, 12 createLive,
13 createUser,
14 doubleFollow, 13 doubleFollow,
15 flushAndRunMultipleServers, 14 flushAndRunMultipleServers,
16 getLive, 15 getLive,
17 getMyUserInformation,
18 getVideo, 16 getVideo,
19 getVideoIdFromUUID, 17 getVideoIdFromUUID,
20 getVideosList, 18 getVideosList,
@@ -30,7 +28,6 @@ import {
30 testImage, 28 testImage,
31 updateCustomSubConfig, 29 updateCustomSubConfig,
32 updateLive, 30 updateLive,
33 userLogin,
34 waitJobs, 31 waitJobs,
35 waitUntilLiveStarts 32 waitUntilLiveStarts
36} from '../../../../shared/extra-utils' 33} from '../../../../shared/extra-utils'
@@ -39,9 +36,6 @@ const expect = chai.expect
39 36
40describe('Test live', function () { 37describe('Test live', function () {
41 let servers: ServerInfo[] = [] 38 let servers: ServerInfo[] = []
42 let userId: number
43 let userAccessToken: string
44 let userChannelId: number
45 39
46 before(async function () { 40 before(async function () {
47 this.timeout(120000) 41 this.timeout(120000)
@@ -62,22 +56,6 @@ describe('Test live', function () {
62 } 56 }
63 }) 57 })
64 58
65 {
66 const user = { username: 'user1', password: 'superpassword' }
67 const res = await createUser({
68 url: servers[0].url,
69 accessToken: servers[0].accessToken,
70 username: user.username,
71 password: user.password
72 })
73 userId = res.body.user.id
74
75 userAccessToken = await userLogin(servers[0], user)
76
77 const resMe = await getMyUserInformation(servers[0].url, userAccessToken)
78 userChannelId = (resMe.body as User).videoChannels[0].id
79 }
80
81 // Server 1 and server 2 follow each other 59 // Server 1 and server 2 follow each other
82 await doubleFollow(servers[0], servers[1]) 60 await doubleFollow(servers[0], servers[1])
83 }) 61 })