1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import * as chai from 'chai'
5 import { VideoDetails } from '../../../../shared/models/videos'
8 checkVideoFilesWereRemoved,
11 flushAndRunMultipleServers,
12 getFollowingListPaginationAndSort,
22 setAccessTokensToServers,
25 updateCustomSubConfig,
30 } from '../../../../shared/extra-utils'
31 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
33 import * as magnetUtil from 'magnet-uri'
36 listVideoRedundancies,
37 removeVideoRedundancy,
39 } from '../../../../shared/extra-utils/server/redundancy'
40 import { ActorFollow } from '../../../../shared/models/actors'
41 import { readdir } from 'fs-extra'
42 import { join } from 'path'
43 import { VideoRedundancy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../../shared/models/redundancy'
44 import { getStats } from '../../../../shared/extra-utils/server/stats'
45 import { ServerStats } from '../../../../shared/models/server/server-stats.model'
46 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
48 const expect = chai.expect
50 let servers: ServerInfo[] = []
51 let video1Server2UUID: string
52 let video1Server2Id: number
54 function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
55 const parsed = magnetUtil.decode(file.magnetUri)
57 for (const ws of baseWebseeds) {
58 const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
59 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
62 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
65 async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebtorrent = true) {
66 const strategies: any[] = []
68 if (strategy !== null) {
71 min_lifetime: '1 hour',
81 enabled: withWebtorrent
89 check_interval: '5 seconds',
95 servers = await flushAndRunMultipleServers(3, config)
97 // Get the access tokens
98 await setAccessTokensToServers(servers)
101 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
102 video1Server2UUID = res.body.video.uuid
103 video1Server2Id = res.body.video.id
105 await viewVideo(servers[1].url, video1Server2UUID)
108 await waitJobs(servers)
110 // Server 1 and server 2 follow each other
111 await doubleFollow(servers[0], servers[1])
112 // Server 1 and server 3 follow each other
113 await doubleFollow(servers[0], servers[2])
114 // Server 2 and server 3 follow each other
115 await doubleFollow(servers[1], servers[2])
117 await waitJobs(servers)
120 async function check1WebSeed (videoUUID?: string) {
121 if (!videoUUID) videoUUID = video1Server2UUID
124 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
127 for (const server of servers) {
128 // With token to avoid issues with video follow constraints
129 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
131 const video: VideoDetails = res.body
132 for (const f of video.files) {
133 checkMagnetWebseeds(f, webseeds, server)
138 async function check2Webseeds (videoUUID?: string) {
139 if (!videoUUID) videoUUID = video1Server2UUID
142 `http://localhost:${servers[0].port}/static/redundancy/${videoUUID}`,
143 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
146 for (const server of servers) {
147 const res = await getVideo(server.url, videoUUID)
149 const video: VideoDetails = res.body
151 for (const file of video.files) {
152 checkMagnetWebseeds(file, webseeds, server)
154 await makeGetRequest({
156 statusCodeExpected: HttpStatusCode.OK_200,
157 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
160 await makeGetRequest({
162 statusCodeExpected: HttpStatusCode.OK_200,
163 path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
169 const directories = [
170 'test' + servers[0].internalServerNumber + '/redundancy',
171 'test' + servers[1].internalServerNumber + '/videos'
174 for (const directory of directories) {
175 const files = await readdir(join(root(), directory))
176 expect(files).to.have.length.at.least(4)
178 for (const resolution of [ 240, 360, 480, 720 ]) {
179 expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
184 async function check0PlaylistRedundancies (videoUUID?: string) {
185 if (!videoUUID) videoUUID = video1Server2UUID
187 for (const server of servers) {
188 // With token to avoid issues with video follow constraints
189 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
190 const video: VideoDetails = res.body
192 expect(video.streamingPlaylists).to.be.an('array')
193 expect(video.streamingPlaylists).to.have.lengthOf(1)
194 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
198 async function check1PlaylistRedundancies (videoUUID?: string) {
199 if (!videoUUID) videoUUID = video1Server2UUID
201 for (const server of servers) {
202 const res = await getVideo(server.url, videoUUID)
203 const video: VideoDetails = res.body
205 expect(video.streamingPlaylists).to.have.lengthOf(1)
206 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
208 const redundancy = video.streamingPlaylists[0].redundancies[0]
210 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
213 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
214 const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
216 const res = await getVideo(servers[0].url, videoUUID)
217 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
219 for (const resolution of [ 240, 360, 480, 720 ]) {
220 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
223 const directories = [
224 'test' + servers[0].internalServerNumber + '/redundancy/hls',
225 'test' + servers[1].internalServerNumber + '/streaming-playlists/hls'
228 for (const directory of directories) {
229 const files = await readdir(join(root(), directory, videoUUID))
230 expect(files).to.have.length.at.least(4)
232 for (const resolution of [ 240, 360, 480, 720 ]) {
233 const filename = `${videoUUID}-${resolution}-fragmented.mp4`
235 expect(files.find(f => f === filename)).to.not.be.undefined
240 async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
241 let totalSize: number = null
244 if (strategy !== 'manual') {
249 const res = await getStats(servers[0].url)
250 const data: ServerStats = res.body
252 expect(data.videosRedundancy).to.have.lengthOf(statsLength)
254 const stat = data.videosRedundancy[0]
255 expect(stat.strategy).to.equal(strategy)
256 expect(stat.totalSize).to.equal(totalSize)
261 async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual) {
262 const stat = await checkStatsGlobal(strategy)
264 expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
265 expect(stat.totalVideoFiles).to.equal(4)
266 expect(stat.totalVideos).to.equal(1)
269 async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) {
270 const stat = await checkStatsGlobal(strategy)
272 expect(stat.totalUsed).to.equal(0)
273 expect(stat.totalVideoFiles).to.equal(0)
274 expect(stat.totalVideos).to.equal(0)
277 async function enableRedundancyOnServer1 () {
278 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
280 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
281 const follows: ActorFollow[] = res.body.data
282 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
283 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
285 expect(server3).to.not.be.undefined
286 expect(server3.following.hostRedundancyAllowed).to.be.false
288 expect(server2).to.not.be.undefined
289 expect(server2.following.hostRedundancyAllowed).to.be.true
292 async function disableRedundancyOnServer1 () {
293 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, false)
295 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
296 const follows: ActorFollow[] = res.body.data
297 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
298 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
300 expect(server3).to.not.be.undefined
301 expect(server3.following.hostRedundancyAllowed).to.be.false
303 expect(server2).to.not.be.undefined
304 expect(server2.following.hostRedundancyAllowed).to.be.false
307 describe('Test videos redundancy', function () {
309 describe('With most-views strategy', function () {
310 const strategy = 'most-views'
315 return flushAndRunServers(strategy)
318 it('Should have 1 webseed on the first video', async function () {
319 await check1WebSeed()
320 await check0PlaylistRedundancies()
321 await checkStatsWithoutRedundancy(strategy)
324 it('Should enable redundancy on server 1', function () {
325 return enableRedundancyOnServer1()
328 it('Should have 2 webseeds on the first video', async function () {
331 await waitJobs(servers)
332 await waitUntilLog(servers[0], 'Duplicated ', 5)
333 await waitJobs(servers)
335 await check2Webseeds()
336 await check1PlaylistRedundancies()
337 await checkStatsWith1Redundancy(strategy)
340 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
343 await disableRedundancyOnServer1()
345 await waitJobs(servers)
348 await check1WebSeed()
349 await check0PlaylistRedundancies()
351 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos', join('playlists', 'hls') ])
354 after(async function () {
355 return cleanupTests(servers)
359 describe('With trending strategy', function () {
360 const strategy = 'trending'
365 return flushAndRunServers(strategy)
368 it('Should have 1 webseed on the first video', async function () {
369 await check1WebSeed()
370 await check0PlaylistRedundancies()
371 await checkStatsWithoutRedundancy(strategy)
374 it('Should enable redundancy on server 1', function () {
375 return enableRedundancyOnServer1()
378 it('Should have 2 webseeds on the first video', async function () {
381 await waitJobs(servers)
382 await waitUntilLog(servers[0], 'Duplicated ', 5)
383 await waitJobs(servers)
385 await check2Webseeds()
386 await check1PlaylistRedundancies()
387 await checkStatsWith1Redundancy(strategy)
390 it('Should unfollow on server 1 and remove duplicated videos', async function () {
393 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
395 await waitJobs(servers)
398 await check1WebSeed()
399 await check0PlaylistRedundancies()
401 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ 'videos' ])
404 after(async function () {
405 await cleanupTests(servers)
409 describe('With recently added strategy', function () {
410 const strategy = 'recently-added'
415 return flushAndRunServers(strategy, { min_views: 3 })
418 it('Should have 1 webseed on the first video', async function () {
419 await check1WebSeed()
420 await check0PlaylistRedundancies()
421 await checkStatsWithoutRedundancy(strategy)
424 it('Should enable redundancy on server 1', function () {
425 return enableRedundancyOnServer1()
428 it('Should still have 1 webseed on the first video', async function () {
431 await waitJobs(servers)
433 await waitJobs(servers)
435 await check1WebSeed()
436 await check0PlaylistRedundancies()
437 await checkStatsWithoutRedundancy(strategy)
440 it('Should view 2 times the first video to have > min_views config', async function () {
443 await viewVideo(servers[0].url, video1Server2UUID)
444 await viewVideo(servers[2].url, video1Server2UUID)
447 await waitJobs(servers)
450 it('Should have 2 webseeds on the first video', async function () {
453 await waitJobs(servers)
454 await waitUntilLog(servers[0], 'Duplicated ', 5)
455 await waitJobs(servers)
457 await check2Webseeds()
458 await check1PlaylistRedundancies()
459 await checkStatsWith1Redundancy(strategy)
462 it('Should remove the video and the redundancy files', async function () {
465 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
467 await waitJobs(servers)
469 for (const server of servers) {
470 await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
474 after(async function () {
475 await cleanupTests(servers)
479 describe('With only HLS files', function () {
480 const strategy = 'recently-added'
482 before(async function () {
485 await flushAndRunServers(strategy, { min_views: 3 }, false)
488 it('Should have 0 playlist redundancy on the first video', async function () {
489 await check1WebSeed()
490 await check0PlaylistRedundancies()
493 it('Should enable redundancy on server 1', function () {
494 return enableRedundancyOnServer1()
497 it('Should still have 0 redundancy on the first video', async function () {
500 await waitJobs(servers)
502 await waitJobs(servers)
504 await check0PlaylistRedundancies()
505 await checkStatsWithoutRedundancy(strategy)
508 it('Should have 1 redundancy on the first video', async function () {
511 await viewVideo(servers[0].url, video1Server2UUID)
512 await viewVideo(servers[2].url, video1Server2UUID)
515 await waitJobs(servers)
517 await waitJobs(servers)
518 await waitUntilLog(servers[0], 'Duplicated ', 1)
519 await waitJobs(servers)
521 await check1PlaylistRedundancies()
522 await checkStatsWith1Redundancy(strategy)
525 it('Should remove the video and the redundancy files', async function () {
528 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
530 await waitJobs(servers)
532 for (const server of servers) {
533 await checkVideoFilesWereRemoved(video1Server2UUID, server.internalServerNumber)
538 describe('With manual strategy', function () {
542 return flushAndRunServers(null)
545 it('Should have 1 webseed on the first video', async function () {
546 await check1WebSeed()
547 await check0PlaylistRedundancies()
548 await checkStatsWithoutRedundancy('manual')
551 it('Should create a redundancy on first video', async function () {
552 await addVideoRedundancy({
554 accessToken: servers[0].accessToken,
555 videoId: video1Server2Id
559 it('Should have 2 webseeds on the first video', async function () {
562 await waitJobs(servers)
563 await waitUntilLog(servers[0], 'Duplicated ', 5)
564 await waitJobs(servers)
566 await check2Webseeds()
567 await check1PlaylistRedundancies()
568 await checkStatsWith1Redundancy('manual')
571 it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
574 const res = await listVideoRedundancies({
576 accessToken: servers[0].accessToken,
577 target: 'remote-videos'
580 const videos = res.body.data as VideoRedundancy[]
581 expect(videos).to.have.lengthOf(1)
583 const video = videos[0]
584 for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
585 await removeVideoRedundancy({
587 accessToken: servers[0].accessToken,
592 await waitJobs(servers)
595 await check1WebSeed()
596 await check0PlaylistRedundancies()
598 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
601 after(async function () {
602 await cleanupTests(servers)
606 describe('Test expiration', function () {
607 const strategy = 'recently-added'
609 async function checkContains (servers: ServerInfo[], str: string) {
610 for (const server of servers) {
611 const res = await getVideo(server.url, video1Server2UUID)
612 const video: VideoDetails = res.body
614 for (const f of video.files) {
615 expect(f.magnetUri).to.contain(str)
620 async function checkNotContains (servers: ServerInfo[], str: string) {
621 for (const server of servers) {
622 const res = await getVideo(server.url, video1Server2UUID)
623 const video: VideoDetails = res.body
625 for (const f of video.files) {
626 expect(f.magnetUri).to.not.contain(str)
631 before(async function () {
634 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
636 await enableRedundancyOnServer1()
639 it('Should still have 2 webseeds after 10 seconds', async function () {
645 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
647 // Maybe a server deleted a redundancy in the scheduler
650 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
654 it('Should stop server 1 and expire video redundancy', async function () {
657 killallServers([ servers[0] ])
661 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
664 after(async function () {
665 await cleanupTests(servers)
669 describe('Test file replacement', function () {
670 let video2Server2UUID: string
671 const strategy = 'recently-added'
673 before(async function () {
676 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
678 await enableRedundancyOnServer1()
680 await waitJobs(servers)
681 await waitUntilLog(servers[0], 'Duplicated ', 5)
682 await waitJobs(servers)
684 await check2Webseeds()
685 await check1PlaylistRedundancies()
686 await checkStatsWith1Redundancy(strategy)
688 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2' })
689 video2Server2UUID = res.body.video.uuid
692 it('Should cache video 2 webseeds on the first video', async function () {
695 await waitJobs(servers)
699 while (checked === false) {
703 await check1WebSeed(video1Server2UUID)
704 await check0PlaylistRedundancies(video1Server2UUID)
705 await check2Webseeds(video2Server2UUID)
706 await check1PlaylistRedundancies(video2Server2UUID)
715 it('Should disable strategy and remove redundancies', async function () {
718 await waitJobs(servers)
720 killallServers([ servers[0] ])
721 await reRunServer(servers[0], {
724 check_interval: '1 second',
730 await waitJobs(servers)
732 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].internalServerNumber, [ join('redundancy', 'hls') ])
735 after(async function () {
736 await cleanupTests(servers)