aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/api/videos
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tests/src/api/videos')
-rw-r--r--packages/tests/src/api/videos/channel-import-videos.ts161
-rw-r--r--packages/tests/src/api/videos/index.ts23
-rw-r--r--packages/tests/src/api/videos/multiple-servers.ts1095
-rw-r--r--packages/tests/src/api/videos/resumable-upload.ts316
-rw-r--r--packages/tests/src/api/videos/single-server.ts461
-rw-r--r--packages/tests/src/api/videos/video-captions.ts189
-rw-r--r--packages/tests/src/api/videos/video-change-ownership.ts314
-rw-r--r--packages/tests/src/api/videos/video-channel-syncs.ts321
-rw-r--r--packages/tests/src/api/videos/video-channels.ts556
-rw-r--r--packages/tests/src/api/videos/video-comments.ts335
-rw-r--r--packages/tests/src/api/videos/video-description.ts103
-rw-r--r--packages/tests/src/api/videos/video-files.ts202
-rw-r--r--packages/tests/src/api/videos/video-imports.ts635
-rw-r--r--packages/tests/src/api/videos/video-nsfw.ts227
-rw-r--r--packages/tests/src/api/videos/video-passwords.ts97
-rw-r--r--packages/tests/src/api/videos/video-playlist-thumbnails.ts234
-rw-r--r--packages/tests/src/api/videos/video-playlists.ts1210
-rw-r--r--packages/tests/src/api/videos/video-privacy.ts294
-rw-r--r--packages/tests/src/api/videos/video-schedule-update.ts155
-rw-r--r--packages/tests/src/api/videos/video-source.ts448
-rw-r--r--packages/tests/src/api/videos/video-static-file-privacy.ts602
-rw-r--r--packages/tests/src/api/videos/video-storyboard.ts213
-rw-r--r--packages/tests/src/api/videos/videos-common-filters.ts499
-rw-r--r--packages/tests/src/api/videos/videos-history.ts230
-rw-r--r--packages/tests/src/api/videos/videos-overview.ts129
25 files changed, 9049 insertions, 0 deletions
diff --git a/packages/tests/src/api/videos/channel-import-videos.ts b/packages/tests/src/api/videos/channel-import-videos.ts
new file mode 100644
index 000000000..d0e47fe95
--- /dev/null
+++ b/packages/tests/src/api/videos/channel-import-videos.ts
@@ -0,0 +1,161 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FIXTURE_URLS } from '@tests/shared/tests.js'
5import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 createSingleServer,
8 getServerImportConfig,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultVideoChannel,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos import in a channel', function () {
16 if (areHttpImportTestsDisabled()) return
17
18 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
19
20 describe('Import using ' + mode, function () {
21 let server: PeerTubeServer
22
23 before(async function () {
24 this.timeout(120_000)
25
26 server = await createSingleServer(1, getServerImportConfig(mode))
27
28 await setAccessTokensToServers([ server ])
29 await setDefaultVideoChannel([ server ])
30
31 await server.config.enableChannelSync()
32 })
33
34 it('Should import a whole channel without specifying the sync id', async function () {
35 this.timeout(240_000)
36
37 await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel })
38 await waitJobs(server)
39
40 const videos = await server.videos.listByChannel({ handle: server.store.channel.name })
41 expect(videos.total).to.equal(2)
42 })
43
44 it('These imports should not have a sync id', async function () {
45 const { total, data } = await server.imports.getMyVideoImports()
46
47 expect(total).to.equal(2)
48 expect(data).to.have.lengthOf(2)
49
50 for (const videoImport of data) {
51 expect(videoImport.videoChannelSync).to.not.exist
52 }
53 })
54
55 it('Should import a whole channel and specifying the sync id', async function () {
56 this.timeout(240_000)
57
58 {
59 server.store.channel.name = 'channel2'
60 const { id } = await server.channels.create({ attributes: { name: server.store.channel.name } })
61 server.store.channel.id = id
62 }
63
64 {
65 const attributes = {
66 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
67 videoChannelId: server.store.channel.id
68 }
69
70 const { videoChannelSync } = await server.channelSyncs.create({ attributes })
71 server.store.videoChannelSync = videoChannelSync
72
73 await waitJobs(server)
74 }
75
76 await server.channels.importVideos({
77 channelName: server.store.channel.name,
78 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
79 videoChannelSyncId: server.store.videoChannelSync.id
80 })
81
82 await waitJobs(server)
83 })
84
85 it('These imports should have a sync id', async function () {
86 const { total, data } = await server.imports.getMyVideoImports()
87
88 expect(total).to.equal(4)
89 expect(data).to.have.lengthOf(4)
90
91 const importsWithSyncId = data.filter(i => !!i.videoChannelSync)
92 expect(importsWithSyncId).to.have.lengthOf(2)
93
94 for (const videoImport of importsWithSyncId) {
95 expect(videoImport.videoChannelSync).to.exist
96 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
97 }
98 })
99
100 it('Should be able to filter imports by this sync id', async function () {
101 const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id })
102
103 expect(total).to.equal(2)
104 expect(data).to.have.lengthOf(2)
105
106 for (const videoImport of data) {
107 expect(videoImport.videoChannelSync).to.exist
108 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
109 }
110 })
111
112 it('Should limit max amount of videos synced on full sync', async function () {
113 this.timeout(240_000)
114
115 await server.kill()
116 await server.run({
117 import: {
118 video_channel_synchronization: {
119 full_sync_videos_limit: 1
120 }
121 }
122 })
123
124 const { id } = await server.channels.create({ attributes: { name: 'channel3' } })
125 const channel3Id = id
126
127 const { videoChannelSync } = await server.channelSyncs.create({
128 attributes: {
129 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
130 videoChannelId: channel3Id
131 }
132 })
133 const syncId = videoChannelSync.id
134
135 await waitJobs(server)
136
137 await server.channels.importVideos({
138 channelName: 'channel3',
139 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
140 videoChannelSyncId: syncId
141 })
142
143 await waitJobs(server)
144
145 const { total, data } = await server.videos.listByChannel({ handle: 'channel3' })
146
147 expect(total).to.equal(1)
148 expect(data).to.have.lengthOf(1)
149 })
150
151 after(async function () {
152 await server?.kill()
153 })
154 })
155 }
156
157 runSuite('yt-dlp')
158
159 // FIXME: With recent changes on youtube, youtube-dl doesn't fetch live replays which means the test suite fails
160 // runSuite('youtube-dl')
161})
diff --git a/packages/tests/src/api/videos/index.ts b/packages/tests/src/api/videos/index.ts
new file mode 100644
index 000000000..fcb1d5a81
--- /dev/null
+++ b/packages/tests/src/api/videos/index.ts
@@ -0,0 +1,23 @@
1import './multiple-servers.js'
2import './resumable-upload.js'
3import './single-server.js'
4import './video-captions.js'
5import './video-change-ownership.js'
6import './video-channels.js'
7import './channel-import-videos.js'
8import './video-channel-syncs.js'
9import './video-comments.js'
10import './video-description.js'
11import './video-files.js'
12import './video-imports.js'
13import './video-nsfw.js'
14import './video-playlists.js'
15import './video-playlist-thumbnails.js'
16import './video-source.js'
17import './video-privacy.js'
18import './video-schedule-update.js'
19import './videos-common-filters.js'
20import './videos-history.js'
21import './videos-overview.js'
22import './video-static-file-privacy.js'
23import './video-storyboard.js'
diff --git a/packages/tests/src/api/videos/multiple-servers.ts b/packages/tests/src/api/videos/multiple-servers.ts
new file mode 100644
index 000000000..03afd7cbb
--- /dev/null
+++ b/packages/tests/src/api/videos/multiple-servers.ts
@@ -0,0 +1,1095 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import request from 'supertest'
5import { wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@peertube/peertube-models'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 cleanupTests,
10 createMultipleServers,
11 doubleFollow,
12 makeGetRequest,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 setDefaultAccountAvatar,
16 setDefaultChannelAvatar,
17 waitJobs
18} from '@peertube/peertube-server-commands'
19import { testImageGeneratedByFFmpeg, dateIsValid } from '@tests/shared/checks.js'
20import { checkTmpIsEmpty } from '@tests/shared/directories.js'
21import { completeVideoCheck, saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
22import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
23
24describe('Test multiple servers', function () {
25 let servers: PeerTubeServer[] = []
26 const toRemove = []
27 let videoUUID = ''
28 let videoChannelId: number
29
30 before(async function () {
31 this.timeout(120000)
32
33 servers = await createMultipleServers(3)
34
35 // Get the access tokens
36 await setAccessTokensToServers(servers)
37
38 {
39 const videoChannel = {
40 name: 'super_channel_name',
41 displayName: 'my channel',
42 description: 'super channel'
43 }
44 await servers[0].channels.create({ attributes: videoChannel })
45 await setDefaultChannelAvatar(servers[0], videoChannel.name)
46 await setDefaultAccountAvatar(servers)
47
48 const { data } = await servers[0].channels.list({ start: 0, count: 1 })
49 videoChannelId = data[0].id
50 }
51
52 // Server 1 and server 2 follow each other
53 await doubleFollow(servers[0], servers[1])
54 // Server 1 and server 3 follow each other
55 await doubleFollow(servers[0], servers[2])
56 // Server 2 and server 3 follow each other
57 await doubleFollow(servers[1], servers[2])
58 })
59
60 it('Should not have videos for all servers', async function () {
61 for (const server of servers) {
62 const { data } = await server.videos.list()
63 expect(data).to.be.an('array')
64 expect(data.length).to.equal(0)
65 }
66 })
67
68 describe('Should upload the video and propagate on each server', function () {
69
70 it('Should upload the video on server 1 and propagate on each server', async function () {
71 this.timeout(60000)
72
73 const attributes = {
74 name: 'my super name for server 1',
75 category: 5,
76 licence: 4,
77 language: 'ja',
78 nsfw: true,
79 description: 'my super description for server 1',
80 support: 'my super support text for server 1',
81 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
82 tags: [ 'tag1p1', 'tag2p1' ],
83 channelId: videoChannelId,
84 fixture: 'video_short1.webm'
85 }
86 await servers[0].videos.upload({ attributes })
87
88 await waitJobs(servers)
89
90 // All servers should have this video
91 let publishedAt: string = null
92 for (const server of servers) {
93 const isLocal = server.port === servers[0].port
94 const checkAttributes = {
95 name: 'my super name for server 1',
96 category: 5,
97 licence: 4,
98 language: 'ja',
99 nsfw: true,
100 description: 'my super description for server 1',
101 support: 'my super support text for server 1',
102 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
103 account: {
104 name: 'root',
105 host: servers[0].host
106 },
107 isLocal,
108 publishedAt,
109 duration: 10,
110 tags: [ 'tag1p1', 'tag2p1' ],
111 privacy: VideoPrivacy.PUBLIC,
112 commentsEnabled: true,
113 downloadEnabled: true,
114 channel: {
115 displayName: 'my channel',
116 name: 'super_channel_name',
117 description: 'super channel',
118 isLocal
119 },
120 fixture: 'video_short1.webm',
121 files: [
122 {
123 resolution: 720,
124 size: 572456
125 }
126 ]
127 }
128
129 const { data } = await server.videos.list()
130 expect(data).to.be.an('array')
131 expect(data.length).to.equal(1)
132 const video = data[0]
133
134 await completeVideoCheck({ server, originServer: servers[0], videoUUID: video.uuid, attributes: checkAttributes })
135 publishedAt = video.publishedAt as string
136
137 expect(video.channel.avatars).to.have.lengthOf(2)
138 expect(video.account.avatars).to.have.lengthOf(2)
139
140 for (const image of [ ...video.channel.avatars, ...video.account.avatars ]) {
141 expect(image.createdAt).to.exist
142 expect(image.updatedAt).to.exist
143 expect(image.width).to.be.above(20).and.below(1000)
144 expect(image.path).to.exist
145
146 await makeGetRequest({
147 url: server.url,
148 path: image.path,
149 expectedStatus: HttpStatusCode.OK_200
150 })
151 }
152 }
153 })
154
155 it('Should upload the video on server 2 and propagate on each server', async function () {
156 this.timeout(240000)
157
158 const user = {
159 username: 'user1',
160 password: 'super_password'
161 }
162 await servers[1].users.create({ username: user.username, password: user.password })
163 const userAccessToken = await servers[1].login.getAccessToken(user)
164
165 const attributes = {
166 name: 'my super name for server 2',
167 category: 4,
168 licence: 3,
169 language: 'de',
170 nsfw: true,
171 description: 'my super description for server 2',
172 support: 'my super support text for server 2',
173 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
174 fixture: 'video_short2.webm',
175 thumbnailfile: 'custom-thumbnail.jpg',
176 previewfile: 'custom-preview.jpg'
177 }
178 await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' })
179
180 // Transcoding
181 await waitJobs(servers)
182
183 // All servers should have this video
184 for (const server of servers) {
185 const isLocal = server.url === servers[1].url
186 const checkAttributes = {
187 name: 'my super name for server 2',
188 category: 4,
189 licence: 3,
190 language: 'de',
191 nsfw: true,
192 description: 'my super description for server 2',
193 support: 'my super support text for server 2',
194 account: {
195 name: 'user1',
196 host: servers[1].host
197 },
198 isLocal,
199 commentsEnabled: true,
200 downloadEnabled: true,
201 duration: 5,
202 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
203 privacy: VideoPrivacy.PUBLIC,
204 channel: {
205 displayName: 'Main user1 channel',
206 name: 'user1_channel',
207 description: 'super channel',
208 isLocal
209 },
210 fixture: 'video_short2.webm',
211 files: [
212 {
213 resolution: 240,
214 size: 270000
215 },
216 {
217 resolution: 360,
218 size: 359000
219 },
220 {
221 resolution: 480,
222 size: 465000
223 },
224 {
225 resolution: 720,
226 size: 750000
227 }
228 ],
229 thumbnailfile: 'custom-thumbnail',
230 previewfile: 'custom-preview'
231 }
232
233 const { data } = await server.videos.list()
234 expect(data).to.be.an('array')
235 expect(data.length).to.equal(2)
236 const video = data[1]
237
238 await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
239 }
240 })
241
242 it('Should upload two videos on server 3 and propagate on each server', async function () {
243 this.timeout(45000)
244
245 {
246 const attributes = {
247 name: 'my super name for server 3',
248 category: 6,
249 licence: 5,
250 language: 'de',
251 nsfw: true,
252 description: 'my super description for server 3',
253 support: 'my super support text for server 3',
254 tags: [ 'tag1p3' ],
255 fixture: 'video_short3.webm'
256 }
257 await servers[2].videos.upload({ attributes })
258 }
259
260 {
261 const attributes = {
262 name: 'my super name for server 3-2',
263 category: 7,
264 licence: 6,
265 language: 'ko',
266 nsfw: false,
267 description: 'my super description for server 3-2',
268 support: 'my super support text for server 3-2',
269 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
270 fixture: 'video_short.webm'
271 }
272 await servers[2].videos.upload({ attributes })
273 }
274
275 await waitJobs(servers)
276
277 // All servers should have this video
278 for (const server of servers) {
279 const isLocal = server.url === servers[2].url
280 const { data } = await server.videos.list()
281
282 expect(data).to.be.an('array')
283 expect(data.length).to.equal(4)
284
285 // We not sure about the order of the two last uploads
286 let video1 = null
287 let video2 = null
288 if (data[2].name === 'my super name for server 3') {
289 video1 = data[2]
290 video2 = data[3]
291 } else {
292 video1 = data[3]
293 video2 = data[2]
294 }
295
296 const checkAttributesVideo1 = {
297 name: 'my super name for server 3',
298 category: 6,
299 licence: 5,
300 language: 'de',
301 nsfw: true,
302 description: 'my super description for server 3',
303 support: 'my super support text for server 3',
304 account: {
305 name: 'root',
306 host: servers[2].host
307 },
308 isLocal,
309 duration: 5,
310 commentsEnabled: true,
311 downloadEnabled: true,
312 tags: [ 'tag1p3' ],
313 privacy: VideoPrivacy.PUBLIC,
314 channel: {
315 displayName: 'Main root channel',
316 name: 'root_channel',
317 description: '',
318 isLocal
319 },
320 fixture: 'video_short3.webm',
321 files: [
322 {
323 resolution: 720,
324 size: 292677
325 }
326 ]
327 }
328 await completeVideoCheck({ server, originServer: servers[2], videoUUID: video1.uuid, attributes: checkAttributesVideo1 })
329
330 const checkAttributesVideo2 = {
331 name: 'my super name for server 3-2',
332 category: 7,
333 licence: 6,
334 language: 'ko',
335 nsfw: false,
336 description: 'my super description for server 3-2',
337 support: 'my super support text for server 3-2',
338 account: {
339 name: 'root',
340 host: servers[2].host
341 },
342 commentsEnabled: true,
343 downloadEnabled: true,
344 isLocal,
345 duration: 5,
346 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
347 privacy: VideoPrivacy.PUBLIC,
348 channel: {
349 displayName: 'Main root channel',
350 name: 'root_channel',
351 description: '',
352 isLocal
353 },
354 fixture: 'video_short.webm',
355 files: [
356 {
357 resolution: 720,
358 size: 218910
359 }
360 ]
361 }
362 await completeVideoCheck({ server, originServer: servers[2], videoUUID: video2.uuid, attributes: checkAttributesVideo2 })
363 }
364 })
365 })
366
367 describe('It should list local videos', function () {
368 it('Should list only local videos on server 1', async function () {
369 const { data, total } = await servers[0].videos.list({ isLocal: true })
370
371 expect(total).to.equal(1)
372 expect(data).to.be.an('array')
373 expect(data.length).to.equal(1)
374 expect(data[0].name).to.equal('my super name for server 1')
375 })
376
377 it('Should list only local videos on server 2', async function () {
378 const { data, total } = await servers[1].videos.list({ isLocal: true })
379
380 expect(total).to.equal(1)
381 expect(data).to.be.an('array')
382 expect(data.length).to.equal(1)
383 expect(data[0].name).to.equal('my super name for server 2')
384 })
385
386 it('Should list only local videos on server 3', async function () {
387 const { data, total } = await servers[2].videos.list({ isLocal: true })
388
389 expect(total).to.equal(2)
390 expect(data).to.be.an('array')
391 expect(data.length).to.equal(2)
392 expect(data[0].name).to.equal('my super name for server 3')
393 expect(data[1].name).to.equal('my super name for server 3-2')
394 })
395 })
396
397 describe('Should seed the uploaded video', function () {
398
399 it('Should add the file 1 by asking server 3', async function () {
400 this.retries(2)
401 this.timeout(30000)
402
403 const { data } = await servers[2].videos.list()
404
405 const video = data[0]
406 toRemove.push(data[2])
407 toRemove.push(data[3])
408
409 const videoDetails = await servers[2].videos.get({ id: video.id })
410
411 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
412 })
413
414 it('Should add the file 2 by asking server 1', async function () {
415 this.retries(2)
416 this.timeout(30000)
417
418 const { data } = await servers[0].videos.list()
419
420 const video = data[1]
421 const videoDetails = await servers[0].videos.get({ id: video.id })
422
423 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
424 })
425
426 it('Should add the file 3 by asking server 2', async function () {
427 this.retries(2)
428 this.timeout(30000)
429
430 const { data } = await servers[1].videos.list()
431
432 const video = data[2]
433 const videoDetails = await servers[1].videos.get({ id: video.id })
434
435 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
436 })
437
438 it('Should add the file 3-2 by asking server 1', async function () {
439 this.retries(2)
440 this.timeout(30000)
441
442 const { data } = await servers[0].videos.list()
443
444 const video = data[3]
445 const videoDetails = await servers[0].videos.get({ id: video.id })
446
447 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
448 })
449
450 it('Should add the file 2 in 360p by asking server 1', async function () {
451 this.retries(2)
452 this.timeout(30000)
453
454 const { data } = await servers[0].videos.list()
455
456 const video = data.find(v => v.name === 'my super name for server 2')
457 const videoDetails = await servers[0].videos.get({ id: video.id })
458
459 const file = videoDetails.files.find(f => f.resolution.id === 360)
460 expect(file).not.to.be.undefined
461
462 await checkWebTorrentWorks(file.magnetUri)
463 })
464 })
465
466 describe('Should update video views, likes and dislikes', function () {
467 let localVideosServer3 = []
468 let remoteVideosServer1 = []
469 let remoteVideosServer2 = []
470 let remoteVideosServer3 = []
471
472 before(async function () {
473 {
474 const { data } = await servers[0].videos.list()
475 remoteVideosServer1 = data.filter(video => video.isLocal === false).map(video => video.uuid)
476 }
477
478 {
479 const { data } = await servers[1].videos.list()
480 remoteVideosServer2 = data.filter(video => video.isLocal === false).map(video => video.uuid)
481 }
482
483 {
484 const { data } = await servers[2].videos.list()
485 localVideosServer3 = data.filter(video => video.isLocal === true).map(video => video.uuid)
486 remoteVideosServer3 = data.filter(video => video.isLocal === false).map(video => video.uuid)
487 }
488 })
489
490 it('Should view multiple videos on owned servers', async function () {
491 this.timeout(30000)
492
493 await servers[2].views.simulateView({ id: localVideosServer3[0] })
494 await wait(1000)
495
496 await servers[2].views.simulateView({ id: localVideosServer3[0] })
497 await servers[2].views.simulateView({ id: localVideosServer3[1] })
498
499 await wait(1000)
500
501 await servers[2].views.simulateView({ id: localVideosServer3[0] })
502 await servers[2].views.simulateView({ id: localVideosServer3[0] })
503
504 await waitJobs(servers)
505
506 for (const server of servers) {
507 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
508 }
509
510 await waitJobs(servers)
511
512 for (const server of servers) {
513 const { data } = await server.videos.list()
514
515 const video0 = data.find(v => v.uuid === localVideosServer3[0])
516 const video1 = data.find(v => v.uuid === localVideosServer3[1])
517
518 expect(video0.views).to.equal(3)
519 expect(video1.views).to.equal(1)
520 }
521 })
522
523 it('Should view multiple videos on each servers', async function () {
524 this.timeout(45000)
525
526 const tasks: Promise<any>[] = []
527 tasks.push(servers[0].views.simulateView({ id: remoteVideosServer1[0] }))
528 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
529 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
530 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[0] }))
531 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
532 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
533 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
534 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
535 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
536 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
537
538 await Promise.all(tasks)
539
540 await waitJobs(servers)
541
542 for (const server of servers) {
543 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
544 }
545
546 await waitJobs(servers)
547
548 let baseVideos = null
549
550 for (const server of servers) {
551 const { data } = await server.videos.list()
552
553 // Initialize base videos for future comparisons
554 if (baseVideos === null) {
555 baseVideos = data
556 continue
557 }
558
559 for (const baseVideo of baseVideos) {
560 const sameVideo = data.find(video => video.name === baseVideo.name)
561 expect(baseVideo.views).to.equal(sameVideo.views)
562 }
563 }
564 })
565
566 it('Should like and dislikes videos on different services', async function () {
567 this.timeout(50000)
568
569 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
570 await wait(500)
571 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'dislike' })
572 await wait(500)
573 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
574 await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'like' })
575 await wait(500)
576 await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'dislike' })
577 await servers[2].videos.rate({ id: remoteVideosServer3[1], rating: 'dislike' })
578 await wait(500)
579 await servers[2].videos.rate({ id: remoteVideosServer3[0], rating: 'like' })
580
581 await waitJobs(servers)
582 await wait(5000)
583 await waitJobs(servers)
584
585 let baseVideos = null
586 for (const server of servers) {
587 const { data } = await server.videos.list()
588
589 // Initialize base videos for future comparisons
590 if (baseVideos === null) {
591 baseVideos = data
592 continue
593 }
594
595 for (const baseVideo of baseVideos) {
596 const sameVideo = data.find(video => video.name === baseVideo.name)
597 expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
598 expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`)
599 }
600 }
601 })
602 })
603
604 describe('Should manipulate these videos', function () {
605 let updatedAtMin: Date
606
607 it('Should update video 3', async function () {
608 this.timeout(30000)
609
610 const attributes = {
611 name: 'my super video updated',
612 category: 10,
613 licence: 7,
614 language: 'fr',
615 nsfw: true,
616 description: 'my super description updated',
617 support: 'my super support text updated',
618 tags: [ 'tag_up_1', 'tag_up_2' ],
619 thumbnailfile: 'custom-thumbnail.jpg',
620 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
621 previewfile: 'custom-preview.jpg'
622 }
623
624 updatedAtMin = new Date()
625 await servers[2].videos.update({ id: toRemove[0].id, attributes })
626
627 await waitJobs(servers)
628 })
629
630 it('Should have the video 3 updated on each server', async function () {
631 this.timeout(30000)
632
633 for (const server of servers) {
634 const { data } = await server.videos.list()
635
636 const videoUpdated = data.find(video => video.name === 'my super video updated')
637 expect(!!videoUpdated).to.be.true
638
639 expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin)
640
641 const isLocal = server.url === servers[2].url
642 const checkAttributes = {
643 name: 'my super video updated',
644 category: 10,
645 licence: 7,
646 language: 'fr',
647 nsfw: true,
648 description: 'my super description updated',
649 support: 'my super support text updated',
650 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
651 account: {
652 name: 'root',
653 host: servers[2].host
654 },
655 isLocal,
656 duration: 5,
657 commentsEnabled: true,
658 downloadEnabled: true,
659 tags: [ 'tag_up_1', 'tag_up_2' ],
660 privacy: VideoPrivacy.PUBLIC,
661 channel: {
662 displayName: 'Main root channel',
663 name: 'root_channel',
664 description: '',
665 isLocal
666 },
667 fixture: 'video_short3.webm',
668 files: [
669 {
670 resolution: 720,
671 size: 292677
672 }
673 ],
674 thumbnailfile: 'custom-thumbnail',
675 previewfile: 'custom-preview'
676 }
677 await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes })
678 }
679 })
680
681 it('Should only update thumbnail and update updatedAt attribute', async function () {
682 this.timeout(30000)
683
684 const attributes = {
685 thumbnailfile: 'custom-thumbnail.jpg'
686 }
687
688 updatedAtMin = new Date()
689 await servers[2].videos.update({ id: toRemove[0].id, attributes })
690
691 await waitJobs(servers)
692
693 for (const server of servers) {
694 const { data } = await server.videos.list()
695
696 const videoUpdated = data.find(video => video.name === 'my super video updated')
697 expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin)
698 }
699 })
700
701 it('Should remove the videos 3 and 3-2 by asking server 3 and correctly delete files', async function () {
702 this.timeout(30000)
703
704 for (const id of [ toRemove[0].id, toRemove[1].id ]) {
705 await saveVideoInServers(servers, id)
706
707 await servers[2].videos.remove({ id })
708
709 await waitJobs(servers)
710
711 for (const server of servers) {
712 await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
713 }
714 }
715 })
716
717 it('Should have videos 1 and 3 on each server', async function () {
718 for (const server of servers) {
719 const { data } = await server.videos.list()
720
721 expect(data).to.be.an('array')
722 expect(data.length).to.equal(2)
723 expect(data[0].name).not.to.equal(data[1].name)
724 expect(data[0].name).not.to.equal(toRemove[0].name)
725 expect(data[1].name).not.to.equal(toRemove[0].name)
726 expect(data[0].name).not.to.equal(toRemove[1].name)
727 expect(data[1].name).not.to.equal(toRemove[1].name)
728
729 videoUUID = data.find(video => video.name === 'my super name for server 1').uuid
730 }
731 })
732
733 it('Should get the same video by UUID on each server', async function () {
734 let baseVideo = null
735 for (const server of servers) {
736 const video = await server.videos.get({ id: videoUUID })
737
738 if (baseVideo === null) {
739 baseVideo = video
740 continue
741 }
742
743 expect(baseVideo.name).to.equal(video.name)
744 expect(baseVideo.uuid).to.equal(video.uuid)
745 expect(baseVideo.category.id).to.equal(video.category.id)
746 expect(baseVideo.language.id).to.equal(video.language.id)
747 expect(baseVideo.licence.id).to.equal(video.licence.id)
748 expect(baseVideo.nsfw).to.equal(video.nsfw)
749 expect(baseVideo.account.name).to.equal(video.account.name)
750 expect(baseVideo.account.displayName).to.equal(video.account.displayName)
751 expect(baseVideo.account.url).to.equal(video.account.url)
752 expect(baseVideo.account.host).to.equal(video.account.host)
753 expect(baseVideo.tags).to.deep.equal(video.tags)
754 }
755 })
756
757 it('Should get the preview from each server', async function () {
758 for (const server of servers) {
759 const video = await server.videos.get({ id: videoUUID })
760
761 await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
762 }
763 })
764 })
765
766 describe('Should comment these videos', function () {
767 let childOfFirstChild: VideoCommentThreadTree
768
769 it('Should add comment (threads and replies)', async function () {
770 this.timeout(25000)
771
772 {
773 const text = 'my super first comment'
774 await servers[0].comments.createThread({ videoId: videoUUID, text })
775 }
776
777 {
778 const text = 'my super second comment'
779 await servers[2].comments.createThread({ videoId: videoUUID, text })
780 }
781
782 await waitJobs(servers)
783
784 {
785 const threadId = await servers[1].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
786
787 const text = 'my super answer to thread 1'
788 await servers[1].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text })
789 }
790
791 await waitJobs(servers)
792
793 {
794 const threadId = await servers[2].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
795
796 const body = await servers[2].comments.getThread({ videoId: videoUUID, threadId })
797 const childCommentId = body.children[0].comment.id
798
799 const text3 = 'my second answer to thread 1'
800 await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: text3 })
801
802 const text2 = 'my super answer to answer of thread 1'
803 await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: childCommentId, text: text2 })
804 }
805
806 await waitJobs(servers)
807 })
808
809 it('Should have these threads', async function () {
810 for (const server of servers) {
811 const body = await server.comments.listThreads({ videoId: videoUUID })
812
813 expect(body.total).to.equal(2)
814 expect(body.data).to.be.an('array')
815 expect(body.data).to.have.lengthOf(2)
816
817 {
818 const comment = body.data.find(c => c.text === 'my super first comment')
819 expect(comment).to.not.be.undefined
820 expect(comment.inReplyToCommentId).to.be.null
821 expect(comment.account.name).to.equal('root')
822 expect(comment.account.host).to.equal(servers[0].host)
823 expect(comment.totalReplies).to.equal(3)
824 expect(dateIsValid(comment.createdAt as string)).to.be.true
825 expect(dateIsValid(comment.updatedAt as string)).to.be.true
826 }
827
828 {
829 const comment = body.data.find(c => c.text === 'my super second comment')
830 expect(comment).to.not.be.undefined
831 expect(comment.inReplyToCommentId).to.be.null
832 expect(comment.account.name).to.equal('root')
833 expect(comment.account.host).to.equal(servers[2].host)
834 expect(comment.totalReplies).to.equal(0)
835 expect(dateIsValid(comment.createdAt as string)).to.be.true
836 expect(dateIsValid(comment.updatedAt as string)).to.be.true
837 }
838 }
839 })
840
841 it('Should have these comments', async function () {
842 for (const server of servers) {
843 const body = await server.comments.listThreads({ videoId: videoUUID })
844 const threadId = body.data.find(c => c.text === 'my super first comment').id
845
846 const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
847
848 expect(tree.comment.text).equal('my super first comment')
849 expect(tree.comment.account.name).equal('root')
850 expect(tree.comment.account.host).equal(servers[0].host)
851 expect(tree.children).to.have.lengthOf(2)
852
853 const firstChild = tree.children[0]
854 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
855 expect(firstChild.comment.account.name).equal('root')
856 expect(firstChild.comment.account.host).equal(servers[1].host)
857 expect(firstChild.children).to.have.lengthOf(1)
858
859 childOfFirstChild = firstChild.children[0]
860 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
861 expect(childOfFirstChild.comment.account.name).equal('root')
862 expect(childOfFirstChild.comment.account.host).equal(servers[2].host)
863 expect(childOfFirstChild.children).to.have.lengthOf(0)
864
865 const secondChild = tree.children[1]
866 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
867 expect(secondChild.comment.account.name).equal('root')
868 expect(secondChild.comment.account.host).equal(servers[2].host)
869 expect(secondChild.children).to.have.lengthOf(0)
870 }
871 })
872
873 it('Should delete a reply', async function () {
874 this.timeout(30000)
875
876 await servers[2].comments.delete({ videoId: videoUUID, commentId: childOfFirstChild.comment.id })
877
878 await waitJobs(servers)
879 })
880
881 it('Should have this comment marked as deleted', async function () {
882 for (const server of servers) {
883 const { data } = await server.comments.listThreads({ videoId: videoUUID })
884 const threadId = data.find(c => c.text === 'my super first comment').id
885
886 const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
887 expect(tree.comment.text).equal('my super first comment')
888
889 const firstChild = tree.children[0]
890 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
891 expect(firstChild.children).to.have.lengthOf(1)
892
893 const deletedComment = firstChild.children[0].comment
894 expect(deletedComment.isDeleted).to.be.true
895 expect(deletedComment.deletedAt).to.not.be.null
896 expect(deletedComment.account).to.be.null
897 expect(deletedComment.text).to.equal('')
898
899 const secondChild = tree.children[1]
900 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
901 }
902 })
903
904 it('Should delete the thread comments', async function () {
905 this.timeout(30000)
906
907 const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
908 const commentId = data.find(c => c.text === 'my super first comment').id
909 await servers[0].comments.delete({ videoId: videoUUID, commentId })
910
911 await waitJobs(servers)
912 })
913
914 it('Should have the threads marked as deleted on other servers too', async function () {
915 for (const server of servers) {
916 const body = await server.comments.listThreads({ videoId: videoUUID })
917
918 expect(body.total).to.equal(2)
919 expect(body.data).to.be.an('array')
920 expect(body.data).to.have.lengthOf(2)
921
922 {
923 const comment = body.data[0]
924 expect(comment).to.not.be.undefined
925 expect(comment.inReplyToCommentId).to.be.null
926 expect(comment.account.name).to.equal('root')
927 expect(comment.account.host).to.equal(servers[2].host)
928 expect(comment.totalReplies).to.equal(0)
929 expect(dateIsValid(comment.createdAt as string)).to.be.true
930 expect(dateIsValid(comment.updatedAt as string)).to.be.true
931 }
932
933 {
934 const deletedComment = body.data[1]
935 expect(deletedComment).to.not.be.undefined
936 expect(deletedComment.isDeleted).to.be.true
937 expect(deletedComment.deletedAt).to.not.be.null
938 expect(deletedComment.text).to.equal('')
939 expect(deletedComment.inReplyToCommentId).to.be.null
940 expect(deletedComment.account).to.be.null
941 expect(deletedComment.totalReplies).to.equal(2)
942 expect(dateIsValid(deletedComment.createdAt as string)).to.be.true
943 expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true
944 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
945 }
946 }
947 })
948
949 it('Should delete a remote thread by the origin server', async function () {
950 this.timeout(5000)
951
952 const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
953 const commentId = data.find(c => c.text === 'my super second comment').id
954 await servers[0].comments.delete({ videoId: videoUUID, commentId })
955
956 await waitJobs(servers)
957 })
958
959 it('Should have the threads marked as deleted on other servers too', async function () {
960 for (const server of servers) {
961 const body = await server.comments.listThreads({ videoId: videoUUID })
962
963 expect(body.total).to.equal(2)
964 expect(body.data).to.have.lengthOf(2)
965
966 {
967 const comment = body.data[0]
968 expect(comment.text).to.equal('')
969 expect(comment.isDeleted).to.be.true
970 expect(comment.createdAt).to.not.be.null
971 expect(comment.deletedAt).to.not.be.null
972 expect(comment.account).to.be.null
973 expect(comment.totalReplies).to.equal(0)
974 }
975
976 {
977 const comment = body.data[1]
978 expect(comment.text).to.equal('')
979 expect(comment.isDeleted).to.be.true
980 expect(comment.createdAt).to.not.be.null
981 expect(comment.deletedAt).to.not.be.null
982 expect(comment.account).to.be.null
983 expect(comment.totalReplies).to.equal(2)
984 }
985 }
986 })
987
988 it('Should disable comments and download', async function () {
989 this.timeout(20000)
990
991 const attributes = {
992 commentsEnabled: false,
993 downloadEnabled: false
994 }
995
996 await servers[0].videos.update({ id: videoUUID, attributes })
997
998 await waitJobs(servers)
999
1000 for (const server of servers) {
1001 const video = await server.videos.get({ id: videoUUID })
1002 expect(video.commentsEnabled).to.be.false
1003 expect(video.downloadEnabled).to.be.false
1004
1005 const text = 'my super forbidden comment'
1006 await server.comments.createThread({ videoId: videoUUID, text, expectedStatus: HttpStatusCode.CONFLICT_409 })
1007 }
1008 })
1009 })
1010
1011 describe('With minimum parameters', function () {
1012 it('Should upload and propagate the video', async function () {
1013 this.timeout(120000)
1014
1015 const path = '/api/v1/videos/upload'
1016
1017 const req = request(servers[1].url)
1018 .post(path)
1019 .set('Accept', 'application/json')
1020 .set('Authorization', 'Bearer ' + servers[1].accessToken)
1021 .field('name', 'minimum parameters')
1022 .field('privacy', '1')
1023 .field('channelId', '1')
1024
1025 await req.attach('videofile', buildAbsoluteFixturePath('video_short.webm'))
1026 .expect(HttpStatusCode.OK_200)
1027
1028 await waitJobs(servers)
1029
1030 for (const server of servers) {
1031 const { data } = await server.videos.list()
1032 const video = data.find(v => v.name === 'minimum parameters')
1033
1034 const isLocal = server.url === servers[1].url
1035 const checkAttributes = {
1036 name: 'minimum parameters',
1037 category: null,
1038 licence: null,
1039 language: null,
1040 nsfw: false,
1041 description: null,
1042 support: null,
1043 account: {
1044 name: 'root',
1045 host: servers[1].host
1046 },
1047 isLocal,
1048 duration: 5,
1049 commentsEnabled: true,
1050 downloadEnabled: true,
1051 tags: [],
1052 privacy: VideoPrivacy.PUBLIC,
1053 channel: {
1054 displayName: 'Main root channel',
1055 name: 'root_channel',
1056 description: '',
1057 isLocal
1058 },
1059 fixture: 'video_short.webm',
1060 files: [
1061 {
1062 resolution: 720,
1063 size: 61000
1064 },
1065 {
1066 resolution: 480,
1067 size: 40000
1068 },
1069 {
1070 resolution: 360,
1071 size: 32000
1072 },
1073 {
1074 resolution: 240,
1075 size: 23000
1076 }
1077 ]
1078 }
1079 await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
1080 }
1081 })
1082 })
1083
1084 describe('TMP directory', function () {
1085 it('Should have an empty tmp directory', async function () {
1086 for (const server of servers) {
1087 await checkTmpIsEmpty(server)
1088 }
1089 })
1090 })
1091
1092 after(async function () {
1093 await cleanupTests(servers)
1094 })
1095})
diff --git a/packages/tests/src/api/videos/resumable-upload.ts b/packages/tests/src/api/videos/resumable-upload.ts
new file mode 100644
index 000000000..628e0298c
--- /dev/null
+++ b/packages/tests/src/api/videos/resumable-upload.ts
@@ -0,0 +1,316 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists } from 'fs-extra/esm'
5import { readdir, stat } from 'fs/promises'
6import { join } from 'path'
7import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models'
8import { buildAbsoluteFixturePath, sha1 } from '@peertube/peertube-node-utils'
9import {
10 cleanupTests,
11 createSingleServer,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 setDefaultVideoChannel
15} from '@peertube/peertube-server-commands'
16
17// Most classic resumable upload tests are done in other test suites
18
19describe('Test resumable upload', function () {
20 const path = '/api/v1/videos/upload-resumable'
21 const defaultFixture = 'video_short.mp4'
22 let server: PeerTubeServer
23 let rootId: number
24 let userAccessToken: string
25 let userChannelId: number
26
27 async function buildSize (fixture: string, size?: number) {
28 if (size !== undefined) return size
29
30 const baseFixture = buildAbsoluteFixturePath(fixture)
31 return (await stat(baseFixture)).size
32 }
33
34 async function prepareUpload (options: {
35 channelId?: number
36 token?: string
37 size?: number
38 originalName?: string
39 lastModified?: number
40 } = {}) {
41 const { token, originalName, lastModified } = options
42
43 const size = await buildSize(defaultFixture, options.size)
44
45 const attributes = {
46 name: 'video',
47 channelId: options.channelId ?? server.store.channel.id,
48 privacy: VideoPrivacy.PUBLIC,
49 fixture: defaultFixture
50 }
51
52 const mimetype = 'video/mp4'
53
54 const res = await server.videos.prepareResumableUpload({ path, token, attributes, size, mimetype, originalName, lastModified })
55
56 return res.header['location'].split('?')[1]
57 }
58
59 async function sendChunks (options: {
60 token?: string
61 pathUploadId: string
62 size?: number
63 expectedStatus?: HttpStatusCodeType
64 contentLength?: number
65 contentRange?: string
66 contentRangeBuilder?: (start: number, chunk: any) => string
67 digestBuilder?: (chunk: any) => string
68 }) {
69 const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder, digestBuilder } = options
70
71 const size = await buildSize(defaultFixture, options.size)
72 const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
73
74 return server.videos.sendResumableChunks({
75 token,
76 path,
77 pathUploadId,
78 videoFilePath: absoluteFilePath,
79 size,
80 contentLength,
81 contentRangeBuilder,
82 digestBuilder,
83 expectedStatus
84 })
85 }
86
87 async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
88 const uploadId = uploadIdArg.replace(/^upload_id=/, '')
89
90 const subPath = join('tmp', 'resumable-uploads', `${rootId}-${uploadId}.mp4`)
91 const filePath = server.servers.buildDirectory(subPath)
92 const exists = await pathExists(filePath)
93
94 if (expectedSize === null) {
95 expect(exists).to.be.false
96 return
97 }
98
99 expect(exists).to.be.true
100
101 expect((await stat(filePath)).size).to.equal(expectedSize)
102 }
103
104 async function countResumableUploads (wait?: number) {
105 const subPath = join('tmp', 'resumable-uploads')
106 const filePath = server.servers.buildDirectory(subPath)
107 await new Promise(resolve => setTimeout(resolve, wait))
108 const files = await readdir(filePath)
109 return files.length
110 }
111
112 before(async function () {
113 this.timeout(30000)
114
115 server = await createSingleServer(1)
116 await setAccessTokensToServers([ server ])
117 await setDefaultVideoChannel([ server ])
118
119 const body = await server.users.getMyInfo()
120 rootId = body.id
121
122 {
123 userAccessToken = await server.users.generateUserAndToken('user1')
124 const { videoChannels } = await server.users.getMyInfo({ token: userAccessToken })
125 userChannelId = videoChannels[0].id
126 }
127
128 await server.users.update({ userId: rootId, videoQuota: 10_000_000 })
129 })
130
131 describe('Directory cleaning', function () {
132
133 it('Should correctly delete files after an upload', async function () {
134 const uploadId = await prepareUpload()
135 await sendChunks({ pathUploadId: uploadId })
136 await server.videos.endResumableUpload({ path, pathUploadId: uploadId })
137
138 expect(await countResumableUploads()).to.equal(0)
139 })
140
141 it('Should correctly delete corrupt files', async function () {
142 const uploadId = await prepareUpload({ size: 8 * 1024 })
143 await sendChunks({ pathUploadId: uploadId, size: 8 * 1024, expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 })
144
145 expect(await countResumableUploads(2000)).to.equal(0)
146 })
147
148 it('Should not delete files after an unfinished upload', async function () {
149 await prepareUpload()
150
151 expect(await countResumableUploads()).to.equal(2)
152 })
153
154 it('Should not delete recent uploads', async function () {
155 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
156
157 expect(await countResumableUploads()).to.equal(2)
158 })
159
160 it('Should delete old uploads', async function () {
161 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
162
163 expect(await countResumableUploads()).to.equal(0)
164 })
165 })
166
167 describe('Resumable upload and chunks', function () {
168
169 it('Should accept the same amount of chunks', async function () {
170 const uploadId = await prepareUpload()
171 await sendChunks({ pathUploadId: uploadId })
172
173 await checkFileSize(uploadId, null)
174 })
175
176 it('Should not accept more chunks than expected', async function () {
177 const uploadId = await prepareUpload({ size: 100 })
178
179 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
180 await checkFileSize(uploadId, 0)
181 })
182
183 it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
184 const uploadId = await prepareUpload({ size: 1500 })
185
186 // Content length check can be different depending on the node version
187 try {
188 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 })
189 await checkFileSize(uploadId, 0)
190 } catch {
191 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
192 await checkFileSize(uploadId, 0)
193 }
194 })
195
196 it('Should not accept more chunks than expected with an invalid content length', async function () {
197 const uploadId = await prepareUpload({ size: 500 })
198
199 const size = 1000
200
201 // Content length check seems to have changed in v16
202 const expectedStatus = process.version.startsWith('v16')
203 ? HttpStatusCode.CONFLICT_409
204 : HttpStatusCode.BAD_REQUEST_400
205
206 const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}`
207 await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size })
208 await checkFileSize(uploadId, 0)
209 })
210
211 it('Should be able to accept 2 PUT requests', async function () {
212 const uploadId = await prepareUpload()
213
214 const result1 = await sendChunks({ pathUploadId: uploadId })
215 const result2 = await sendChunks({ pathUploadId: uploadId })
216
217 expect(result1.body.video.uuid).to.exist
218 expect(result1.body.video.uuid).to.equal(result2.body.video.uuid)
219
220 expect(result1.headers['x-resumable-upload-cached']).to.not.exist
221 expect(result2.headers['x-resumable-upload-cached']).to.equal('true')
222
223 await checkFileSize(uploadId, null)
224 })
225
226 it('Should not have the same upload id with 2 different users', async function () {
227 const originalName = 'toto.mp4'
228 const lastModified = new Date().getTime()
229
230 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
231 const uploadId2 = await prepareUpload({ originalName, lastModified, channelId: userChannelId, token: userAccessToken })
232
233 expect(uploadId1).to.not.equal(uploadId2)
234 })
235
236 it('Should have the same upload id with the same user', async function () {
237 const originalName = 'toto.mp4'
238 const lastModified = new Date().getTime()
239
240 const uploadId1 = await prepareUpload({ originalName, lastModified })
241 const uploadId2 = await prepareUpload({ originalName, lastModified })
242
243 expect(uploadId1).to.equal(uploadId2)
244 })
245
246 it('Should not cache a request with 2 different users', async function () {
247 const originalName = 'toto.mp4'
248 const lastModified = new Date().getTime()
249
250 const uploadId = await prepareUpload({ originalName, lastModified, token: server.accessToken })
251
252 await sendChunks({ pathUploadId: uploadId, token: server.accessToken })
253 await sendChunks({ pathUploadId: uploadId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
254 })
255
256 it('Should not cache a request after a delete', async function () {
257 const originalName = 'toto.mp4'
258 const lastModified = new Date().getTime()
259 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
260
261 await sendChunks({ pathUploadId: uploadId1 })
262 await server.videos.endResumableUpload({ path, pathUploadId: uploadId1 })
263
264 const uploadId2 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
265 expect(uploadId1).to.equal(uploadId2)
266
267 const result2 = await sendChunks({ pathUploadId: uploadId1 })
268 expect(result2.headers['x-resumable-upload-cached']).to.not.exist
269 })
270
271 it('Should not cache after video deletion', async function () {
272 const originalName = 'toto.mp4'
273 const lastModified = new Date().getTime()
274
275 const uploadId1 = await prepareUpload({ originalName, lastModified })
276 const result1 = await sendChunks({ pathUploadId: uploadId1 })
277 await server.videos.remove({ id: result1.body.video.uuid })
278
279 const uploadId2 = await prepareUpload({ originalName, lastModified })
280 const result2 = await sendChunks({ pathUploadId: uploadId2 })
281 expect(result1.body.video.uuid).to.not.equal(result2.body.video.uuid)
282
283 expect(result2.headers['x-resumable-upload-cached']).to.not.exist
284
285 await checkFileSize(uploadId1, null)
286 await checkFileSize(uploadId2, null)
287 })
288
289 it('Should refuse an invalid digest', async function () {
290 const uploadId = await prepareUpload({ token: server.accessToken })
291
292 await sendChunks({
293 pathUploadId: uploadId,
294 token: server.accessToken,
295 digestBuilder: () => 'sha=' + 'a'.repeat(40),
296 expectedStatus: 460 as any
297 })
298 })
299
300 it('Should accept an appropriate digest', async function () {
301 const uploadId = await prepareUpload({ token: server.accessToken })
302
303 await sendChunks({
304 pathUploadId: uploadId,
305 token: server.accessToken,
306 digestBuilder: (chunk: Buffer) => {
307 return 'sha1=' + sha1(chunk, 'base64')
308 }
309 })
310 })
311 })
312
313 after(async function () {
314 await cleanupTests([ server ])
315 })
316})
diff --git a/packages/tests/src/api/videos/single-server.ts b/packages/tests/src/api/videos/single-server.ts
new file mode 100644
index 000000000..b87192a57
--- /dev/null
+++ b/packages/tests/src/api/videos/single-server.ts
@@ -0,0 +1,461 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { Video, VideoPrivacy } from '@peertube/peertube-models'
6import { checkVideoFilesWereRemoved, completeVideoCheck } from '@tests/shared/videos.js'
7import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
8import {
9 cleanupTests,
10 createSingleServer,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultAccountAvatar,
14 setDefaultChannelAvatar,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17
18describe('Test a single server', function () {
19
20 function runSuite (mode: 'legacy' | 'resumable') {
21 let server: PeerTubeServer = null
22 let videoId: number | string
23 let videoId2: string
24 let videoUUID = ''
25 let videosListBase: any[] = null
26
27 const getCheckAttributes = () => ({
28 name: 'my super name',
29 category: 2,
30 licence: 6,
31 language: 'zh',
32 nsfw: true,
33 description: 'my super description',
34 support: 'my super support text',
35 account: {
36 name: 'root',
37 host: server.host
38 },
39 isLocal: true,
40 duration: 5,
41 tags: [ 'tag1', 'tag2', 'tag3' ],
42 privacy: VideoPrivacy.PUBLIC,
43 commentsEnabled: true,
44 downloadEnabled: true,
45 channel: {
46 displayName: 'Main root channel',
47 name: 'root_channel',
48 description: '',
49 isLocal: true
50 },
51 fixture: 'video_short.webm',
52 files: [
53 {
54 resolution: 720,
55 size: 218910
56 }
57 ]
58 })
59
60 const updateCheckAttributes = () => ({
61 name: 'my super video updated',
62 category: 4,
63 licence: 2,
64 language: 'ar',
65 nsfw: false,
66 description: 'my super description updated',
67 support: 'my super support text updated',
68 account: {
69 name: 'root',
70 host: server.host
71 },
72 isLocal: true,
73 tags: [ 'tagup1', 'tagup2' ],
74 privacy: VideoPrivacy.PUBLIC,
75 duration: 5,
76 commentsEnabled: false,
77 downloadEnabled: false,
78 channel: {
79 name: 'root_channel',
80 displayName: 'Main root channel',
81 description: '',
82 isLocal: true
83 },
84 fixture: 'video_short3.webm',
85 files: [
86 {
87 resolution: 720,
88 size: 292677
89 }
90 ]
91 })
92
93 before(async function () {
94 this.timeout(30000)
95
96 server = await createSingleServer(1, {})
97
98 await setAccessTokensToServers([ server ])
99 await setDefaultChannelAvatar(server)
100 await setDefaultAccountAvatar(server)
101 })
102
103 it('Should list video categories', async function () {
104 const categories = await server.videos.getCategories()
105 expect(Object.keys(categories)).to.have.length.above(10)
106
107 expect(categories[11]).to.equal('News & Politics')
108 })
109
110 it('Should list video licences', async function () {
111 const licences = await server.videos.getLicences()
112 expect(Object.keys(licences)).to.have.length.above(5)
113
114 expect(licences[3]).to.equal('Attribution - No Derivatives')
115 })
116
117 it('Should list video languages', async function () {
118 const languages = await server.videos.getLanguages()
119 expect(Object.keys(languages)).to.have.length.above(5)
120
121 expect(languages['ru']).to.equal('Russian')
122 })
123
124 it('Should list video privacies', async function () {
125 const privacies = await server.videos.getPrivacies()
126 expect(Object.keys(privacies)).to.have.length.at.least(3)
127
128 expect(privacies[3]).to.equal('Private')
129 })
130
131 it('Should not have videos', async function () {
132 const { data, total } = await server.videos.list()
133
134 expect(total).to.equal(0)
135 expect(data).to.be.an('array')
136 expect(data.length).to.equal(0)
137 })
138
139 it('Should upload the video', async function () {
140 const attributes = {
141 name: 'my super name',
142 category: 2,
143 nsfw: true,
144 licence: 6,
145 tags: [ 'tag1', 'tag2', 'tag3' ]
146 }
147 const video = await server.videos.upload({ attributes, mode })
148 expect(video).to.not.be.undefined
149 expect(video.id).to.equal(1)
150 expect(video.uuid).to.have.length.above(5)
151
152 videoId = video.id
153 videoUUID = video.uuid
154 })
155
156 it('Should get and seed the uploaded video', async function () {
157 this.timeout(5000)
158
159 const { data, total } = await server.videos.list()
160
161 expect(total).to.equal(1)
162 expect(data).to.be.an('array')
163 expect(data.length).to.equal(1)
164
165 const video = data[0]
166 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
167 })
168
169 it('Should get the video by UUID', async function () {
170 this.timeout(5000)
171
172 const video = await server.videos.get({ id: videoUUID })
173 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
174 })
175
176 it('Should have the views updated', async function () {
177 this.timeout(20000)
178
179 await server.views.simulateView({ id: videoId })
180 await server.views.simulateView({ id: videoId })
181 await server.views.simulateView({ id: videoId })
182
183 await wait(1500)
184
185 await server.views.simulateView({ id: videoId })
186 await server.views.simulateView({ id: videoId })
187
188 await wait(1500)
189
190 await server.views.simulateView({ id: videoId })
191 await server.views.simulateView({ id: videoId })
192
193 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
194
195 const video = await server.videos.get({ id: videoId })
196 expect(video.views).to.equal(3)
197 })
198
199 it('Should remove the video', async function () {
200 const video = await server.videos.get({ id: videoId })
201 await server.videos.remove({ id: videoId })
202
203 await checkVideoFilesWereRemoved({ video, server })
204 })
205
206 it('Should not have videos', async function () {
207 const { total, data } = await server.videos.list()
208
209 expect(total).to.equal(0)
210 expect(data).to.be.an('array')
211 expect(data).to.have.lengthOf(0)
212 })
213
214 it('Should upload 6 videos', async function () {
215 this.timeout(120000)
216
217 const videos = new Set([
218 'video_short.mp4', 'video_short.ogv', 'video_short.webm',
219 'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
220 ])
221
222 for (const video of videos) {
223 const attributes = {
224 name: video + ' name',
225 description: video + ' description',
226 category: 2,
227 licence: 1,
228 language: 'en',
229 nsfw: true,
230 tags: [ 'tag1', 'tag2', 'tag3' ],
231 fixture: video
232 }
233
234 await server.videos.upload({ attributes, mode })
235 }
236 })
237
238 it('Should have the correct durations', async function () {
239 const { total, data } = await server.videos.list()
240
241 expect(total).to.equal(6)
242 expect(data).to.be.an('array')
243 expect(data).to.have.lengthOf(6)
244
245 const videosByName: { [ name: string ]: Video } = {}
246 data.forEach(v => { videosByName[v.name] = v })
247
248 expect(videosByName['video_short.mp4 name'].duration).to.equal(5)
249 expect(videosByName['video_short.ogv name'].duration).to.equal(5)
250 expect(videosByName['video_short.webm name'].duration).to.equal(5)
251 expect(videosByName['video_short1.webm name'].duration).to.equal(10)
252 expect(videosByName['video_short2.webm name'].duration).to.equal(5)
253 expect(videosByName['video_short3.webm name'].duration).to.equal(5)
254 })
255
256 it('Should have the correct thumbnails', async function () {
257 const { data } = await server.videos.list()
258
259 // For the next test
260 videosListBase = data
261
262 for (const video of data) {
263 const videoName = video.name.replace(' name', '')
264 await testImageGeneratedByFFmpeg(server.url, videoName, video.thumbnailPath)
265 }
266 })
267
268 it('Should list only the two first videos', async function () {
269 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: 'name' })
270
271 expect(total).to.equal(6)
272 expect(data.length).to.equal(2)
273 expect(data[0].name).to.equal(videosListBase[0].name)
274 expect(data[1].name).to.equal(videosListBase[1].name)
275 })
276
277 it('Should list only the next three videos', async function () {
278 const { total, data } = await server.videos.list({ start: 2, count: 3, sort: 'name' })
279
280 expect(total).to.equal(6)
281 expect(data.length).to.equal(3)
282 expect(data[0].name).to.equal(videosListBase[2].name)
283 expect(data[1].name).to.equal(videosListBase[3].name)
284 expect(data[2].name).to.equal(videosListBase[4].name)
285 })
286
287 it('Should list the last video', async function () {
288 const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name' })
289
290 expect(total).to.equal(6)
291 expect(data.length).to.equal(1)
292 expect(data[0].name).to.equal(videosListBase[5].name)
293 })
294
295 it('Should not have the total field', async function () {
296 const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name', skipCount: true })
297
298 expect(total).to.not.exist
299 expect(data.length).to.equal(1)
300 expect(data[0].name).to.equal(videosListBase[5].name)
301 })
302
303 it('Should list and sort by name in descending order', async function () {
304 const { total, data } = await server.videos.list({ sort: '-name' })
305
306 expect(total).to.equal(6)
307 expect(data.length).to.equal(6)
308 expect(data[0].name).to.equal('video_short.webm name')
309 expect(data[1].name).to.equal('video_short.ogv name')
310 expect(data[2].name).to.equal('video_short.mp4 name')
311 expect(data[3].name).to.equal('video_short3.webm name')
312 expect(data[4].name).to.equal('video_short2.webm name')
313 expect(data[5].name).to.equal('video_short1.webm name')
314
315 videoId = data[3].uuid
316 videoId2 = data[5].uuid
317 })
318
319 it('Should list and sort by trending in descending order', async function () {
320 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-trending' })
321
322 expect(total).to.equal(6)
323 expect(data.length).to.equal(2)
324 })
325
326 it('Should list and sort by hotness in descending order', async function () {
327 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-hot' })
328
329 expect(total).to.equal(6)
330 expect(data.length).to.equal(2)
331 })
332
333 it('Should list and sort by best in descending order', async function () {
334 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-best' })
335
336 expect(total).to.equal(6)
337 expect(data.length).to.equal(2)
338 })
339
340 it('Should update a video', async function () {
341 const attributes = {
342 name: 'my super video updated',
343 category: 4,
344 licence: 2,
345 language: 'ar',
346 nsfw: false,
347 description: 'my super description updated',
348 commentsEnabled: false,
349 downloadEnabled: false,
350 tags: [ 'tagup1', 'tagup2' ]
351 }
352 await server.videos.update({ id: videoId, attributes })
353 })
354
355 it('Should have the video updated', async function () {
356 this.timeout(60000)
357
358 await waitJobs([ server ])
359
360 const video = await server.videos.get({ id: videoId })
361
362 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: updateCheckAttributes() })
363 })
364
365 it('Should update only the tags of a video', async function () {
366 const attributes = {
367 tags: [ 'supertag', 'tag1', 'tag2' ]
368 }
369 await server.videos.update({ id: videoId, attributes })
370
371 const video = await server.videos.get({ id: videoId })
372
373 await completeVideoCheck({
374 server,
375 originServer: server,
376 videoUUID: video.uuid,
377 attributes: Object.assign(updateCheckAttributes(), attributes)
378 })
379 })
380
381 it('Should update only the description of a video', async function () {
382 const attributes = {
383 description: 'hello everybody'
384 }
385 await server.videos.update({ id: videoId, attributes })
386
387 const video = await server.videos.get({ id: videoId })
388
389 await completeVideoCheck({
390 server,
391 originServer: server,
392 videoUUID: video.uuid,
393 attributes: Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
394 })
395 })
396
397 it('Should like a video', async function () {
398 await server.videos.rate({ id: videoId, rating: 'like' })
399
400 const video = await server.videos.get({ id: videoId })
401
402 expect(video.likes).to.equal(1)
403 expect(video.dislikes).to.equal(0)
404 })
405
406 it('Should dislike the same video', async function () {
407 await server.videos.rate({ id: videoId, rating: 'dislike' })
408
409 const video = await server.videos.get({ id: videoId })
410
411 expect(video.likes).to.equal(0)
412 expect(video.dislikes).to.equal(1)
413 })
414
415 it('Should sort by originallyPublishedAt', async function () {
416 {
417 const now = new Date()
418 const attributes = { originallyPublishedAt: now.toISOString() }
419 await server.videos.update({ id: videoId, attributes })
420
421 const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
422 const names = data.map(v => v.name)
423
424 expect(names[0]).to.equal('my super video updated')
425 expect(names[1]).to.equal('video_short2.webm name')
426 expect(names[2]).to.equal('video_short1.webm name')
427 expect(names[3]).to.equal('video_short.webm name')
428 expect(names[4]).to.equal('video_short.ogv name')
429 expect(names[5]).to.equal('video_short.mp4 name')
430 }
431
432 {
433 const now = new Date()
434 const attributes = { originallyPublishedAt: now.toISOString() }
435 await server.videos.update({ id: videoId2, attributes })
436
437 const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
438 const names = data.map(v => v.name)
439
440 expect(names[0]).to.equal('video_short1.webm name')
441 expect(names[1]).to.equal('my super video updated')
442 expect(names[2]).to.equal('video_short2.webm name')
443 expect(names[3]).to.equal('video_short.webm name')
444 expect(names[4]).to.equal('video_short.ogv name')
445 expect(names[5]).to.equal('video_short.mp4 name')
446 }
447 })
448
449 after(async function () {
450 await cleanupTests([ server ])
451 })
452 }
453
454 describe('Legacy upload', function () {
455 runSuite('legacy')
456 })
457
458 describe('Resumable upload', function () {
459 runSuite('resumable')
460 })
461})
diff --git a/packages/tests/src/api/videos/video-captions.ts b/packages/tests/src/api/videos/video-captions.ts
new file mode 100644
index 000000000..027022549
--- /dev/null
+++ b/packages/tests/src/api/videos/video-captions.ts
@@ -0,0 +1,189 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13import { testCaptionFile } from '@tests/shared/captions.js'
14import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
15
16describe('Test video captions', function () {
17 const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
18
19 let servers: PeerTubeServer[]
20 let videoUUID: string
21
22 before(async function () {
23 this.timeout(60000)
24
25 servers = await createMultipleServers(2)
26
27 await setAccessTokensToServers(servers)
28 await doubleFollow(servers[0], servers[1])
29
30 await waitJobs(servers)
31
32 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video name' } })
33 videoUUID = uuid
34
35 await waitJobs(servers)
36 })
37
38 it('Should list the captions and return an empty list', async function () {
39 for (const server of servers) {
40 const body = await server.captions.list({ videoId: videoUUID })
41 expect(body.total).to.equal(0)
42 expect(body.data).to.have.lengthOf(0)
43 }
44 })
45
46 it('Should create two new captions', async function () {
47 this.timeout(30000)
48
49 await servers[0].captions.add({
50 language: 'ar',
51 videoId: videoUUID,
52 fixture: 'subtitle-good1.vtt'
53 })
54
55 await servers[0].captions.add({
56 language: 'zh',
57 videoId: videoUUID,
58 fixture: 'subtitle-good2.vtt',
59 mimeType: 'application/octet-stream'
60 })
61
62 await waitJobs(servers)
63 })
64
65 it('Should list these uploaded captions', async function () {
66 for (const server of servers) {
67 const body = await server.captions.list({ videoId: videoUUID })
68 expect(body.total).to.equal(2)
69 expect(body.data).to.have.lengthOf(2)
70
71 const caption1 = body.data[0]
72 expect(caption1.language.id).to.equal('ar')
73 expect(caption1.language.label).to.equal('Arabic')
74 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
75 await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.')
76
77 const caption2 = body.data[1]
78 expect(caption2.language.id).to.equal('zh')
79 expect(caption2.language.label).to.equal('Chinese')
80 expect(caption2.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
81 await testCaptionFile(server.url, caption2.captionPath, 'Subtitle good 2.')
82 }
83 })
84
85 it('Should replace an existing caption', async function () {
86 this.timeout(30000)
87
88 await servers[0].captions.add({
89 language: 'ar',
90 videoId: videoUUID,
91 fixture: 'subtitle-good2.vtt'
92 })
93
94 await waitJobs(servers)
95 })
96
97 it('Should have this caption updated', async function () {
98 for (const server of servers) {
99 const body = await server.captions.list({ videoId: videoUUID })
100 expect(body.total).to.equal(2)
101 expect(body.data).to.have.lengthOf(2)
102
103 const caption1 = body.data[0]
104 expect(caption1.language.id).to.equal('ar')
105 expect(caption1.language.label).to.equal('Arabic')
106 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
107 await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 2.')
108 }
109 })
110
111 it('Should replace an existing caption with a srt file and convert it', async function () {
112 this.timeout(30000)
113
114 await servers[0].captions.add({
115 language: 'ar',
116 videoId: videoUUID,
117 fixture: 'subtitle-good.srt'
118 })
119
120 await waitJobs(servers)
121
122 // Cache invalidation
123 await wait(3000)
124 })
125
126 it('Should have this caption updated and converted', async function () {
127 for (const server of servers) {
128 const body = await server.captions.list({ videoId: videoUUID })
129 expect(body.total).to.equal(2)
130 expect(body.data).to.have.lengthOf(2)
131
132 const caption1 = body.data[0]
133 expect(caption1.language.id).to.equal('ar')
134 expect(caption1.language.label).to.equal('Arabic')
135 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
136
137 const expected = 'WEBVTT FILE\r\n' +
138 '\r\n' +
139 '1\r\n' +
140 '00:00:01.600 --> 00:00:04.200\r\n' +
141 'English (US)\r\n' +
142 '\r\n' +
143 '2\r\n' +
144 '00:00:05.900 --> 00:00:07.999\r\n' +
145 'This is a subtitle in American English\r\n' +
146 '\r\n' +
147 '3\r\n' +
148 '00:00:10.000 --> 00:00:14.000\r\n' +
149 'Adding subtitles is very easy to do\r\n'
150 await testCaptionFile(server.url, caption1.captionPath, expected)
151 }
152 })
153
154 it('Should remove one caption', async function () {
155 this.timeout(30000)
156
157 await servers[0].captions.delete({ videoId: videoUUID, language: 'ar' })
158
159 await waitJobs(servers)
160 })
161
162 it('Should only list the caption that was not deleted', async function () {
163 for (const server of servers) {
164 const body = await server.captions.list({ videoId: videoUUID })
165 expect(body.total).to.equal(1)
166 expect(body.data).to.have.lengthOf(1)
167
168 const caption = body.data[0]
169
170 expect(caption.language.id).to.equal('zh')
171 expect(caption.language.label).to.equal('Chinese')
172 expect(caption.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
173 await testCaptionFile(server.url, caption.captionPath, 'Subtitle good 2.')
174 }
175 })
176
177 it('Should remove the video, and thus all video captions', async function () {
178 const video = await servers[0].videos.get({ id: videoUUID })
179 const { data: captions } = await servers[0].captions.list({ videoId: videoUUID })
180
181 await servers[0].videos.remove({ id: videoUUID })
182
183 await checkVideoFilesWereRemoved({ server: servers[0], video, captions })
184 })
185
186 after(async function () {
187 await cleanupTests(servers)
188 })
189})
diff --git a/packages/tests/src/api/videos/video-change-ownership.ts b/packages/tests/src/api/videos/video-change-ownership.ts
new file mode 100644
index 000000000..717c37469
--- /dev/null
+++ b/packages/tests/src/api/videos/video-change-ownership.ts
@@ -0,0 +1,314 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 ChangeOwnershipCommand,
6 cleanupTests,
7 createMultipleServers,
8 createSingleServer,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
16
17describe('Test video change ownership - nominal', function () {
18 let servers: PeerTubeServer[] = []
19
20 const firstUser = 'first'
21 const secondUser = 'second'
22
23 let firstUserToken = ''
24 let firstUserChannelId: number
25
26 let secondUserToken = ''
27 let secondUserChannelId: number
28
29 let lastRequestId: number
30
31 let liveId: number
32
33 let command: ChangeOwnershipCommand
34
35 before(async function () {
36 this.timeout(240000)
37
38 servers = await createMultipleServers(2)
39 await setAccessTokensToServers(servers)
40 await setDefaultVideoChannel(servers)
41
42 await servers[0].config.updateCustomSubConfig({
43 newConfig: {
44 transcoding: {
45 enabled: false
46 },
47 live: {
48 enabled: true
49 }
50 }
51 })
52
53 firstUserToken = await servers[0].users.generateUserAndToken(firstUser)
54 secondUserToken = await servers[0].users.generateUserAndToken(secondUser)
55
56 {
57 const { videoChannels } = await servers[0].users.getMyInfo({ token: firstUserToken })
58 firstUserChannelId = videoChannels[0].id
59 }
60
61 {
62 const { videoChannels } = await servers[0].users.getMyInfo({ token: secondUserToken })
63 secondUserChannelId = videoChannels[0].id
64 }
65
66 {
67 const attributes = {
68 name: 'my super name',
69 description: 'my super description'
70 }
71 const { id } = await servers[0].videos.upload({ token: firstUserToken, attributes })
72
73 servers[0].store.videoCreated = await servers[0].videos.get({ id })
74 }
75
76 {
77 const attributes = { name: 'live', channelId: firstUserChannelId, privacy: VideoPrivacy.PUBLIC }
78 const video = await servers[0].live.create({ token: firstUserToken, fields: attributes })
79
80 liveId = video.id
81 }
82
83 command = servers[0].changeOwnership
84
85 await doubleFollow(servers[0], servers[1])
86 })
87
88 it('Should not have video change ownership', async function () {
89 {
90 const body = await command.list({ token: firstUserToken })
91
92 expect(body.total).to.equal(0)
93 expect(body.data).to.be.an('array')
94 expect(body.data.length).to.equal(0)
95 }
96
97 {
98 const body = await command.list({ token: secondUserToken })
99
100 expect(body.total).to.equal(0)
101 expect(body.data).to.be.an('array')
102 expect(body.data.length).to.equal(0)
103 }
104 })
105
106 it('Should send a request to change ownership of a video', async function () {
107 this.timeout(15000)
108
109 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
110 })
111
112 it('Should only return a request to change ownership for the second user', async function () {
113 {
114 const body = await command.list({ token: firstUserToken })
115
116 expect(body.total).to.equal(0)
117 expect(body.data).to.be.an('array')
118 expect(body.data.length).to.equal(0)
119 }
120
121 {
122 const body = await command.list({ token: secondUserToken })
123
124 expect(body.total).to.equal(1)
125 expect(body.data).to.be.an('array')
126 expect(body.data.length).to.equal(1)
127
128 lastRequestId = body.data[0].id
129 }
130 })
131
132 it('Should accept the same change ownership request without crashing', async function () {
133 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
134 })
135
136 it('Should not create multiple change ownership requests while one is waiting', async function () {
137 const body = await command.list({ token: secondUserToken })
138
139 expect(body.total).to.equal(1)
140 expect(body.data).to.be.an('array')
141 expect(body.data.length).to.equal(1)
142 })
143
144 it('Should not be possible to refuse the change of ownership from first user', async function () {
145 await command.refuse({ token: firstUserToken, ownershipId: lastRequestId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
146 })
147
148 it('Should be possible to refuse the change of ownership from second user', async function () {
149 await command.refuse({ token: secondUserToken, ownershipId: lastRequestId })
150 })
151
152 it('Should send a new request to change ownership of a video', async function () {
153 this.timeout(15000)
154
155 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
156 })
157
158 it('Should return two requests to change ownership for the second user', async function () {
159 {
160 const body = await command.list({ token: firstUserToken })
161
162 expect(body.total).to.equal(0)
163 expect(body.data).to.be.an('array')
164 expect(body.data.length).to.equal(0)
165 }
166
167 {
168 const body = await command.list({ token: secondUserToken })
169
170 expect(body.total).to.equal(2)
171 expect(body.data).to.be.an('array')
172 expect(body.data.length).to.equal(2)
173
174 lastRequestId = body.data[0].id
175 }
176 })
177
178 it('Should not be possible to accept the change of ownership from first user', async function () {
179 await command.accept({
180 token: firstUserToken,
181 ownershipId: lastRequestId,
182 channelId: secondUserChannelId,
183 expectedStatus: HttpStatusCode.FORBIDDEN_403
184 })
185 })
186
187 it('Should be possible to accept the change of ownership from second user', async function () {
188 await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
189
190 await waitJobs(servers)
191 })
192
193 it('Should have the channel of the video updated', async function () {
194 for (const server of servers) {
195 const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
196
197 expect(video.name).to.equal('my super name')
198 expect(video.channel.displayName).to.equal('Main second channel')
199 expect(video.channel.name).to.equal('second_channel')
200 }
201 })
202
203 it('Should send a request to change ownership of a live', async function () {
204 this.timeout(15000)
205
206 await command.create({ token: firstUserToken, videoId: liveId, username: secondUser })
207
208 const body = await command.list({ token: secondUserToken })
209
210 expect(body.total).to.equal(3)
211 expect(body.data.length).to.equal(3)
212
213 lastRequestId = body.data[0].id
214 })
215
216 it('Should accept a live ownership change', async function () {
217 this.timeout(20000)
218
219 await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
220
221 await waitJobs(servers)
222
223 for (const server of servers) {
224 const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
225
226 expect(video.name).to.equal('my super name')
227 expect(video.channel.displayName).to.equal('Main second channel')
228 expect(video.channel.name).to.equal('second_channel')
229 }
230 })
231
232 after(async function () {
233 await cleanupTests(servers)
234 })
235})
236
237describe('Test video change ownership - quota too small', function () {
238 let server: PeerTubeServer
239 const firstUser = 'first'
240 const secondUser = 'second'
241
242 let firstUserToken = ''
243 let secondUserToken = ''
244 let lastRequestId: number
245
246 before(async function () {
247 this.timeout(50000)
248
249 // Run one server
250 server = await createSingleServer(1)
251 await setAccessTokensToServers([ server ])
252
253 await server.users.create({ username: secondUser, videoQuota: 10 })
254
255 firstUserToken = await server.users.generateUserAndToken(firstUser)
256 secondUserToken = await server.login.getAccessToken(secondUser)
257
258 // Upload some videos on the server
259 const attributes = {
260 name: 'my super name',
261 description: 'my super description'
262 }
263 await server.videos.upload({ token: firstUserToken, attributes })
264
265 await waitJobs(server)
266
267 const { data } = await server.videos.list()
268 expect(data.length).to.equal(1)
269
270 server.store.videoCreated = data.find(video => video.name === 'my super name')
271 })
272
273 it('Should send a request to change ownership of a video', async function () {
274 this.timeout(15000)
275
276 await server.changeOwnership.create({ token: firstUserToken, videoId: server.store.videoCreated.id, username: secondUser })
277 })
278
279 it('Should only return a request to change ownership for the second user', async function () {
280 {
281 const body = await server.changeOwnership.list({ token: firstUserToken })
282
283 expect(body.total).to.equal(0)
284 expect(body.data).to.be.an('array')
285 expect(body.data.length).to.equal(0)
286 }
287
288 {
289 const body = await server.changeOwnership.list({ token: secondUserToken })
290
291 expect(body.total).to.equal(1)
292 expect(body.data).to.be.an('array')
293 expect(body.data.length).to.equal(1)
294
295 lastRequestId = body.data[0].id
296 }
297 })
298
299 it('Should not be possible to accept the change of ownership from second user because of exceeded quota', async function () {
300 const { videoChannels } = await server.users.getMyInfo({ token: secondUserToken })
301 const channelId = videoChannels[0].id
302
303 await server.changeOwnership.accept({
304 token: secondUserToken,
305 ownershipId: lastRequestId,
306 channelId,
307 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
308 })
309 })
310
311 after(async function () {
312 await cleanupTests([ server ])
313 })
314})
diff --git a/packages/tests/src/api/videos/video-channel-syncs.ts b/packages/tests/src/api/videos/video-channel-syncs.ts
new file mode 100644
index 000000000..54212bcb5
--- /dev/null
+++ b/packages/tests/src/api/videos/video-channel-syncs.ts
@@ -0,0 +1,321 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
5import { VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 getServerImportConfig,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 setDefaultVideoChannel,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17import { SQLCommand } from '@tests/shared/sql-command.js'
18import { FIXTURE_URLS } from '@tests/shared/tests.js'
19
20describe('Test channel synchronizations', function () {
21 if (areHttpImportTestsDisabled()) return
22
23 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
24
25 describe('Sync using ' + mode, function () {
26 let servers: PeerTubeServer[]
27 let sqlCommands: SQLCommand[] = []
28
29 let startTestDate: Date
30
31 let rootChannelSyncId: number
32 const userInfo = {
33 accessToken: '',
34 username: 'user1',
35 channelName: 'user1_channel',
36 channelId: -1,
37 syncId: -1
38 }
39
40 async function changeDateForSync (channelSyncId: number, newDate: string) {
41 await sqlCommands[0].updateQuery(
42 `UPDATE "videoChannelSync" ` +
43 `SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
44 `WHERE id=${channelSyncId}`
45 )
46 }
47
48 async function listAllVideosOfChannel (channelName: string) {
49 return servers[0].videos.listByChannel({
50 handle: channelName,
51 include: VideoInclude.NOT_PUBLISHED_STATE
52 })
53 }
54
55 async function forceSyncAll (videoChannelSyncId: number, fromDate = '1970-01-01') {
56 await changeDateForSync(videoChannelSyncId, fromDate)
57
58 await servers[0].debug.sendCommand({
59 body: {
60 command: 'process-video-channel-sync-latest'
61 }
62 })
63
64 await waitJobs(servers)
65 }
66
67 before(async function () {
68 this.timeout(240_000)
69
70 startTestDate = new Date()
71
72 servers = await createMultipleServers(2, getServerImportConfig(mode))
73
74 await setAccessTokensToServers(servers)
75 await setDefaultVideoChannel(servers)
76 await setDefaultChannelAvatar(servers)
77 await setDefaultAccountAvatar(servers)
78
79 await servers[0].config.enableChannelSync()
80
81 {
82 userInfo.accessToken = await servers[0].users.generateUserAndToken(userInfo.username)
83
84 const { videoChannels } = await servers[0].users.getMyInfo({ token: userInfo.accessToken })
85 userInfo.channelId = videoChannels[0].id
86 }
87
88 sqlCommands = servers.map(s => new SQLCommand(s))
89 })
90
91 it('Should fetch the latest channel videos of a remote channel', async function () {
92 this.timeout(120_000)
93
94 {
95 const { video } = await servers[0].imports.importVideo({
96 attributes: {
97 channelId: servers[0].store.channel.id,
98 privacy: VideoPrivacy.PUBLIC,
99 targetUrl: FIXTURE_URLS.youtube
100 }
101 })
102
103 expect(video.name).to.equal('small video - youtube')
104 expect(video.waitTranscoding).to.be.true
105
106 const { total } = await listAllVideosOfChannel('root_channel')
107 expect(total).to.equal(1)
108 }
109
110 const { videoChannelSync } = await servers[0].channelSyncs.create({
111 attributes: {
112 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
113 videoChannelId: servers[0].store.channel.id
114 }
115 })
116 rootChannelSyncId = videoChannelSync.id
117
118 await forceSyncAll(rootChannelSyncId)
119
120 {
121 const { total, data } = await listAllVideosOfChannel('root_channel')
122 expect(total).to.equal(2)
123 expect(data[0].name).to.equal('test')
124 expect(data[0].waitTranscoding).to.be.true
125 }
126 })
127
128 it('Should add another synchronization', async function () {
129 const externalChannelUrl = FIXTURE_URLS.youtubeChannel + '?foo=bar'
130
131 const { videoChannelSync } = await servers[0].channelSyncs.create({
132 attributes: {
133 externalChannelUrl,
134 videoChannelId: servers[0].store.channel.id
135 }
136 })
137
138 expect(videoChannelSync.externalChannelUrl).to.equal(externalChannelUrl)
139 expect(videoChannelSync.channel.id).to.equal(servers[0].store.channel.id)
140 expect(videoChannelSync.channel.name).to.equal('root_channel')
141 expect(videoChannelSync.state.id).to.equal(VideoChannelSyncState.WAITING_FIRST_RUN)
142 expect(new Date(videoChannelSync.createdAt)).to.be.above(startTestDate).and.to.be.at.most(new Date())
143 })
144
145 it('Should add a synchronization for another user', async function () {
146 const { videoChannelSync } = await servers[0].channelSyncs.create({
147 attributes: {
148 externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
149 videoChannelId: userInfo.channelId
150 },
151 token: userInfo.accessToken
152 })
153 userInfo.syncId = videoChannelSync.id
154 })
155
156 it('Should not import a channel if not asked', async function () {
157 await waitJobs(servers)
158
159 const { data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
160
161 expect(data[0].state).to.contain({
162 id: VideoChannelSyncState.WAITING_FIRST_RUN,
163 label: 'Waiting first run'
164 })
165 })
166
167 it('Should only fetch the videos newer than the creation date', async function () {
168 this.timeout(120_000)
169
170 await forceSyncAll(userInfo.syncId, '2019-03-01')
171
172 const { data, total } = await listAllVideosOfChannel(userInfo.channelName)
173
174 expect(total).to.equal(1)
175 expect(data[0].name).to.equal('test')
176 })
177
178 it('Should list channel synchronizations', async function () {
179 // Root
180 {
181 const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: 'root' })
182 expect(total).to.equal(2)
183
184 expect(data[0]).to.deep.contain({
185 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
186 state: {
187 id: VideoChannelSyncState.SYNCED,
188 label: 'Synchronized'
189 }
190 })
191
192 expect(new Date(data[0].lastSyncAt)).to.be.greaterThan(startTestDate)
193
194 expect(data[0].channel).to.contain({ id: servers[0].store.channel.id })
195 expect(data[1]).to.contain({ externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?foo=bar' })
196 }
197
198 // User
199 {
200 const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
201 expect(total).to.equal(1)
202 expect(data[0]).to.deep.contain({
203 externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
204 state: {
205 id: VideoChannelSyncState.SYNCED,
206 label: 'Synchronized'
207 }
208 })
209 }
210 })
211
212 it('Should list imports of a channel synchronization', async function () {
213 const { total, data } = await servers[0].imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId })
214
215 expect(total).to.equal(1)
216 expect(data).to.have.lengthOf(1)
217 expect(data[0].video.name).to.equal('test')
218 })
219
220 it('Should remove user\'s channel synchronizations', async function () {
221 await servers[0].channelSyncs.delete({ channelSyncId: userInfo.syncId })
222
223 const { total } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
224 expect(total).to.equal(0)
225 })
226
227 // FIXME: youtube-dl/yt-dlp doesn't work when speicifying a port after the hostname
228 // it('Should import a remote PeerTube channel', async function () {
229 // this.timeout(240_000)
230
231 // await servers[1].videos.quickUpload({ name: 'remote 1' })
232 // await waitJobs(servers)
233
234 // const { videoChannelSync } = await servers[0].channelSyncs.create({
235 // attributes: {
236 // externalChannelUrl: servers[1].url + '/c/root_channel',
237 // videoChannelId: userInfo.channelId
238 // },
239 // token: userInfo.accessToken
240 // })
241 // await servers[0].channels.importVideos({
242 // channelName: userInfo.channelName,
243 // externalChannelUrl: servers[1].url + '/c/root_channel',
244 // videoChannelSyncId: videoChannelSync.id,
245 // token: userInfo.accessToken
246 // })
247
248 // await waitJobs(servers)
249
250 // const { data, total } = await servers[0].videos.listByChannel({
251 // handle: userInfo.channelName,
252 // include: VideoInclude.NOT_PUBLISHED_STATE
253 // })
254
255 // expect(total).to.equal(2)
256 // expect(data[0].name).to.equal('remote 1')
257 // })
258
259 // it('Should keep synced a remote PeerTube channel', async function () {
260 // this.timeout(240_000)
261
262 // await servers[1].videos.quickUpload({ name: 'remote 2' })
263 // await waitJobs(servers)
264
265 // await servers[0].debug.sendCommand({
266 // body: {
267 // command: 'process-video-channel-sync-latest'
268 // }
269 // })
270
271 // await waitJobs(servers)
272
273 // const { data, total } = await servers[0].videos.listByChannel({
274 // handle: userInfo.channelName,
275 // include: VideoInclude.NOT_PUBLISHED_STATE
276 // })
277 // expect(total).to.equal(2)
278 // expect(data[0].name).to.equal('remote 2')
279 // })
280
281 it('Should fetch the latest videos of a youtube playlist', async function () {
282 this.timeout(120_000)
283
284 const { id: channelId } = await servers[0].channels.create({
285 attributes: {
286 name: 'channel2'
287 }
288 })
289
290 const { videoChannelSync: { id: videoChannelSyncId } } = await servers[0].channelSyncs.create({
291 attributes: {
292 externalChannelUrl: FIXTURE_URLS.youtubePlaylist,
293 videoChannelId: channelId
294 }
295 })
296
297 await forceSyncAll(videoChannelSyncId)
298
299 {
300
301 const { total, data } = await listAllVideosOfChannel('channel2')
302 expect(total).to.equal(2)
303 expect(data[0].name).to.equal('test')
304 expect(data[1].name).to.equal('small video - youtube')
305 }
306 })
307
308 after(async function () {
309 for (const sqlCommand of sqlCommands) {
310 await sqlCommand.cleanup()
311 }
312
313 await cleanupTests(servers)
314 })
315 })
316 }
317
318 // FIXME: suite is broken with youtube-dl
319 // runSuite('youtube-dl')
320 runSuite('yt-dlp')
321})
diff --git a/packages/tests/src/api/videos/video-channels.ts b/packages/tests/src/api/videos/video-channels.ts
new file mode 100644
index 000000000..64b1b9315
--- /dev/null
+++ b/packages/tests/src/api/videos/video-channels.ts
@@ -0,0 +1,556 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename } from 'path'
5import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/server/initializers/constants.js'
6import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js'
7import { SQLCommand } from '@tests/shared/sql-command.js'
8import { wait } from '@peertube/peertube-core-utils'
9import { ActorImageType, User, VideoChannel } from '@peertube/peertube-models'
10import {
11 cleanupTests,
12 createMultipleServers,
13 doubleFollow,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 setDefaultAccountAvatar,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20
21async function findChannel (server: PeerTubeServer, channelId: number) {
22 const body = await server.channels.list({ sort: '-name' })
23
24 return body.data.find(c => c.id === channelId)
25}
26
27describe('Test video channels', function () {
28 let servers: PeerTubeServer[]
29 let sqlCommands: SQLCommand[] = []
30
31 let userInfo: User
32 let secondVideoChannelId: number
33 let totoChannel: number
34 let videoUUID: string
35 let accountName: string
36 let secondUserChannelName: string
37
38 const avatarPaths: { [ port: number ]: string } = {}
39 const bannerPaths: { [ port: number ]: string } = {}
40
41 before(async function () {
42 this.timeout(60000)
43
44 servers = await createMultipleServers(2)
45
46 await setAccessTokensToServers(servers)
47 await setDefaultVideoChannel(servers)
48 await setDefaultAccountAvatar(servers)
49
50 await doubleFollow(servers[0], servers[1])
51
52 sqlCommands = servers.map(s => new SQLCommand(s))
53 })
54
55 it('Should have one video channel (created with root)', async () => {
56 const body = await servers[0].channels.list({ start: 0, count: 2 })
57
58 expect(body.total).to.equal(1)
59 expect(body.data).to.be.an('array')
60 expect(body.data).to.have.lengthOf(1)
61 })
62
63 it('Should create another video channel', async function () {
64 this.timeout(30000)
65
66 {
67 const videoChannel = {
68 name: 'second_video_channel',
69 displayName: 'second video channel',
70 description: 'super video channel description',
71 support: 'super video channel support text'
72 }
73 const created = await servers[0].channels.create({ attributes: videoChannel })
74 secondVideoChannelId = created.id
75 }
76
77 // The channel is 1 is propagated to servers 2
78 {
79 const attributes = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
80 const { uuid } = await servers[0].videos.upload({ attributes })
81 videoUUID = uuid
82 }
83
84 await waitJobs(servers)
85 })
86
87 it('Should have two video channels when getting my information', async () => {
88 userInfo = await servers[0].users.getMyInfo()
89
90 expect(userInfo.videoChannels).to.be.an('array')
91 expect(userInfo.videoChannels).to.have.lengthOf(2)
92
93 const videoChannels = userInfo.videoChannels
94 expect(videoChannels[0].name).to.equal('root_channel')
95 expect(videoChannels[0].displayName).to.equal('Main root channel')
96
97 expect(videoChannels[1].name).to.equal('second_video_channel')
98 expect(videoChannels[1].displayName).to.equal('second video channel')
99 expect(videoChannels[1].description).to.equal('super video channel description')
100 expect(videoChannels[1].support).to.equal('super video channel support text')
101
102 accountName = userInfo.account.name + '@' + userInfo.account.host
103 })
104
105 it('Should have two video channels when getting account channels on server 1', async function () {
106 const body = await servers[0].channels.listByAccount({ accountName })
107 expect(body.total).to.equal(2)
108
109 const videoChannels = body.data
110
111 expect(videoChannels).to.be.an('array')
112 expect(videoChannels).to.have.lengthOf(2)
113
114 expect(videoChannels[0].name).to.equal('root_channel')
115 expect(videoChannels[0].displayName).to.equal('Main root channel')
116
117 expect(videoChannels[1].name).to.equal('second_video_channel')
118 expect(videoChannels[1].displayName).to.equal('second video channel')
119 expect(videoChannels[1].description).to.equal('super video channel description')
120 expect(videoChannels[1].support).to.equal('super video channel support text')
121 })
122
123 it('Should paginate and sort account channels', async function () {
124 {
125 const body = await servers[0].channels.listByAccount({
126 accountName,
127 start: 0,
128 count: 1,
129 sort: 'createdAt'
130 })
131
132 expect(body.total).to.equal(2)
133 expect(body.data).to.have.lengthOf(1)
134
135 const videoChannel: VideoChannel = body.data[0]
136 expect(videoChannel.name).to.equal('root_channel')
137 }
138
139 {
140 const body = await servers[0].channels.listByAccount({
141 accountName,
142 start: 0,
143 count: 1,
144 sort: '-createdAt'
145 })
146
147 expect(body.total).to.equal(2)
148 expect(body.data).to.have.lengthOf(1)
149 expect(body.data[0].name).to.equal('second_video_channel')
150 }
151
152 {
153 const body = await servers[0].channels.listByAccount({
154 accountName,
155 start: 1,
156 count: 1,
157 sort: '-createdAt'
158 })
159
160 expect(body.total).to.equal(2)
161 expect(body.data).to.have.lengthOf(1)
162 expect(body.data[0].name).to.equal('root_channel')
163 }
164 })
165
166 it('Should have one video channel when getting account channels on server 2', async function () {
167 const body = await servers[1].channels.listByAccount({ accountName })
168
169 expect(body.total).to.equal(1)
170 expect(body.data).to.be.an('array')
171 expect(body.data).to.have.lengthOf(1)
172
173 const videoChannel = body.data[0]
174 expect(videoChannel.name).to.equal('second_video_channel')
175 expect(videoChannel.displayName).to.equal('second video channel')
176 expect(videoChannel.description).to.equal('super video channel description')
177 expect(videoChannel.support).to.equal('super video channel support text')
178 })
179
180 it('Should list video channels', async function () {
181 const body = await servers[0].channels.list({ start: 1, count: 1, sort: '-name' })
182
183 expect(body.total).to.equal(2)
184 expect(body.data).to.be.an('array')
185 expect(body.data).to.have.lengthOf(1)
186 expect(body.data[0].name).to.equal('root_channel')
187 expect(body.data[0].displayName).to.equal('Main root channel')
188 })
189
190 it('Should update video channel', async function () {
191 this.timeout(15000)
192
193 const videoChannelAttributes = {
194 displayName: 'video channel updated',
195 description: 'video channel description updated',
196 support: 'support updated'
197 }
198
199 await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
200
201 await waitJobs(servers)
202 })
203
204 it('Should have video channel updated', async function () {
205 for (const server of servers) {
206 const body = await server.channels.list({ start: 0, count: 1, sort: '-name' })
207
208 expect(body.total).to.equal(2)
209 expect(body.data).to.be.an('array')
210 expect(body.data).to.have.lengthOf(1)
211
212 expect(body.data[0].name).to.equal('second_video_channel')
213 expect(body.data[0].displayName).to.equal('video channel updated')
214 expect(body.data[0].description).to.equal('video channel description updated')
215 expect(body.data[0].support).to.equal('support updated')
216 }
217 })
218
219 it('Should not have updated the video support field', async function () {
220 for (const server of servers) {
221 const video = await server.videos.get({ id: videoUUID })
222 expect(video.support).to.equal('video support field')
223 }
224 })
225
226 it('Should update another accounts video channel', async function () {
227 this.timeout(15000)
228
229 const result = await servers[0].users.generate('second_user')
230 secondUserChannelName = result.userChannelName
231
232 await servers[0].videos.quickUpload({ name: 'video', token: result.token })
233
234 const videoChannelAttributes = {
235 displayName: 'video channel updated',
236 description: 'video channel description updated',
237 support: 'support updated'
238 }
239
240 await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes })
241
242 await waitJobs(servers)
243 })
244
245 it('Should have another accounts video channel updated', async function () {
246 for (const server of servers) {
247 const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` })
248
249 expect(body.displayName).to.equal('video channel updated')
250 expect(body.description).to.equal('video channel description updated')
251 expect(body.support).to.equal('support updated')
252 }
253 })
254
255 it('Should update the channel support field and update videos too', async function () {
256 this.timeout(35000)
257
258 const videoChannelAttributes = {
259 support: 'video channel support text updated',
260 bulkVideosSupportUpdate: true
261 }
262
263 await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
264
265 await waitJobs(servers)
266
267 for (const server of servers) {
268 const video = await server.videos.get({ id: videoUUID })
269 expect(video.support).to.equal(videoChannelAttributes.support)
270 }
271 })
272
273 it('Should update video channel avatar', async function () {
274 this.timeout(15000)
275
276 const fixture = 'avatar.png'
277
278 await servers[0].channels.updateImage({
279 channelName: 'second_video_channel',
280 fixture,
281 type: 'avatar'
282 })
283
284 await waitJobs(servers)
285
286 for (let i = 0; i < servers.length; i++) {
287 const server = servers[i]
288
289 const videoChannel = await findChannel(server, secondVideoChannelId)
290 const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR]
291
292 expect(videoChannel.avatars.length).to.equal(expectedSizes.length, 'Expected avatars to be generated in all sizes')
293
294 for (const avatar of videoChannel.avatars) {
295 avatarPaths[server.port] = avatar.path
296 await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png')
297 await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
298
299 const row = await sqlCommands[i].getActorImage(basename(avatarPaths[server.port]))
300
301 expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true)
302 }
303 }
304 })
305
306 it('Should update video channel banner', async function () {
307 this.timeout(15000)
308
309 const fixture = 'banner.jpg'
310
311 await servers[0].channels.updateImage({
312 channelName: 'second_video_channel',
313 fixture,
314 type: 'banner'
315 })
316
317 await waitJobs(servers)
318
319 for (let i = 0; i < servers.length; i++) {
320 const server = servers[i]
321
322 const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host })
323
324 bannerPaths[server.port] = videoChannel.banners[0].path
325 await testImage(server.url, 'banner-resized', bannerPaths[server.port])
326 await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
327
328 const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port]))
329 expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height)
330 expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width)
331 }
332 })
333
334 it('Should still correctly list channels', async function () {
335 {
336 const body = await servers[0].channels.list({ start: 1, count: 1, sort: 'createdAt' })
337
338 expect(body.total).to.equal(3)
339 expect(body.data).to.have.lengthOf(1)
340 expect(body.data[0].name).to.equal('second_video_channel')
341 }
342
343 {
344 const body = await servers[0].channels.listByAccount({ accountName, start: 1, count: 1, sort: 'createdAt' })
345
346 expect(body.total).to.equal(2)
347 expect(body.data).to.have.lengthOf(1)
348 expect(body.data[0].name).to.equal('second_video_channel')
349 }
350 })
351
352 it('Should delete the video channel avatar', async function () {
353 this.timeout(15000)
354 await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' })
355
356 await waitJobs(servers)
357
358 for (const server of servers) {
359 const videoChannel = await findChannel(server, secondVideoChannelId)
360 await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false)
361
362 expect(videoChannel.avatars).to.be.empty
363 }
364 })
365
366 it('Should delete the video channel banner', async function () {
367 this.timeout(15000)
368
369 await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'banner' })
370
371 await waitJobs(servers)
372
373 for (const server of servers) {
374 const videoChannel = await findChannel(server, secondVideoChannelId)
375 await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false)
376
377 expect(videoChannel.banners).to.be.empty
378 }
379 })
380
381 it('Should list the second video channel videos', async function () {
382 for (const server of servers) {
383 const channelURI = 'second_video_channel@' + servers[0].host
384 const { total, data } = await server.videos.listByChannel({ handle: channelURI })
385
386 expect(total).to.equal(1)
387 expect(data).to.be.an('array')
388 expect(data).to.have.lengthOf(1)
389 expect(data[0].name).to.equal('my video name')
390 }
391 })
392
393 it('Should change the video channel of a video', async function () {
394 await servers[0].videos.update({ id: videoUUID, attributes: { channelId: servers[0].store.channel.id } })
395
396 await waitJobs(servers)
397 })
398
399 it('Should list the first video channel videos', async function () {
400 for (const server of servers) {
401 {
402 const secondChannelURI = 'second_video_channel@' + servers[0].host
403 const { total } = await server.videos.listByChannel({ handle: secondChannelURI })
404 expect(total).to.equal(0)
405 }
406
407 {
408 const channelURI = 'root_channel@' + servers[0].host
409 const { total, data } = await server.videos.listByChannel({ handle: channelURI })
410 expect(total).to.equal(1)
411
412 expect(data).to.be.an('array')
413 expect(data).to.have.lengthOf(1)
414 expect(data[0].name).to.equal('my video name')
415 }
416 }
417 })
418
419 it('Should delete video channel', async function () {
420 await servers[0].channels.delete({ channelName: 'second_video_channel' })
421 })
422
423 it('Should have video channel deleted', async function () {
424 const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' })
425
426 expect(body.total).to.equal(2)
427 expect(body.data).to.be.an('array')
428 expect(body.data).to.have.lengthOf(2)
429 expect(body.data[0].displayName).to.equal('Main root channel')
430 expect(body.data[1].displayName).to.equal('video channel updated')
431 })
432
433 it('Should create the main channel with a suffix if there is a conflict', async function () {
434 {
435 const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' }
436 const created = await servers[0].channels.create({ attributes: videoChannel })
437 totoChannel = created.id
438 }
439
440 {
441 await servers[0].users.create({ username: 'toto', password: 'password' })
442 const accessToken = await servers[0].login.getAccessToken({ username: 'toto', password: 'password' })
443
444 const { videoChannels } = await servers[0].users.getMyInfo({ token: accessToken })
445 expect(videoChannels[0].name).to.equal('toto_channel-1')
446 }
447 })
448
449 it('Should report correct channel views per days', async function () {
450 {
451 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
452
453 for (const channel of data) {
454 expect(channel).to.haveOwnProperty('viewsPerDay')
455 expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
456
457 for (const v of channel.viewsPerDay) {
458 expect(v.date).to.be.an('string')
459 expect(v.views).to.equal(0)
460 }
461 }
462 }
463
464 {
465 // video has been posted on channel servers[0].store.videoChannel.id since last update
466 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' })
467 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' })
468
469 // Wait the repeatable job
470 await wait(8000)
471
472 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
473 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
474 expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2)
475 }
476 })
477
478 it('Should report correct total views count', async function () {
479 // check if there's the property
480 {
481 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
482
483 for (const channel of data) {
484 expect(channel).to.haveOwnProperty('totalViews')
485 expect(channel.totalViews).to.be.a('number')
486 }
487 }
488
489 // Check if the totalViews count can be updated
490 {
491 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
492 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
493 expect(channelWithView.totalViews).to.equal(2)
494 }
495 })
496
497 it('Should report correct videos count', async function () {
498 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
499
500 const totoChannel = data.find(c => c.name === 'toto_channel')
501 const rootChannel = data.find(c => c.name === 'root_channel')
502
503 expect(rootChannel.videosCount).to.equal(1)
504 expect(totoChannel.videosCount).to.equal(0)
505 })
506
507 it('Should search among account video channels', async function () {
508 {
509 const body = await servers[0].channels.listByAccount({ accountName, search: 'root' })
510 expect(body.total).to.equal(1)
511
512 const channels = body.data
513 expect(channels).to.have.lengthOf(1)
514 }
515
516 {
517 const body = await servers[0].channels.listByAccount({ accountName, search: 'does not exist' })
518 expect(body.total).to.equal(0)
519
520 const channels = body.data
521 expect(channels).to.have.lengthOf(0)
522 }
523 })
524
525 it('Should list channels by updatedAt desc if a video has been uploaded', async function () {
526 this.timeout(30000)
527
528 await servers[0].videos.upload({ attributes: { channelId: totoChannel } })
529 await waitJobs(servers)
530
531 for (const server of servers) {
532 const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
533
534 expect(data[0].name).to.equal('toto_channel')
535 expect(data[1].name).to.equal('root_channel')
536 }
537
538 await servers[0].videos.upload({ attributes: { channelId: servers[0].store.channel.id } })
539 await waitJobs(servers)
540
541 for (const server of servers) {
542 const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
543
544 expect(data[0].name).to.equal('root_channel')
545 expect(data[1].name).to.equal('toto_channel')
546 }
547 })
548
549 after(async function () {
550 for (const sqlCommand of sqlCommands) {
551 await sqlCommand.cleanup()
552 }
553
554 await cleanupTests(servers)
555 })
556})
diff --git a/packages/tests/src/api/videos/video-comments.ts b/packages/tests/src/api/videos/video-comments.ts
new file mode 100644
index 000000000..f17db9979
--- /dev/null
+++ b/packages/tests/src/api/videos/video-comments.ts
@@ -0,0 +1,335 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { dateIsValid, testImage } from '@tests/shared/checks.js'
5import {
6 cleanupTests,
7 CommentsCommand,
8 createSingleServer,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultAccountAvatar,
12 setDefaultChannelAvatar
13} from '@peertube/peertube-server-commands'
14
15describe('Test video comments', function () {
16 let server: PeerTubeServer
17 let videoId: number
18 let videoUUID: string
19 let threadId: number
20 let replyToDeleteId: number
21
22 let userAccessTokenServer1: string
23
24 let command: CommentsCommand
25
26 before(async function () {
27 this.timeout(120000)
28
29 server = await createSingleServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 const { id, uuid } = await server.videos.upload()
34 videoUUID = uuid
35 videoId = id
36
37 await setDefaultChannelAvatar(server)
38 await setDefaultAccountAvatar(server)
39
40 userAccessTokenServer1 = await server.users.generateUserAndToken('user1')
41 await setDefaultChannelAvatar(server, 'user1_channel')
42 await setDefaultAccountAvatar(server, userAccessTokenServer1)
43
44 command = server.comments
45 })
46
47 describe('User comments', function () {
48
49 it('Should not have threads on this video', async function () {
50 const body = await command.listThreads({ videoId: videoUUID })
51
52 expect(body.total).to.equal(0)
53 expect(body.totalNotDeletedComments).to.equal(0)
54 expect(body.data).to.be.an('array')
55 expect(body.data).to.have.lengthOf(0)
56 })
57
58 it('Should create a thread in this video', async function () {
59 const text = 'my super first comment'
60
61 const comment = await command.createThread({ videoId: videoUUID, text })
62
63 expect(comment.inReplyToCommentId).to.be.null
64 expect(comment.text).equal('my super first comment')
65 expect(comment.videoId).to.equal(videoId)
66 expect(comment.id).to.equal(comment.threadId)
67 expect(comment.account.name).to.equal('root')
68 expect(comment.account.host).to.equal(server.host)
69 expect(comment.account.url).to.equal(server.url + '/accounts/root')
70 expect(comment.totalReplies).to.equal(0)
71 expect(comment.totalRepliesFromVideoAuthor).to.equal(0)
72 expect(dateIsValid(comment.createdAt as string)).to.be.true
73 expect(dateIsValid(comment.updatedAt as string)).to.be.true
74 })
75
76 it('Should list threads of this video', async function () {
77 const body = await command.listThreads({ videoId: videoUUID })
78
79 expect(body.total).to.equal(1)
80 expect(body.totalNotDeletedComments).to.equal(1)
81 expect(body.data).to.be.an('array')
82 expect(body.data).to.have.lengthOf(1)
83
84 const comment = body.data[0]
85 expect(comment.inReplyToCommentId).to.be.null
86 expect(comment.text).equal('my super first comment')
87 expect(comment.videoId).to.equal(videoId)
88 expect(comment.id).to.equal(comment.threadId)
89 expect(comment.account.name).to.equal('root')
90 expect(comment.account.host).to.equal(server.host)
91
92 for (const avatar of comment.account.avatars) {
93 await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png')
94 }
95
96 expect(comment.totalReplies).to.equal(0)
97 expect(comment.totalRepliesFromVideoAuthor).to.equal(0)
98 expect(dateIsValid(comment.createdAt as string)).to.be.true
99 expect(dateIsValid(comment.updatedAt as string)).to.be.true
100
101 threadId = comment.threadId
102 })
103
104 it('Should get all the thread created', async function () {
105 const body = await command.getThread({ videoId: videoUUID, threadId })
106
107 const rootComment = body.comment
108 expect(rootComment.inReplyToCommentId).to.be.null
109 expect(rootComment.text).equal('my super first comment')
110 expect(rootComment.videoId).to.equal(videoId)
111 expect(dateIsValid(rootComment.createdAt as string)).to.be.true
112 expect(dateIsValid(rootComment.updatedAt as string)).to.be.true
113 })
114
115 it('Should create multiple replies in this thread', async function () {
116 const text1 = 'my super answer to thread 1'
117 const created = await command.addReply({ videoId, toCommentId: threadId, text: text1 })
118 const childCommentId = created.id
119
120 const text2 = 'my super answer to answer of thread 1'
121 await command.addReply({ videoId, toCommentId: childCommentId, text: text2 })
122
123 const text3 = 'my second answer to thread 1'
124 await command.addReply({ videoId, toCommentId: threadId, text: text3 })
125 })
126
127 it('Should get correctly the replies', async function () {
128 const tree = await command.getThread({ videoId: videoUUID, threadId })
129
130 expect(tree.comment.text).equal('my super first comment')
131 expect(tree.children).to.have.lengthOf(2)
132
133 const firstChild = tree.children[0]
134 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
135 expect(firstChild.children).to.have.lengthOf(1)
136
137 const childOfFirstChild = firstChild.children[0]
138 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
139 expect(childOfFirstChild.children).to.have.lengthOf(0)
140
141 const secondChild = tree.children[1]
142 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
143 expect(secondChild.children).to.have.lengthOf(0)
144
145 replyToDeleteId = secondChild.comment.id
146 })
147
148 it('Should create other threads', async function () {
149 const text1 = 'super thread 2'
150 await command.createThread({ videoId: videoUUID, text: text1 })
151
152 const text2 = 'super thread 3'
153 await command.createThread({ videoId: videoUUID, text: text2 })
154 })
155
156 it('Should list the threads', async function () {
157 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
158
159 expect(body.total).to.equal(3)
160 expect(body.totalNotDeletedComments).to.equal(6)
161 expect(body.data).to.be.an('array')
162 expect(body.data).to.have.lengthOf(3)
163
164 expect(body.data[0].text).to.equal('my super first comment')
165 expect(body.data[0].totalReplies).to.equal(3)
166 expect(body.data[1].text).to.equal('super thread 2')
167 expect(body.data[1].totalReplies).to.equal(0)
168 expect(body.data[2].text).to.equal('super thread 3')
169 expect(body.data[2].totalReplies).to.equal(0)
170 })
171
172 it('Should list the and sort them by total replies', async function () {
173 const body = await command.listThreads({ videoId: videoUUID, sort: 'totalReplies' })
174
175 expect(body.data[2].text).to.equal('my super first comment')
176 expect(body.data[2].totalReplies).to.equal(3)
177 })
178
179 it('Should delete a reply', async function () {
180 await command.delete({ videoId, commentId: replyToDeleteId })
181
182 {
183 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
184
185 expect(body.total).to.equal(3)
186 expect(body.totalNotDeletedComments).to.equal(5)
187 }
188
189 {
190 const tree = await command.getThread({ videoId: videoUUID, threadId })
191
192 expect(tree.comment.text).equal('my super first comment')
193 expect(tree.children).to.have.lengthOf(2)
194
195 const firstChild = tree.children[0]
196 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
197 expect(firstChild.children).to.have.lengthOf(1)
198
199 const childOfFirstChild = firstChild.children[0]
200 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
201 expect(childOfFirstChild.children).to.have.lengthOf(0)
202
203 const deletedChildOfFirstChild = tree.children[1]
204 expect(deletedChildOfFirstChild.comment.text).to.equal('')
205 expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
206 expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
207 expect(deletedChildOfFirstChild.comment.account).to.be.null
208 expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
209 }
210 })
211
212 it('Should delete a complete thread', async function () {
213 await command.delete({ videoId, commentId: threadId })
214
215 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
216 expect(body.total).to.equal(3)
217 expect(body.data).to.be.an('array')
218 expect(body.data).to.have.lengthOf(3)
219
220 expect(body.data[0].text).to.equal('')
221 expect(body.data[0].isDeleted).to.be.true
222 expect(body.data[0].deletedAt).to.not.be.null
223 expect(body.data[0].account).to.be.null
224 expect(body.data[0].totalReplies).to.equal(2)
225 expect(body.data[1].text).to.equal('super thread 2')
226 expect(body.data[1].totalReplies).to.equal(0)
227 expect(body.data[2].text).to.equal('super thread 3')
228 expect(body.data[2].totalReplies).to.equal(0)
229 })
230
231 it('Should count replies from the video author correctly', async function () {
232 await command.createThread({ videoId: videoUUID, text: 'my super first comment' })
233
234 const { data } = await command.listThreads({ videoId: videoUUID })
235 const threadId2 = data[0].threadId
236
237 const text2 = 'a first answer to thread 4 by a third party'
238 await command.addReply({ token: userAccessTokenServer1, videoId, toCommentId: threadId2, text: text2 })
239
240 const text3 = 'my second answer to thread 4'
241 await command.addReply({ videoId, toCommentId: threadId2, text: text3 })
242
243 const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 })
244 expect(tree.comment.totalRepliesFromVideoAuthor).to.equal(1)
245 expect(tree.comment.totalReplies).to.equal(2)
246 })
247 })
248
249 describe('All instance comments', function () {
250
251 it('Should list instance comments as admin', async function () {
252 {
253 const { data, total } = await command.listForAdmin({ start: 0, count: 1 })
254
255 expect(total).to.equal(7)
256 expect(data).to.have.lengthOf(1)
257 expect(data[0].text).to.equal('my second answer to thread 4')
258 expect(data[0].account.name).to.equal('root')
259 expect(data[0].account.displayName).to.equal('root')
260 expect(data[0].account.avatars).to.have.lengthOf(2)
261 }
262
263 {
264 const { data, total } = await command.listForAdmin({ start: 1, count: 2 })
265
266 expect(total).to.equal(7)
267 expect(data).to.have.lengthOf(2)
268
269 expect(data[0].account.avatars).to.have.lengthOf(2)
270 expect(data[1].account.avatars).to.have.lengthOf(2)
271 }
272 })
273
274 it('Should filter instance comments by isLocal', async function () {
275 const { total, data } = await command.listForAdmin({ isLocal: false })
276
277 expect(data).to.have.lengthOf(0)
278 expect(total).to.equal(0)
279 })
280
281 it('Should filter instance comments by onLocalVideo', async function () {
282 {
283 const { total, data } = await command.listForAdmin({ onLocalVideo: false })
284
285 expect(data).to.have.lengthOf(0)
286 expect(total).to.equal(0)
287 }
288
289 {
290 const { total, data } = await command.listForAdmin({ onLocalVideo: true })
291
292 expect(data).to.not.have.lengthOf(0)
293 expect(total).to.not.equal(0)
294 }
295 })
296
297 it('Should search instance comments by account', async function () {
298 const { total, data } = await command.listForAdmin({ searchAccount: 'user' })
299
300 expect(data).to.have.lengthOf(1)
301 expect(total).to.equal(1)
302
303 expect(data[0].text).to.equal('a first answer to thread 4 by a third party')
304 })
305
306 it('Should search instance comments by video', async function () {
307 {
308 const { total, data } = await command.listForAdmin({ searchVideo: 'video' })
309
310 expect(data).to.have.lengthOf(7)
311 expect(total).to.equal(7)
312 }
313
314 {
315 const { total, data } = await command.listForAdmin({ searchVideo: 'hello' })
316
317 expect(data).to.have.lengthOf(0)
318 expect(total).to.equal(0)
319 }
320 })
321
322 it('Should search instance comments', async function () {
323 const { total, data } = await command.listForAdmin({ search: 'super thread 3' })
324
325 expect(total).to.equal(1)
326
327 expect(data).to.have.lengthOf(1)
328 expect(data[0].text).to.equal('super thread 3')
329 })
330 })
331
332 after(async function () {
333 await cleanupTests([ server ])
334 })
335})
diff --git a/packages/tests/src/api/videos/video-description.ts b/packages/tests/src/api/videos/video-description.ts
new file mode 100644
index 000000000..eb41cd71c
--- /dev/null
+++ b/packages/tests/src/api/videos/video-description.ts
@@ -0,0 +1,103 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 waitJobs
11} from '@peertube/peertube-server-commands'
12
13describe('Test video description', function () {
14 let servers: PeerTubeServer[] = []
15 let videoUUID = ''
16 let videoId: number
17
18 const longDescription = 'my super description for server 1'.repeat(50)
19
20 // 30 characters * 6 -> 240 characters
21 const truncatedDescription = 'my super description for server 1'.repeat(7) + 'my super descrip...'
22
23 before(async function () {
24 this.timeout(40000)
25
26 // Run servers
27 servers = await createMultipleServers(2)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31
32 // Server 1 and server 2 follow each other
33 await doubleFollow(servers[0], servers[1])
34 })
35
36 it('Should upload video with long description', async function () {
37 this.timeout(30000)
38
39 const attributes = {
40 description: longDescription
41 }
42 await servers[0].videos.upload({ attributes })
43
44 await waitJobs(servers)
45
46 const { data } = await servers[0].videos.list()
47
48 videoId = data[0].id
49 videoUUID = data[0].uuid
50 })
51
52 it('Should have a truncated description on each server when listing videos', async function () {
53 for (const server of servers) {
54 const { data } = await server.videos.list()
55 const video = data.find(v => v.uuid === videoUUID)
56
57 expect(video.description).to.equal(truncatedDescription)
58 expect(video.truncatedDescription).to.equal(truncatedDescription)
59 }
60 })
61
62 it('Should not have a truncated description on each server when getting videos', async function () {
63 for (const server of servers) {
64 const video = await server.videos.get({ id: videoUUID })
65
66 expect(video.description).to.equal(longDescription)
67 expect(video.truncatedDescription).to.equal(truncatedDescription)
68 }
69 })
70
71 it('Should fetch long description on each server', async function () {
72 for (const server of servers) {
73 const video = await server.videos.get({ id: videoUUID })
74
75 const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
76 expect(description).to.equal(longDescription)
77 }
78 })
79
80 it('Should update with a short description', async function () {
81 const attributes = {
82 description: 'short description'
83 }
84 await servers[0].videos.update({ id: videoId, attributes })
85
86 await waitJobs(servers)
87 })
88
89 it('Should have a small description on each server', async function () {
90 for (const server of servers) {
91 const video = await server.videos.get({ id: videoUUID })
92
93 expect(video.description).to.equal('short description')
94
95 const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
96 expect(description).to.equal('short description')
97 }
98 })
99
100 after(async function () {
101 await cleanupTests(servers)
102 })
103})
diff --git a/packages/tests/src/api/videos/video-files.ts b/packages/tests/src/api/videos/video-files.ts
new file mode 100644
index 000000000..1d7c218a4
--- /dev/null
+++ b/packages/tests/src/api/videos/video-files.ts
@@ -0,0 +1,202 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeRawRequest,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos files', function () {
16 let servers: PeerTubeServer[]
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(150_000)
22
23 servers = await createMultipleServers(2)
24 await setAccessTokensToServers(servers)
25
26 await doubleFollow(servers[0], servers[1])
27
28 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
29 })
30
31 describe('When deleting all files', function () {
32 let validId1: string
33 let validId2: string
34
35 before(async function () {
36 this.timeout(360_000)
37
38 {
39 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' })
40 validId1 = uuid
41 }
42
43 {
44 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' })
45 validId2 = uuid
46 }
47
48 await waitJobs(servers)
49 })
50
51 it('Should delete web video files', async function () {
52 this.timeout(30_000)
53
54 await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1 })
55
56 await waitJobs(servers)
57
58 for (const server of servers) {
59 const video = await server.videos.get({ id: validId1 })
60
61 expect(video.files).to.have.lengthOf(0)
62 expect(video.streamingPlaylists).to.have.lengthOf(1)
63 }
64 })
65
66 it('Should delete HLS files', async function () {
67 this.timeout(30_000)
68
69 await servers[0].videos.removeHLSPlaylist({ videoId: validId2 })
70
71 await waitJobs(servers)
72
73 for (const server of servers) {
74 const video = await server.videos.get({ id: validId2 })
75
76 expect(video.files).to.have.length.above(0)
77 expect(video.streamingPlaylists).to.have.lengthOf(0)
78 }
79 })
80 })
81
82 describe('When deleting a specific file', function () {
83 let webVideoId: string
84 let hlsId: string
85
86 before(async function () {
87 this.timeout(120_000)
88
89 {
90 const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' })
91 webVideoId = uuid
92 }
93
94 {
95 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
96 hlsId = uuid
97 }
98
99 await waitJobs(servers)
100 })
101
102 it('Shoulde delete a web video file', async function () {
103 this.timeout(30_000)
104
105 const video = await servers[0].videos.get({ id: webVideoId })
106 const files = video.files
107
108 await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id })
109
110 await waitJobs(servers)
111
112 for (const server of servers) {
113 const video = await server.videos.get({ id: webVideoId })
114
115 expect(video.files).to.have.lengthOf(files.length - 1)
116 expect(video.files.find(f => f.id === files[0].id)).to.not.exist
117 }
118 })
119
120 it('Should delete all web video files', async function () {
121 this.timeout(30_000)
122
123 const video = await servers[0].videos.get({ id: webVideoId })
124 const files = video.files
125
126 for (const file of files) {
127 await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: file.id })
128 }
129
130 await waitJobs(servers)
131
132 for (const server of servers) {
133 const video = await server.videos.get({ id: webVideoId })
134
135 expect(video.files).to.have.lengthOf(0)
136 }
137 })
138
139 it('Should delete a hls file', async function () {
140 this.timeout(30_000)
141
142 const video = await servers[0].videos.get({ id: hlsId })
143 const files = video.streamingPlaylists[0].files
144 const toDelete = files[0]
145
146 await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: toDelete.id })
147
148 await waitJobs(servers)
149
150 for (const server of servers) {
151 const video = await server.videos.get({ id: hlsId })
152
153 expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
154 expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist
155
156 const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
157
158 expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false
159 expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true
160 }
161 })
162
163 it('Should delete all hls files', async function () {
164 this.timeout(30_000)
165
166 const video = await servers[0].videos.get({ id: hlsId })
167 const files = video.streamingPlaylists[0].files
168
169 for (const file of files) {
170 await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: file.id })
171 }
172
173 await waitJobs(servers)
174
175 for (const server of servers) {
176 const video = await server.videos.get({ id: hlsId })
177
178 expect(video.streamingPlaylists).to.have.lengthOf(0)
179 }
180 })
181
182 it('Should not delete last file of a video', async function () {
183 this.timeout(60_000)
184
185 const webVideoOnly = await servers[0].videos.get({ id: hlsId })
186 const hlsOnly = await servers[0].videos.get({ id: webVideoId })
187
188 for (let i = 0; i < 4; i++) {
189 await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[i].id })
190 await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id })
191 }
192
193 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
194 await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[4].id, expectedStatus })
195 await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus })
196 })
197 })
198
199 after(async function () {
200 await cleanupTests(servers)
201 })
202})
diff --git a/packages/tests/src/api/videos/video-imports.ts b/packages/tests/src/api/videos/video-imports.ts
new file mode 100644
index 000000000..6c874d713
--- /dev/null
+++ b/packages/tests/src/api/videos/video-imports.ts
@@ -0,0 +1,635 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, remove } from 'fs-extra/esm'
5import { readdir } from 'fs/promises'
6import { join } from 'path'
7import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
8import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@peertube/peertube-models'
9import {
10 cleanupTests,
11 createMultipleServers,
12 createSingleServer,
13 doubleFollow,
14 getServerImportConfig,
15 PeerTubeServer,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20import { DeepPartial } from '@peertube/peertube-typescript-utils'
21import { testCaptionFile } from '@tests/shared/captions.js'
22import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
23import { FIXTURE_URLS } from '@tests/shared/tests.js'
24
25async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) {
26 const videoHttp = await server.videos.get({ id: idHttp })
27
28 expect(videoHttp.name).to.equal('small video - youtube')
29 expect(videoHttp.category.label).to.equal('News & Politics')
30 expect(videoHttp.licence.label).to.equal('Attribution')
31 expect(videoHttp.language.label).to.equal('Unknown')
32 expect(videoHttp.nsfw).to.be.false
33 expect(videoHttp.description).to.equal('this is a super description')
34 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ])
35 expect(videoHttp.files).to.have.lengthOf(1)
36
37 const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt)
38 expect(originallyPublishedAt.getDate()).to.equal(14)
39 expect(originallyPublishedAt.getMonth()).to.equal(0)
40 expect(originallyPublishedAt.getFullYear()).to.equal(2019)
41
42 const videoMagnet = await server.videos.get({ id: idMagnet })
43 const videoTorrent = await server.videos.get({ id: idTorrent })
44
45 for (const video of [ videoMagnet, videoTorrent ]) {
46 expect(video.category.label).to.equal('Unknown')
47 expect(video.licence.label).to.equal('Unknown')
48 expect(video.language.label).to.equal('Unknown')
49 expect(video.nsfw).to.be.false
50 expect(video.description).to.equal('this is a super torrent description')
51 expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ])
52 expect(video.files).to.have.lengthOf(1)
53 }
54
55 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4')
56 expect(videoMagnet.name).to.contain('super peertube2 video')
57
58 const bodyCaptions = await server.captions.list({ videoId: idHttp })
59 expect(bodyCaptions.total).to.equal(2)
60}
61
62async function checkVideoServer2 (server: PeerTubeServer, id: number | string) {
63 const video = await server.videos.get({ id })
64
65 expect(video.name).to.equal('my super name')
66 expect(video.category.label).to.equal('Entertainment')
67 expect(video.licence.label).to.equal('Public Domain Dedication')
68 expect(video.language.label).to.equal('English')
69 expect(video.nsfw).to.be.false
70 expect(video.description).to.equal('my super description')
71 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ])
72
73 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', video.thumbnailPath)
74
75 expect(video.files).to.have.lengthOf(1)
76
77 const bodyCaptions = await server.captions.list({ videoId: id })
78 expect(bodyCaptions.total).to.equal(2)
79}
80
81describe('Test video imports', function () {
82
83 if (areHttpImportTestsDisabled()) return
84
85 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
86
87 describe('Import ' + mode, function () {
88 let servers: PeerTubeServer[] = []
89
90 before(async function () {
91 this.timeout(60_000)
92
93 servers = await createMultipleServers(2, getServerImportConfig(mode))
94
95 await setAccessTokensToServers(servers)
96 await setDefaultVideoChannel(servers)
97
98 for (const server of servers) {
99 await server.config.updateExistingSubConfig({
100 newConfig: {
101 transcoding: {
102 alwaysTranscodeOriginalResolution: false
103 }
104 }
105 })
106 }
107
108 await doubleFollow(servers[0], servers[1])
109 })
110
111 it('Should import videos on server 1', async function () {
112 this.timeout(60_000)
113
114 const baseAttributes = {
115 channelId: servers[0].store.channel.id,
116 privacy: VideoPrivacy.PUBLIC
117 }
118
119 {
120 const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube }
121 const { video } = await servers[0].imports.importVideo({ attributes })
122 expect(video.name).to.equal('small video - youtube')
123
124 {
125 expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`))
126 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
127
128 const suffix = mode === 'yt-dlp'
129 ? '_yt_dlp'
130 : ''
131
132 await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath)
133 await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_preview' + suffix, video.previewPath)
134 }
135
136 const bodyCaptions = await servers[0].captions.list({ videoId: video.id })
137 const videoCaptions = bodyCaptions.data
138 expect(videoCaptions).to.have.lengthOf(2)
139
140 {
141 const enCaption = videoCaptions.find(caption => caption.language.id === 'en')
142 expect(enCaption).to.exist
143 expect(enCaption.language.label).to.equal('English')
144 expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`))
145
146 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` +
147 `(Language: en[ \n]+)?` +
148 `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+English \\(US\\)[ \n]+` +
149 `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+This is a subtitle in American English[ \n]+` +
150 `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Adding subtitles is very easy to do`
151 await testCaptionFile(servers[0].url, enCaption.captionPath, new RegExp(regex))
152 }
153
154 {
155 const frCaption = videoCaptions.find(caption => caption.language.id === 'fr')
156 expect(frCaption).to.exist
157 expect(frCaption.language.label).to.equal('French')
158 expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`))
159
160 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` +
161 `(Language: fr[ \n]+)?` +
162 `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+Français \\(FR\\)[ \n]+` +
163 `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+C'est un sous-titre français[ \n]+` +
164 `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Ajouter un sous-titre est vraiment facile`
165
166 await testCaptionFile(servers[0].url, frCaption.captionPath, new RegExp(regex))
167 }
168 }
169
170 {
171 const attributes = {
172 ...baseAttributes,
173 magnetUri: FIXTURE_URLS.magnet,
174 description: 'this is a super torrent description',
175 tags: [ 'tag_torrent1', 'tag_torrent2' ]
176 }
177 const { video } = await servers[0].imports.importVideo({ attributes })
178 expect(video.name).to.equal('super peertube2 video')
179 }
180
181 {
182 const attributes = {
183 ...baseAttributes,
184 torrentfile: 'video-720p.torrent' as any,
185 description: 'this is a super torrent description',
186 tags: [ 'tag_torrent1', 'tag_torrent2' ]
187 }
188 const { video } = await servers[0].imports.importVideo({ attributes })
189 expect(video.name).to.equal('你好 世界 720p.mp4')
190 }
191 })
192
193 it('Should list the videos to import in my videos on server 1', async function () {
194 const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' })
195
196 expect(total).to.equal(3)
197
198 expect(data).to.have.lengthOf(3)
199 expect(data[0].name).to.equal('small video - youtube')
200 expect(data[1].name).to.equal('super peertube2 video')
201 expect(data[2].name).to.equal('你好 世界 720p.mp4')
202 })
203
204 it('Should list the videos to import in my imports on server 1', async function () {
205 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' })
206 expect(total).to.equal(3)
207
208 expect(videoImports).to.have.lengthOf(3)
209
210 expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube)
211 expect(videoImports[2].magnetUri).to.be.null
212 expect(videoImports[2].torrentName).to.be.null
213 expect(videoImports[2].video.name).to.equal('small video - youtube')
214
215 expect(videoImports[1].targetUrl).to.be.null
216 expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet)
217 expect(videoImports[1].torrentName).to.be.null
218 expect(videoImports[1].video.name).to.equal('super peertube2 video')
219
220 expect(videoImports[0].targetUrl).to.be.null
221 expect(videoImports[0].magnetUri).to.be.null
222 expect(videoImports[0].torrentName).to.equal('video-720p.torrent')
223 expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4')
224 })
225
226 it('Should filter my imports on target URL', async function () {
227 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ targetUrl: FIXTURE_URLS.youtube })
228 expect(total).to.equal(1)
229 expect(videoImports).to.have.lengthOf(1)
230
231 expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube)
232 })
233
234 it('Should search in my imports', async function () {
235 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' })
236 expect(total).to.equal(1)
237 expect(videoImports).to.have.lengthOf(1)
238
239 expect(videoImports[0].magnetUri).to.equal(FIXTURE_URLS.magnet)
240 expect(videoImports[0].video.name).to.equal('super peertube2 video')
241 })
242
243 it('Should have the video listed on the two instances', async function () {
244 this.timeout(120_000)
245
246 await waitJobs(servers)
247
248 for (const server of servers) {
249 const { total, data } = await server.videos.list()
250 expect(total).to.equal(3)
251 expect(data).to.have.lengthOf(3)
252
253 const [ videoHttp, videoMagnet, videoTorrent ] = data
254 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
255 }
256 })
257
258 it('Should import a video on server 2 with some fields', async function () {
259 this.timeout(60_000)
260
261 const { video } = await servers[1].imports.importVideo({
262 attributes: {
263 targetUrl: FIXTURE_URLS.youtube,
264 channelId: servers[1].store.channel.id,
265 privacy: VideoPrivacy.PUBLIC,
266 category: 10,
267 licence: 7,
268 language: 'en',
269 name: 'my super name',
270 description: 'my super description',
271 tags: [ 'supertag1', 'supertag2' ],
272 thumbnailfile: 'custom-thumbnail.jpg'
273 }
274 })
275 expect(video.name).to.equal('my super name')
276 })
277
278 it('Should have the videos listed on the two instances', async function () {
279 this.timeout(120_000)
280
281 await waitJobs(servers)
282
283 for (const server of servers) {
284 const { total, data } = await server.videos.list()
285 expect(total).to.equal(4)
286 expect(data).to.have.lengthOf(4)
287
288 await checkVideoServer2(server, data[0].uuid)
289
290 const [ , videoHttp, videoMagnet, videoTorrent ] = data
291 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
292 }
293 })
294
295 it('Should import a video that will be transcoded', async function () {
296 this.timeout(240_000)
297
298 const attributes = {
299 name: 'transcoded video',
300 magnetUri: FIXTURE_URLS.magnet,
301 channelId: servers[1].store.channel.id,
302 privacy: VideoPrivacy.PUBLIC
303 }
304 const { video } = await servers[1].imports.importVideo({ attributes })
305 const videoUUID = video.uuid
306
307 await waitJobs(servers)
308
309 for (const server of servers) {
310 const video = await server.videos.get({ id: videoUUID })
311
312 expect(video.name).to.equal('transcoded video')
313 expect(video.files).to.have.lengthOf(4)
314 }
315 })
316
317 it('Should import no HDR version on a HDR video', async function () {
318 this.timeout(300_000)
319
320 const config: DeepPartial<CustomConfig> = {
321 transcoding: {
322 enabled: true,
323 resolutions: {
324 '0p': false,
325 '144p': true,
326 '240p': true,
327 '360p': false,
328 '480p': false,
329 '720p': false,
330 '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01
331 '1440p': false,
332 '2160p': false
333 },
334 webVideos: { enabled: true },
335 hls: { enabled: false }
336 }
337 }
338 await servers[0].config.updateExistingSubConfig({ newConfig: config })
339
340 const attributes = {
341 name: 'hdr video',
342 targetUrl: FIXTURE_URLS.youtubeHDR,
343 channelId: servers[0].store.channel.id,
344 privacy: VideoPrivacy.PUBLIC
345 }
346 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
347 const videoUUID = videoImported.uuid
348
349 await waitJobs(servers)
350
351 // test resolution
352 const video = await servers[0].videos.get({ id: videoUUID })
353 expect(video.name).to.equal('hdr video')
354 const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id }))
355 expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P)
356 })
357
358 it('Should not import resolution higher than enabled transcoding resolution', async function () {
359 this.timeout(300_000)
360
361 const config: DeepPartial<CustomConfig> = {
362 transcoding: {
363 enabled: true,
364 resolutions: {
365 '0p': false,
366 '144p': true,
367 '240p': false,
368 '360p': false,
369 '480p': false,
370 '720p': false,
371 '1080p': false,
372 '1440p': false,
373 '2160p': false
374 },
375 alwaysTranscodeOriginalResolution: false
376 }
377 }
378 await servers[0].config.updateExistingSubConfig({ newConfig: config })
379
380 const attributes = {
381 name: 'small resolution video',
382 targetUrl: FIXTURE_URLS.youtube,
383 channelId: servers[0].store.channel.id,
384 privacy: VideoPrivacy.PUBLIC
385 }
386 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
387 const videoUUID = videoImported.uuid
388
389 await waitJobs(servers)
390
391 // test resolution
392 const video = await servers[0].videos.get({ id: videoUUID })
393 expect(video.name).to.equal('small resolution video')
394 expect(video.files).to.have.lengthOf(1)
395 expect(video.files[0].resolution.id).to.equal(144)
396 })
397
398 it('Should import resolution higher than enabled transcoding resolution', async function () {
399 this.timeout(300_000)
400
401 const config: DeepPartial<CustomConfig> = {
402 transcoding: {
403 alwaysTranscodeOriginalResolution: true
404 }
405 }
406 await servers[0].config.updateExistingSubConfig({ newConfig: config })
407
408 const attributes = {
409 name: 'bigger resolution video',
410 targetUrl: FIXTURE_URLS.youtube,
411 channelId: servers[0].store.channel.id,
412 privacy: VideoPrivacy.PUBLIC
413 }
414 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
415 const videoUUID = videoImported.uuid
416
417 await waitJobs(servers)
418
419 // test resolution
420 const video = await servers[0].videos.get({ id: videoUUID })
421 expect(video.name).to.equal('bigger resolution video')
422
423 expect(video.files).to.have.lengthOf(2)
424 expect(video.files.find(f => f.resolution.id === 240)).to.exist
425 expect(video.files.find(f => f.resolution.id === 144)).to.exist
426 })
427
428 it('Should import a peertube video', async function () {
429 this.timeout(120_000)
430
431 const toTest = [ FIXTURE_URLS.peertube_long ]
432
433 // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged
434 if (mode === 'yt-dlp') {
435 toTest.push(FIXTURE_URLS.peertube_short)
436 }
437
438 for (const targetUrl of toTest) {
439 await servers[0].config.disableTranscoding()
440
441 const attributes = {
442 targetUrl,
443 channelId: servers[0].store.channel.id,
444 privacy: VideoPrivacy.PUBLIC
445 }
446 const { video } = await servers[0].imports.importVideo({ attributes })
447 const videoUUID = video.uuid
448
449 await waitJobs(servers)
450
451 for (const server of servers) {
452 const video = await server.videos.get({ id: videoUUID })
453
454 expect(video.name).to.equal('E2E tests')
455
456 const { data: captions } = await server.captions.list({ videoId: videoUUID })
457 expect(captions).to.have.lengthOf(1)
458 expect(captions[0].language.id).to.equal('fr')
459
460 const str = `WEBVTT FILE\r?\n\r?\n` +
461 `1\r?\n` +
462 `00:00:04.000 --> 00:00:09.000\r?\n` +
463 `January 1, 1994. The North American`
464 await testCaptionFile(server.url, captions[0].captionPath, new RegExp(str))
465 }
466 }
467 })
468
469 after(async function () {
470 await cleanupTests(servers)
471 })
472 })
473 }
474
475 // FIXME: youtube-dl seems broken
476 // runSuite('youtube-dl')
477
478 runSuite('yt-dlp')
479
480 describe('Delete/cancel an import', function () {
481 let server: PeerTubeServer
482
483 let finishedImportId: number
484 let finishedVideo: Video
485 let pendingImportId: number
486
487 async function importVideo (name: string) {
488 const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
489 const res = await server.imports.importVideo({ attributes })
490
491 return res.id
492 }
493
494 before(async function () {
495 this.timeout(120_000)
496
497 server = await createSingleServer(1)
498
499 await setAccessTokensToServers([ server ])
500 await setDefaultVideoChannel([ server ])
501
502 finishedImportId = await importVideo('finished')
503 await waitJobs([ server ])
504
505 await server.jobs.pauseJobQueue()
506 pendingImportId = await importVideo('pending')
507
508 const { data } = await server.imports.getMyVideoImports()
509 expect(data).to.have.lengthOf(2)
510
511 finishedVideo = data.find(i => i.id === finishedImportId).video
512 })
513
514 it('Should delete a video import', async function () {
515 await server.imports.delete({ importId: finishedImportId })
516
517 const { data } = await server.imports.getMyVideoImports()
518 expect(data).to.have.lengthOf(1)
519 expect(data[0].id).to.equal(pendingImportId)
520 expect(data[0].state.id).to.equal(VideoImportState.PENDING)
521 })
522
523 it('Should not have deleted the associated video', async function () {
524 const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
525 expect(video.name).to.equal('finished')
526 expect(video.state.id).to.equal(VideoState.PUBLISHED)
527 })
528
529 it('Should cancel a video import', async function () {
530 await server.imports.cancel({ importId: pendingImportId })
531
532 const { data } = await server.imports.getMyVideoImports()
533 expect(data).to.have.lengthOf(1)
534 expect(data[0].id).to.equal(pendingImportId)
535 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
536 })
537
538 it('Should not have processed the cancelled video import', async function () {
539 this.timeout(60_000)
540
541 await server.jobs.resumeJobQueue()
542
543 await waitJobs([ server ])
544
545 const { data } = await server.imports.getMyVideoImports()
546 expect(data).to.have.lengthOf(1)
547 expect(data[0].id).to.equal(pendingImportId)
548 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
549 expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT)
550 })
551
552 it('Should delete the cancelled video import', async function () {
553 await server.imports.delete({ importId: pendingImportId })
554 const { data } = await server.imports.getMyVideoImports()
555 expect(data).to.have.lengthOf(0)
556 })
557
558 after(async function () {
559 await cleanupTests([ server ])
560 })
561 })
562
563 describe('Auto update', function () {
564 let server: PeerTubeServer
565
566 function quickPeerTubeImport () {
567 const attributes = {
568 targetUrl: FIXTURE_URLS.peertube_long,
569 channelId: server.store.channel.id,
570 privacy: VideoPrivacy.PUBLIC
571 }
572
573 return server.imports.importVideo({ attributes })
574 }
575
576 async function testBinaryUpdate (releaseUrl: string, releaseName: string) {
577 await remove(join(server.servers.buildDirectory('bin'), releaseName))
578
579 await server.kill()
580 await server.run({
581 import: {
582 videos: {
583 http: {
584 youtube_dl_release: {
585 url: releaseUrl,
586 name: releaseName
587 }
588 }
589 }
590 }
591 })
592
593 await quickPeerTubeImport()
594
595 const base = server.servers.buildDirectory('bin')
596 const content = await readdir(base)
597 const binaryPath = join(base, releaseName)
598
599 expect(await pathExists(binaryPath), `${binaryPath} does not exist in ${base} (${content.join(', ')})`).to.be.true
600 }
601
602 before(async function () {
603 this.timeout(30_000)
604
605 // Run servers
606 server = await createSingleServer(1)
607
608 await setAccessTokensToServers([ server ])
609 await setDefaultVideoChannel([ server ])
610 })
611
612 it('Should update youtube-dl from github URL', async function () {
613 this.timeout(120_000)
614
615 await testBinaryUpdate('https://api.github.com/repos/ytdl-org/youtube-dl/releases', 'youtube-dl')
616 })
617
618 // FIXME: official instance is broken
619 // it('Should update youtube-dl from raw URL', async function () {
620 // this.timeout(120_000)
621
622 // await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl')
623 // })
624
625 it('Should update youtube-dl from youtube-dl fork', async function () {
626 this.timeout(120_000)
627
628 await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp')
629 })
630
631 after(async function () {
632 await cleanupTests([ server ])
633 })
634 })
635})
diff --git a/packages/tests/src/api/videos/video-nsfw.ts b/packages/tests/src/api/videos/video-nsfw.ts
new file mode 100644
index 000000000..fc5225dd2
--- /dev/null
+++ b/packages/tests/src/api/videos/video-nsfw.ts
@@ -0,0 +1,227 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
5import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@peertube/peertube-models'
6
7function createOverviewRes (overview: VideosOverview) {
8 const videos = overview.categories[0].videos
9 return { data: videos, total: videos.length }
10}
11
12describe('Test video NSFW policy', function () {
13 let server: PeerTubeServer
14 let userAccessToken: string
15 let customConfig: CustomConfig
16
17 async function getVideosFunctions (token?: string, query: { nsfw?: BooleanBothQuery } = {}) {
18 const user = await server.users.getMyInfo()
19
20 const channelName = user.videoChannels[0].name
21 const accountName = user.account.name + '@' + user.account.host
22
23 const hasQuery = Object.keys(query).length !== 0
24 let promises: Promise<ResultList<Video>>[]
25
26 if (token) {
27 promises = [
28 server.search.advancedVideoSearch({ token, search: { search: 'n', sort: '-publishedAt', ...query } }),
29 server.videos.listWithToken({ token, ...query }),
30 server.videos.listByAccount({ token, handle: accountName, ...query }),
31 server.videos.listByChannel({ token, handle: channelName, ...query })
32 ]
33
34 // Overviews do not support video filters
35 if (!hasQuery) {
36 const p = server.overviews.getVideos({ page: 1, token })
37 .then(res => createOverviewRes(res))
38 promises.push(p)
39 }
40
41 return Promise.all(promises)
42 }
43
44 promises = [
45 server.search.searchVideos({ search: 'n', sort: '-publishedAt' }),
46 server.videos.list(),
47 server.videos.listByAccount({ token: null, handle: accountName }),
48 server.videos.listByChannel({ token: null, handle: channelName })
49 ]
50
51 // Overviews do not support video filters
52 if (!hasQuery) {
53 const p = server.overviews.getVideos({ page: 1 })
54 .then(res => createOverviewRes(res))
55 promises.push(p)
56 }
57
58 return Promise.all(promises)
59 }
60
61 before(async function () {
62 this.timeout(50000)
63 server = await createSingleServer(1)
64
65 // Get the access tokens
66 await setAccessTokensToServers([ server ])
67
68 {
69 const attributes = { name: 'nsfw', nsfw: true, category: 1 }
70 await server.videos.upload({ attributes })
71 }
72
73 {
74 const attributes = { name: 'normal', nsfw: false, category: 1 }
75 await server.videos.upload({ attributes })
76 }
77
78 customConfig = await server.config.getCustomConfig()
79 })
80
81 describe('Instance default NSFW policy', function () {
82
83 it('Should display NSFW videos with display default NSFW policy', async function () {
84 const serverConfig = await server.config.getConfig()
85 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
86
87 for (const body of await getVideosFunctions()) {
88 expect(body.total).to.equal(2)
89
90 const videos = body.data
91 expect(videos).to.have.lengthOf(2)
92 expect(videos[0].name).to.equal('normal')
93 expect(videos[1].name).to.equal('nsfw')
94 }
95 })
96
97 it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
98 customConfig.instance.defaultNSFWPolicy = 'do_not_list'
99 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
100
101 const serverConfig = await server.config.getConfig()
102 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
103
104 for (const body of await getVideosFunctions()) {
105 expect(body.total).to.equal(1)
106
107 const videos = body.data
108 expect(videos).to.have.lengthOf(1)
109 expect(videos[0].name).to.equal('normal')
110 }
111 })
112
113 it('Should display NSFW videos with blur default NSFW policy', async function () {
114 customConfig.instance.defaultNSFWPolicy = 'blur'
115 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
116
117 const serverConfig = await server.config.getConfig()
118 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
119
120 for (const body of await getVideosFunctions()) {
121 expect(body.total).to.equal(2)
122
123 const videos = body.data
124 expect(videos).to.have.lengthOf(2)
125 expect(videos[0].name).to.equal('normal')
126 expect(videos[1].name).to.equal('nsfw')
127 }
128 })
129 })
130
131 describe('User NSFW policy', function () {
132
133 it('Should create a user having the default nsfw policy', async function () {
134 const username = 'user1'
135 const password = 'my super password'
136 await server.users.create({ username, password })
137
138 userAccessToken = await server.login.getAccessToken({ username, password })
139
140 const user = await server.users.getMyInfo({ token: userAccessToken })
141 expect(user.nsfwPolicy).to.equal('blur')
142 })
143
144 it('Should display NSFW videos with blur user NSFW policy', async function () {
145 customConfig.instance.defaultNSFWPolicy = 'do_not_list'
146 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
147
148 for (const body of await getVideosFunctions(userAccessToken)) {
149 expect(body.total).to.equal(2)
150
151 const videos = body.data
152 expect(videos).to.have.lengthOf(2)
153 expect(videos[0].name).to.equal('normal')
154 expect(videos[1].name).to.equal('nsfw')
155 }
156 })
157
158 it('Should display NSFW videos with display user NSFW policy', async function () {
159 await server.users.updateMe({ nsfwPolicy: 'display' })
160
161 for (const body of await getVideosFunctions(server.accessToken)) {
162 expect(body.total).to.equal(2)
163
164 const videos = body.data
165 expect(videos).to.have.lengthOf(2)
166 expect(videos[0].name).to.equal('normal')
167 expect(videos[1].name).to.equal('nsfw')
168 }
169 })
170
171 it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
172 await server.users.updateMe({ nsfwPolicy: 'do_not_list' })
173
174 for (const body of await getVideosFunctions(server.accessToken)) {
175 expect(body.total).to.equal(1)
176
177 const videos = body.data
178 expect(videos).to.have.lengthOf(1)
179 expect(videos[0].name).to.equal('normal')
180 }
181 })
182
183 it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
184 const { total, data } = await server.videos.listMyVideos()
185 expect(total).to.equal(2)
186
187 expect(data).to.have.lengthOf(2)
188 expect(data[0].name).to.equal('normal')
189 expect(data[1].name).to.equal('nsfw')
190 })
191
192 it('Should display NSFW videos when the nsfw param === true', async function () {
193 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'true' })) {
194 expect(body.total).to.equal(1)
195
196 const videos = body.data
197 expect(videos).to.have.lengthOf(1)
198 expect(videos[0].name).to.equal('nsfw')
199 }
200 })
201
202 it('Should hide NSFW videos when the nsfw param === true', async function () {
203 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'false' })) {
204 expect(body.total).to.equal(1)
205
206 const videos = body.data
207 expect(videos).to.have.lengthOf(1)
208 expect(videos[0].name).to.equal('normal')
209 }
210 })
211
212 it('Should display both videos when the nsfw param === both', async function () {
213 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) {
214 expect(body.total).to.equal(2)
215
216 const videos = body.data
217 expect(videos).to.have.lengthOf(2)
218 expect(videos[0].name).to.equal('normal')
219 expect(videos[1].name).to.equal('nsfw')
220 }
221 })
222 })
223
224 after(async function () {
225 await cleanupTests([ server ])
226 })
227})
diff --git a/packages/tests/src/api/videos/video-passwords.ts b/packages/tests/src/api/videos/video-passwords.ts
new file mode 100644
index 000000000..60e0e28bd
--- /dev/null
+++ b/packages/tests/src/api/videos/video-passwords.ts
@@ -0,0 +1,97 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 VideoPasswordsCommand,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultAccountAvatar,
11 setDefaultChannelAvatar
12} from '@peertube/peertube-server-commands'
13import { VideoPrivacy } from '@peertube/peertube-models'
14
15describe('Test video passwords', function () {
16 let server: PeerTubeServer
17 let videoUUID: string
18
19 let userAccessTokenServer1: string
20
21 let videoPasswords: string[] = []
22 let command: VideoPasswordsCommand
23
24 before(async function () {
25 this.timeout(30000)
26
27 server = await createSingleServer(1)
28
29 await setAccessTokensToServers([ server ])
30
31 for (let i = 0; i < 10; i++) {
32 videoPasswords.push(`password ${i + 1}`)
33 }
34 const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } })
35 videoUUID = uuid
36
37 await setDefaultChannelAvatar(server)
38 await setDefaultAccountAvatar(server)
39
40 userAccessTokenServer1 = await server.users.generateUserAndToken('user1')
41 await setDefaultChannelAvatar(server, 'user1_channel')
42 await setDefaultAccountAvatar(server, userAccessTokenServer1)
43
44 command = server.videoPasswords
45 })
46
47 it('Should list video passwords', async function () {
48 const body = await command.list({ videoId: videoUUID })
49
50 expect(body.total).to.equal(10)
51 expect(body.data).to.be.an('array')
52 expect(body.data).to.have.lengthOf(10)
53 })
54
55 it('Should filter passwords on this video', async function () {
56 const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' })
57
58 expect(body.total).to.equal(10)
59 expect(body.data).to.be.an('array')
60 expect(body.data).to.have.lengthOf(2)
61 expect(body.data[0].password).to.equal('password 4')
62 expect(body.data[1].password).to.equal('password 5')
63 })
64
65 it('Should update password for this video', async function () {
66 videoPasswords = [ 'my super new password 1', 'my super new password 2' ]
67
68 await command.updateAll({ videoId: videoUUID, passwords: videoPasswords })
69 const body = await command.list({ videoId: videoUUID })
70 expect(body.total).to.equal(2)
71 expect(body.data).to.be.an('array')
72 expect(body.data).to.have.lengthOf(2)
73 expect(body.data[0].password).to.equal('my super new password 2')
74 expect(body.data[1].password).to.equal('my super new password 1')
75 })
76
77 it('Should delete one password', async function () {
78 {
79 const body = await command.list({ videoId: videoUUID })
80 expect(body.total).to.equal(2)
81 expect(body.data).to.be.an('array')
82 expect(body.data).to.have.lengthOf(2)
83 await command.remove({ id: body.data[0].id, videoId: videoUUID })
84 }
85 {
86 const body = await command.list({ videoId: videoUUID })
87
88 expect(body.total).to.equal(1)
89 expect(body.data).to.be.an('array')
90 expect(body.data).to.have.lengthOf(1)
91 }
92 })
93
94 after(async function () {
95 await cleanupTests([ server ])
96 })
97})
diff --git a/packages/tests/src/api/videos/video-playlist-thumbnails.ts b/packages/tests/src/api/videos/video-playlist-thumbnails.ts
new file mode 100644
index 000000000..d79c92f72
--- /dev/null
+++ b/packages/tests/src/api/videos/video-playlist-thumbnails.ts
@@ -0,0 +1,234 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
5import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('Playlist thumbnail', function () {
17 let servers: PeerTubeServer[] = []
18
19 let playlistWithoutThumbnailId: number
20 let playlistWithThumbnailId: number
21
22 let withThumbnailE1: number
23 let withThumbnailE2: number
24 let withoutThumbnailE1: number
25 let withoutThumbnailE2: number
26
27 let video1: number
28 let video2: number
29
30 async function getPlaylistWithoutThumbnail (server: PeerTubeServer) {
31 const body = await server.playlists.list({ start: 0, count: 10 })
32
33 return body.data.find(p => p.displayName === 'playlist without thumbnail')
34 }
35
36 async function getPlaylistWithThumbnail (server: PeerTubeServer) {
37 const body = await server.playlists.list({ start: 0, count: 10 })
38
39 return body.data.find(p => p.displayName === 'playlist with thumbnail')
40 }
41
42 before(async function () {
43 this.timeout(120000)
44
45 servers = await createMultipleServers(2)
46
47 // Get the access tokens
48 await setAccessTokensToServers(servers)
49 await setDefaultVideoChannel(servers)
50
51 for (const server of servers) {
52 await server.config.disableTranscoding()
53 }
54
55 // Server 1 and server 2 follow each other
56 await doubleFollow(servers[0], servers[1])
57
58 video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).id
59 video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).id
60
61 await waitJobs(servers)
62 })
63
64 it('Should automatically update the thumbnail when adding an element', async function () {
65 this.timeout(30000)
66
67 const created = await servers[1].playlists.create({
68 attributes: {
69 displayName: 'playlist without thumbnail',
70 privacy: VideoPlaylistPrivacy.PUBLIC,
71 videoChannelId: servers[1].store.channel.id
72 }
73 })
74 playlistWithoutThumbnailId = created.id
75
76 const added = await servers[1].playlists.addElement({
77 playlistId: playlistWithoutThumbnailId,
78 attributes: { videoId: video1 }
79 })
80 withoutThumbnailE1 = added.id
81
82 await waitJobs(servers)
83
84 for (const server of servers) {
85 const p = await getPlaylistWithoutThumbnail(server)
86 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
87 }
88 })
89
90 it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () {
91 this.timeout(30000)
92
93 const created = await servers[1].playlists.create({
94 attributes: {
95 displayName: 'playlist with thumbnail',
96 privacy: VideoPlaylistPrivacy.PUBLIC,
97 videoChannelId: servers[1].store.channel.id,
98 thumbnailfile: 'custom-thumbnail.jpg'
99 }
100 })
101 playlistWithThumbnailId = created.id
102
103 const added = await servers[1].playlists.addElement({
104 playlistId: playlistWithThumbnailId,
105 attributes: { videoId: video1 }
106 })
107 withThumbnailE1 = added.id
108
109 await waitJobs(servers)
110
111 for (const server of servers) {
112 const p = await getPlaylistWithThumbnail(server)
113 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
114 }
115 })
116
117 it('Should automatically update the thumbnail when moving the first element', async function () {
118 this.timeout(30000)
119
120 const added = await servers[1].playlists.addElement({
121 playlistId: playlistWithoutThumbnailId,
122 attributes: { videoId: video2 }
123 })
124 withoutThumbnailE2 = added.id
125
126 await servers[1].playlists.reorderElements({
127 playlistId: playlistWithoutThumbnailId,
128 attributes: {
129 startPosition: 1,
130 insertAfterPosition: 2
131 }
132 })
133
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 const p = await getPlaylistWithoutThumbnail(server)
138 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
139 }
140 })
141
142 it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () {
143 this.timeout(30000)
144
145 const added = await servers[1].playlists.addElement({
146 playlistId: playlistWithThumbnailId,
147 attributes: { videoId: video2 }
148 })
149 withThumbnailE2 = added.id
150
151 await servers[1].playlists.reorderElements({
152 playlistId: playlistWithThumbnailId,
153 attributes: {
154 startPosition: 1,
155 insertAfterPosition: 2
156 }
157 })
158
159 await waitJobs(servers)
160
161 for (const server of servers) {
162 const p = await getPlaylistWithThumbnail(server)
163 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
164 }
165 })
166
167 it('Should automatically update the thumbnail when deleting the first element', async function () {
168 this.timeout(30000)
169
170 await servers[1].playlists.removeElement({
171 playlistId: playlistWithoutThumbnailId,
172 elementId: withoutThumbnailE1
173 })
174
175 await waitJobs(servers)
176
177 for (const server of servers) {
178 const p = await getPlaylistWithoutThumbnail(server)
179 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
180 }
181 })
182
183 it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () {
184 this.timeout(30000)
185
186 await servers[1].playlists.removeElement({
187 playlistId: playlistWithThumbnailId,
188 elementId: withThumbnailE1
189 })
190
191 await waitJobs(servers)
192
193 for (const server of servers) {
194 const p = await getPlaylistWithThumbnail(server)
195 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
196 }
197 })
198
199 it('Should the thumbnail when we delete the last element', async function () {
200 this.timeout(30000)
201
202 await servers[1].playlists.removeElement({
203 playlistId: playlistWithoutThumbnailId,
204 elementId: withoutThumbnailE2
205 })
206
207 await waitJobs(servers)
208
209 for (const server of servers) {
210 const p = await getPlaylistWithoutThumbnail(server)
211 expect(p.thumbnailPath).to.be.null
212 }
213 })
214
215 it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () {
216 this.timeout(30000)
217
218 await servers[1].playlists.removeElement({
219 playlistId: playlistWithThumbnailId,
220 elementId: withThumbnailE2
221 })
222
223 await waitJobs(servers)
224
225 for (const server of servers) {
226 const p = await getPlaylistWithThumbnail(server)
227 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
228 }
229 })
230
231 after(async function () {
232 await cleanupTests(servers)
233 })
234})
diff --git a/packages/tests/src/api/videos/video-playlists.ts b/packages/tests/src/api/videos/video-playlists.ts
new file mode 100644
index 000000000..578d01093
--- /dev/null
+++ b/packages/tests/src/api/videos/video-playlists.ts
@@ -0,0 +1,1210 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import {
6 HttpStatusCode,
7 VideoPlaylist,
8 VideoPlaylistCreateResult,
9 VideoPlaylistElementType,
10 VideoPlaylistElementType_Type,
11 VideoPlaylistPrivacy,
12 VideoPlaylistType,
13 VideoPrivacy
14} from '@peertube/peertube-models'
15import { uuidToShort } from '@peertube/peertube-node-utils'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 PeerTubeServer,
21 PlaylistsCommand,
22 setAccessTokensToServers,
23 setDefaultAccountAvatar,
24 setDefaultVideoChannel,
25 waitJobs
26} from '@peertube/peertube-server-commands'
27import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
28import { checkPlaylistFilesWereRemoved } from '@tests/shared/video-playlists.js'
29
30async function checkPlaylistElementType (
31 servers: PeerTubeServer[],
32 playlistId: string,
33 type: VideoPlaylistElementType_Type,
34 position: number,
35 name: string,
36 total: number
37) {
38 for (const server of servers) {
39 const body = await server.playlists.listVideos({ token: server.accessToken, playlistId, start: 0, count: 10 })
40 expect(body.total).to.equal(total)
41
42 const videoElement = body.data.find(e => e.position === position)
43 expect(videoElement.type).to.equal(type, 'On server ' + server.url)
44
45 if (type === VideoPlaylistElementType.REGULAR) {
46 expect(videoElement.video).to.not.be.null
47 expect(videoElement.video.name).to.equal(name)
48 } else {
49 expect(videoElement.video).to.be.null
50 }
51 }
52}
53
54describe('Test video playlists', function () {
55 let servers: PeerTubeServer[] = []
56
57 let playlistServer2Id1: number
58 let playlistServer2Id2: number
59 let playlistServer2UUID2: string
60
61 let playlistServer1Id: number
62 let playlistServer1DisplayName: string
63 let playlistServer1UUID: string
64 let playlistServer1UUID2: string
65
66 let playlistElementServer1Video4: number
67 let playlistElementServer1Video5: number
68 let playlistElementNSFW: number
69
70 let nsfwVideoServer1: number
71
72 let userTokenServer1: string
73
74 let commands: PlaylistsCommand[]
75
76 before(async function () {
77 this.timeout(240000)
78
79 servers = await createMultipleServers(3)
80
81 // Get the access tokens
82 await setAccessTokensToServers(servers)
83 await setDefaultVideoChannel(servers)
84 await setDefaultAccountAvatar(servers)
85
86 for (const server of servers) {
87 await server.config.disableTranscoding()
88 }
89
90 // Server 1 and server 2 follow each other
91 await doubleFollow(servers[0], servers[1])
92 // Server 1 and server 3 follow each other
93 await doubleFollow(servers[0], servers[2])
94
95 commands = servers.map(s => s.playlists)
96
97 {
98 servers[0].store.videos = []
99 servers[1].store.videos = []
100 servers[2].store.videos = []
101
102 for (const server of servers) {
103 for (let i = 0; i < 7; i++) {
104 const name = `video ${i} server ${server.serverNumber}`
105 const video = await server.videos.upload({ attributes: { name, nsfw: false } })
106
107 server.store.videos.push(video)
108 }
109 }
110 }
111
112 nsfwVideoServer1 = (await servers[0].videos.quickUpload({ name: 'NSFW video', nsfw: true })).id
113
114 userTokenServer1 = await servers[0].users.generateUserAndToken('user1')
115
116 await waitJobs(servers)
117 })
118
119 describe('Check playlists filters and privacies', function () {
120
121 it('Should list video playlist privacies', async function () {
122 const privacies = await commands[0].getPrivacies()
123
124 expect(Object.keys(privacies)).to.have.length.at.least(3)
125 expect(privacies[3]).to.equal('Private')
126 })
127
128 it('Should filter on playlist type', async function () {
129 this.timeout(30000)
130
131 const token = servers[0].accessToken
132
133 await commands[0].create({
134 attributes: {
135 displayName: 'my super playlist',
136 privacy: VideoPlaylistPrivacy.PUBLIC,
137 description: 'my super description',
138 thumbnailfile: 'custom-thumbnail.jpg',
139 videoChannelId: servers[0].store.channel.id
140 }
141 })
142
143 {
144 const body = await commands[0].listByAccount({ token, handle: 'root', playlistType: VideoPlaylistType.WATCH_LATER })
145
146 expect(body.total).to.equal(1)
147 expect(body.data).to.have.lengthOf(1)
148
149 const playlist = body.data[0]
150 expect(playlist.displayName).to.equal('Watch later')
151 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
152 expect(playlist.type.label).to.equal('Watch later')
153 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
154 }
155
156 {
157 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.WATCH_LATER })
158 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.WATCH_LATER })
159
160 for (const body of [ bodyList, bodyChannel ]) {
161 expect(body.total).to.equal(0)
162 expect(body.data).to.have.lengthOf(0)
163 }
164 }
165
166 {
167 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR })
168 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR })
169
170 let playlist: VideoPlaylist = null
171 for (const body of [ bodyList, bodyChannel ]) {
172
173 expect(body.total).to.equal(1)
174 expect(body.data).to.have.lengthOf(1)
175
176 playlist = body.data[0]
177 expect(playlist.displayName).to.equal('my super playlist')
178 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
179 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
180 }
181
182 await commands[0].update({
183 playlistId: playlist.id,
184 attributes: {
185 privacy: VideoPlaylistPrivacy.PRIVATE
186 }
187 })
188 }
189
190 {
191 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR })
192 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR })
193
194 for (const body of [ bodyList, bodyChannel ]) {
195 expect(body.total).to.equal(0)
196 expect(body.data).to.have.lengthOf(0)
197 }
198 }
199
200 {
201 const body = await commands[0].listByAccount({ handle: 'root' })
202 expect(body.total).to.equal(0)
203 expect(body.data).to.have.lengthOf(0)
204 }
205 })
206
207 it('Should get private playlist for a classic user', async function () {
208 const token = await servers[0].users.generateUserAndToken('toto')
209
210 const body = await commands[0].listByAccount({ token, handle: 'toto' })
211
212 expect(body.total).to.equal(1)
213 expect(body.data).to.have.lengthOf(1)
214
215 const playlistId = body.data[0].id
216 await commands[0].listVideos({ token, playlistId })
217 })
218 })
219
220 describe('Create and federate playlists', function () {
221
222 it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
223 this.timeout(30000)
224
225 await commands[0].create({
226 attributes: {
227 displayName: 'my super playlist',
228 privacy: VideoPlaylistPrivacy.PUBLIC,
229 description: 'my super description',
230 thumbnailfile: 'custom-thumbnail.jpg',
231 videoChannelId: servers[0].store.channel.id
232 }
233 })
234
235 await waitJobs(servers)
236 // Processing a playlist by the receiver could be long
237 await wait(3000)
238
239 for (const server of servers) {
240 const body = await server.playlists.list({ start: 0, count: 5 })
241 expect(body.total).to.equal(1)
242 expect(body.data).to.have.lengthOf(1)
243
244 const playlistFromList = body.data[0]
245
246 const playlistFromGet = await server.playlists.get({ playlistId: playlistFromList.uuid })
247
248 for (const playlist of [ playlistFromGet, playlistFromList ]) {
249 expect(playlist.id).to.be.a('number')
250 expect(playlist.uuid).to.be.a('string')
251
252 expect(playlist.isLocal).to.equal(server.serverNumber === 1)
253
254 expect(playlist.displayName).to.equal('my super playlist')
255 expect(playlist.description).to.equal('my super description')
256 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
257 expect(playlist.privacy.label).to.equal('Public')
258 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
259 expect(playlist.type.label).to.equal('Regular')
260 expect(playlist.embedPath).to.equal('/video-playlists/embed/' + playlist.uuid)
261
262 expect(playlist.videosLength).to.equal(0)
263
264 expect(playlist.ownerAccount.name).to.equal('root')
265 expect(playlist.ownerAccount.displayName).to.equal('root')
266 expect(playlist.videoChannel.name).to.equal('root_channel')
267 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
268 }
269 }
270 })
271
272 it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
273 this.timeout(30000)
274
275 {
276 const playlist = await servers[1].playlists.create({
277 attributes: {
278 displayName: 'playlist 2',
279 privacy: VideoPlaylistPrivacy.PUBLIC,
280 videoChannelId: servers[1].store.channel.id
281 }
282 })
283 playlistServer2Id1 = playlist.id
284 }
285
286 {
287 const playlist = await servers[1].playlists.create({
288 attributes: {
289 displayName: 'playlist 3',
290 privacy: VideoPlaylistPrivacy.PUBLIC,
291 thumbnailfile: 'custom-thumbnail.jpg',
292 videoChannelId: servers[1].store.channel.id
293 }
294 })
295
296 playlistServer2Id2 = playlist.id
297 playlistServer2UUID2 = playlist.uuid
298 }
299
300 for (const id of [ playlistServer2Id1, playlistServer2Id2 ]) {
301 await servers[1].playlists.addElement({
302 playlistId: id,
303 attributes: { videoId: servers[1].store.videos[0].id, startTimestamp: 1, stopTimestamp: 2 }
304 })
305 await servers[1].playlists.addElement({
306 playlistId: id,
307 attributes: { videoId: servers[1].store.videos[1].id }
308 })
309 }
310
311 await waitJobs(servers)
312 await wait(3000)
313
314 for (const server of [ servers[0], servers[1] ]) {
315 const body = await server.playlists.list({ start: 0, count: 5 })
316
317 const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
318 expect(playlist2).to.not.be.undefined
319 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
320
321 const playlist3 = body.data.find(p => p.displayName === 'playlist 3')
322 expect(playlist3).to.not.be.undefined
323 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', playlist3.thumbnailPath)
324 }
325
326 const body = await servers[2].playlists.list({ start: 0, count: 5 })
327 expect(body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
328 expect(body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
329 })
330
331 it('Should have the playlist on server 3 after a new follow', async function () {
332 this.timeout(30000)
333
334 // Server 2 and server 3 follow each other
335 await doubleFollow(servers[1], servers[2])
336
337 const body = await servers[2].playlists.list({ start: 0, count: 5 })
338
339 const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
340 expect(playlist2).to.not.be.undefined
341 await testImageGeneratedByFFmpeg(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
342
343 expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
344 })
345 })
346
347 describe('List playlists', function () {
348
349 it('Should correctly list the playlists', async function () {
350 this.timeout(30000)
351
352 {
353 const body = await servers[2].playlists.list({ start: 1, count: 2, sort: 'createdAt' })
354 expect(body.total).to.equal(3)
355
356 const data = body.data
357 expect(data).to.have.lengthOf(2)
358 expect(data[0].displayName).to.equal('playlist 2')
359 expect(data[1].displayName).to.equal('playlist 3')
360 }
361
362 {
363 const body = await servers[2].playlists.list({ start: 1, count: 2, sort: '-createdAt' })
364 expect(body.total).to.equal(3)
365
366 const data = body.data
367 expect(data).to.have.lengthOf(2)
368 expect(data[0].displayName).to.equal('playlist 2')
369 expect(data[1].displayName).to.equal('my super playlist')
370 }
371 })
372
373 it('Should list video channel playlists', async function () {
374 this.timeout(30000)
375
376 {
377 const body = await commands[0].listByChannel({ handle: 'root_channel', start: 0, count: 2, sort: '-createdAt' })
378 expect(body.total).to.equal(1)
379
380 const data = body.data
381 expect(data).to.have.lengthOf(1)
382 expect(data[0].displayName).to.equal('my super playlist')
383 }
384 })
385
386 it('Should list account playlists', async function () {
387 this.timeout(30000)
388
389 {
390 const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: '-createdAt' })
391 expect(body.total).to.equal(2)
392
393 const data = body.data
394 expect(data).to.have.lengthOf(1)
395 expect(data[0].displayName).to.equal('playlist 2')
396 }
397
398 {
399 const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: 'createdAt' })
400 expect(body.total).to.equal(2)
401
402 const data = body.data
403 expect(data).to.have.lengthOf(1)
404 expect(data[0].displayName).to.equal('playlist 3')
405 }
406
407 {
408 const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '3' })
409 expect(body.total).to.equal(1)
410
411 const data = body.data
412 expect(data).to.have.lengthOf(1)
413 expect(data[0].displayName).to.equal('playlist 3')
414 }
415
416 {
417 const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '4' })
418 expect(body.total).to.equal(0)
419
420 const data = body.data
421 expect(data).to.have.lengthOf(0)
422 }
423 })
424 })
425
426 describe('Playlist rights', function () {
427 let unlistedPlaylist: VideoPlaylistCreateResult
428 let privatePlaylist: VideoPlaylistCreateResult
429
430 before(async function () {
431 this.timeout(30000)
432
433 {
434 unlistedPlaylist = await servers[1].playlists.create({
435 attributes: {
436 displayName: 'playlist unlisted',
437 privacy: VideoPlaylistPrivacy.UNLISTED,
438 videoChannelId: servers[1].store.channel.id
439 }
440 })
441 }
442
443 {
444 privatePlaylist = await servers[1].playlists.create({
445 attributes: {
446 displayName: 'playlist private',
447 privacy: VideoPlaylistPrivacy.PRIVATE
448 }
449 })
450 }
451
452 await waitJobs(servers)
453 await wait(3000)
454 })
455
456 it('Should not list unlisted or private playlists', async function () {
457 for (const server of servers) {
458 const results = [
459 await server.playlists.listByAccount({ handle: 'root@' + servers[1].host, sort: '-createdAt' }),
460 await server.playlists.list({ start: 0, count: 2, sort: '-createdAt' })
461 ]
462
463 expect(results[0].total).to.equal(2)
464 expect(results[1].total).to.equal(3)
465
466 for (const body of results) {
467 const data = body.data
468 expect(data).to.have.lengthOf(2)
469 expect(data[0].displayName).to.equal('playlist 3')
470 expect(data[1].displayName).to.equal('playlist 2')
471 }
472 }
473 })
474
475 it('Should not get unlisted playlist using only the id', async function () {
476 await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 })
477 })
478
479 it('Should get unlisted playlist using uuid or shortUUID', async function () {
480 await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid })
481 await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID })
482 })
483
484 it('Should not get private playlist without token', async function () {
485 for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
486 await servers[1].playlists.get({ playlistId: id, expectedStatus: 401 })
487 }
488 })
489
490 it('Should get private playlist with a token', async function () {
491 for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
492 await servers[1].playlists.get({ token: servers[1].accessToken, playlistId: id })
493 }
494 })
495 })
496
497 describe('Update playlists', function () {
498
499 it('Should update a playlist', async function () {
500 this.timeout(30000)
501
502 await servers[1].playlists.update({
503 attributes: {
504 displayName: 'playlist 3 updated',
505 description: 'description updated',
506 privacy: VideoPlaylistPrivacy.UNLISTED,
507 thumbnailfile: 'custom-thumbnail.jpg',
508 videoChannelId: servers[1].store.channel.id
509 },
510 playlistId: playlistServer2Id2
511 })
512
513 await waitJobs(servers)
514
515 for (const server of servers) {
516 const playlist = await server.playlists.get({ playlistId: playlistServer2UUID2 })
517
518 expect(playlist.displayName).to.equal('playlist 3 updated')
519 expect(playlist.description).to.equal('description updated')
520
521 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
522 expect(playlist.privacy.label).to.equal('Unlisted')
523
524 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
525 expect(playlist.type.label).to.equal('Regular')
526
527 expect(playlist.videosLength).to.equal(2)
528
529 expect(playlist.ownerAccount.name).to.equal('root')
530 expect(playlist.ownerAccount.displayName).to.equal('root')
531 expect(playlist.videoChannel.name).to.equal('root_channel')
532 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
533 }
534 })
535 })
536
537 describe('Element timestamps', function () {
538
539 it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
540 this.timeout(30000)
541
542 const addVideo = (attributes: any) => {
543 return commands[0].addElement({ playlistId: playlistServer1Id, attributes })
544 }
545
546 const playlistDisplayName = 'playlist 4'
547 const playlist = await commands[0].create({
548 attributes: {
549 displayName: playlistDisplayName,
550 privacy: VideoPlaylistPrivacy.PUBLIC,
551 videoChannelId: servers[0].store.channel.id
552 }
553 })
554
555 playlistServer1Id = playlist.id
556 playlistServer1DisplayName = playlistDisplayName
557 playlistServer1UUID = playlist.uuid
558
559 await addVideo({ videoId: servers[0].store.videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
560 await addVideo({ videoId: servers[2].store.videos[1].uuid, startTimestamp: 35 })
561 await addVideo({ videoId: servers[2].store.videos[2].uuid })
562 {
563 const element = await addVideo({ videoId: servers[0].store.videos[3].uuid, stopTimestamp: 35 })
564 playlistElementServer1Video4 = element.id
565 }
566
567 {
568 const element = await addVideo({ videoId: servers[0].store.videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
569 playlistElementServer1Video5 = element.id
570 }
571
572 {
573 const element = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
574 playlistElementNSFW = element.id
575
576 await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 4 })
577 await addVideo({ videoId: nsfwVideoServer1 })
578 }
579
580 await waitJobs(servers)
581 })
582
583 it('Should correctly list playlist videos', async function () {
584 this.timeout(30000)
585
586 for (const server of servers) {
587 {
588 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
589
590 expect(body.total).to.equal(8)
591
592 const videoElements = body.data
593 expect(videoElements).to.have.lengthOf(8)
594
595 expect(videoElements[0].video.name).to.equal('video 0 server 1')
596 expect(videoElements[0].position).to.equal(1)
597 expect(videoElements[0].startTimestamp).to.equal(15)
598 expect(videoElements[0].stopTimestamp).to.equal(28)
599
600 expect(videoElements[1].video.name).to.equal('video 1 server 3')
601 expect(videoElements[1].position).to.equal(2)
602 expect(videoElements[1].startTimestamp).to.equal(35)
603 expect(videoElements[1].stopTimestamp).to.be.null
604
605 expect(videoElements[2].video.name).to.equal('video 2 server 3')
606 expect(videoElements[2].position).to.equal(3)
607 expect(videoElements[2].startTimestamp).to.be.null
608 expect(videoElements[2].stopTimestamp).to.be.null
609
610 expect(videoElements[3].video.name).to.equal('video 3 server 1')
611 expect(videoElements[3].position).to.equal(4)
612 expect(videoElements[3].startTimestamp).to.be.null
613 expect(videoElements[3].stopTimestamp).to.equal(35)
614
615 expect(videoElements[4].video.name).to.equal('video 4 server 1')
616 expect(videoElements[4].position).to.equal(5)
617 expect(videoElements[4].startTimestamp).to.equal(45)
618 expect(videoElements[4].stopTimestamp).to.equal(60)
619
620 expect(videoElements[5].video.name).to.equal('NSFW video')
621 expect(videoElements[5].position).to.equal(6)
622 expect(videoElements[5].startTimestamp).to.equal(5)
623 expect(videoElements[5].stopTimestamp).to.be.null
624
625 expect(videoElements[6].video.name).to.equal('NSFW video')
626 expect(videoElements[6].position).to.equal(7)
627 expect(videoElements[6].startTimestamp).to.equal(4)
628 expect(videoElements[6].stopTimestamp).to.be.null
629
630 expect(videoElements[7].video.name).to.equal('NSFW video')
631 expect(videoElements[7].position).to.equal(8)
632 expect(videoElements[7].startTimestamp).to.be.null
633 expect(videoElements[7].stopTimestamp).to.be.null
634 }
635
636 {
637 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 2 })
638 expect(body.data).to.have.lengthOf(2)
639 }
640 }
641 })
642 })
643
644 describe('Element type', function () {
645 let groupUser1: PeerTubeServer[]
646 let groupWithoutToken1: PeerTubeServer[]
647 let group1: PeerTubeServer[]
648 let group2: PeerTubeServer[]
649
650 let video1: string
651 let video2: string
652 let video3: string
653
654 before(async function () {
655 this.timeout(60000)
656
657 groupUser1 = [ Object.assign({}, servers[0], { accessToken: userTokenServer1 }) ]
658 groupWithoutToken1 = [ Object.assign({}, servers[0], { accessToken: undefined }) ]
659 group1 = [ servers[0] ]
660 group2 = [ servers[1], servers[2] ]
661
662 const playlist = await commands[0].create({
663 token: userTokenServer1,
664 attributes: {
665 displayName: 'playlist 56',
666 privacy: VideoPlaylistPrivacy.PUBLIC,
667 videoChannelId: servers[0].store.channel.id
668 }
669 })
670
671 const playlistServer1Id2 = playlist.id
672 playlistServer1UUID2 = playlist.uuid
673
674 const addVideo = (attributes: any) => {
675 return commands[0].addElement({ token: userTokenServer1, playlistId: playlistServer1Id2, attributes })
676 }
677
678 video1 = (await servers[0].videos.quickUpload({ name: 'video 89', token: userTokenServer1 })).uuid
679 video2 = (await servers[1].videos.quickUpload({ name: 'video 90' })).uuid
680 video3 = (await servers[0].videos.quickUpload({ name: 'video 91', nsfw: true })).uuid
681
682 await waitJobs(servers)
683
684 await addVideo({ videoId: video1, startTimestamp: 15, stopTimestamp: 28 })
685 await addVideo({ videoId: video2, startTimestamp: 35 })
686 await addVideo({ videoId: video3 })
687
688 await waitJobs(servers)
689 })
690
691 it('Should update the element type if the video is private/password protected', async function () {
692 this.timeout(20000)
693
694 const name = 'video 89'
695 const position = 1
696
697 {
698 await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PRIVATE } })
699 await waitJobs(servers)
700
701 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
702 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
703 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
704 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
705 }
706
707 {
708 await servers[0].videos.update({
709 id: video1,
710 attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] }
711 })
712 await waitJobs(servers)
713
714 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
715 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
716 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
717 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
718 }
719
720 {
721 await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } })
722 await waitJobs(servers)
723
724 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
725 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
726 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
727 // We deleted the video, so even if we recreated it, the old entry is still deleted
728 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
729 }
730 })
731
732 it('Should update the element type if the video is blacklisted', async function () {
733 this.timeout(20000)
734
735 const name = 'video 89'
736 const position = 1
737
738 {
739 await servers[0].blacklist.add({ videoId: video1, reason: 'reason', unfederate: true })
740 await waitJobs(servers)
741
742 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
743 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
744 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
745 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
746 }
747
748 {
749 await servers[0].blacklist.remove({ videoId: video1 })
750 await waitJobs(servers)
751
752 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
753 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
754 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
755 // We deleted the video (because unfederated), so even if we recreated it, the old entry is still deleted
756 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
757 }
758 })
759
760 it('Should update the element type if the account or server of the video is blocked', async function () {
761 this.timeout(90000)
762
763 const command = servers[0].blocklist
764
765 const name = 'video 90'
766 const position = 2
767
768 {
769 await command.addToMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host })
770 await waitJobs(servers)
771
772 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
773 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
774
775 await command.removeFromMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host })
776 await waitJobs(servers)
777
778 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
779 }
780
781 {
782 await command.addToMyBlocklist({ token: userTokenServer1, server: servers[1].host })
783 await waitJobs(servers)
784
785 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
786 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
787
788 await command.removeFromMyBlocklist({ token: userTokenServer1, server: servers[1].host })
789 await waitJobs(servers)
790
791 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
792 }
793
794 {
795 await command.addToServerBlocklist({ account: 'root@' + servers[1].host })
796 await waitJobs(servers)
797
798 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
799 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
800
801 await command.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
802 await waitJobs(servers)
803
804 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
805 }
806
807 {
808 await command.addToServerBlocklist({ server: servers[1].host })
809 await waitJobs(servers)
810
811 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
812 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
813
814 await command.removeFromServerBlocklist({ server: servers[1].host })
815 await waitJobs(servers)
816
817 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
818 }
819 })
820 })
821
822 describe('Managing playlist elements', function () {
823
824 it('Should reorder the playlist', async function () {
825 this.timeout(30000)
826
827 {
828 await commands[0].reorderElements({
829 playlistId: playlistServer1Id,
830 attributes: {
831 startPosition: 2,
832 insertAfterPosition: 3
833 }
834 })
835
836 await waitJobs(servers)
837
838 for (const server of servers) {
839 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
840 const names = body.data.map(v => v.video.name)
841
842 expect(names).to.deep.equal([
843 'video 0 server 1',
844 'video 2 server 3',
845 'video 1 server 3',
846 'video 3 server 1',
847 'video 4 server 1',
848 'NSFW video',
849 'NSFW video',
850 'NSFW video'
851 ])
852 }
853 }
854
855 {
856 await commands[0].reorderElements({
857 playlistId: playlistServer1Id,
858 attributes: {
859 startPosition: 1,
860 reorderLength: 3,
861 insertAfterPosition: 4
862 }
863 })
864
865 await waitJobs(servers)
866
867 for (const server of servers) {
868 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
869 const names = body.data.map(v => v.video.name)
870
871 expect(names).to.deep.equal([
872 'video 3 server 1',
873 'video 0 server 1',
874 'video 2 server 3',
875 'video 1 server 3',
876 'video 4 server 1',
877 'NSFW video',
878 'NSFW video',
879 'NSFW video'
880 ])
881 }
882 }
883
884 {
885 await commands[0].reorderElements({
886 playlistId: playlistServer1Id,
887 attributes: {
888 startPosition: 6,
889 insertAfterPosition: 3
890 }
891 })
892
893 await waitJobs(servers)
894
895 for (const server of servers) {
896 const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
897 const names = elements.map(v => v.video.name)
898
899 expect(names).to.deep.equal([
900 'video 3 server 1',
901 'video 0 server 1',
902 'video 2 server 3',
903 'NSFW video',
904 'video 1 server 3',
905 'video 4 server 1',
906 'NSFW video',
907 'NSFW video'
908 ])
909
910 for (let i = 1; i <= elements.length; i++) {
911 expect(elements[i - 1].position).to.equal(i)
912 }
913 }
914 }
915 })
916
917 it('Should update startTimestamp/endTimestamp of some elements', async function () {
918 this.timeout(30000)
919
920 await commands[0].updateElement({
921 playlistId: playlistServer1Id,
922 elementId: playlistElementServer1Video4,
923 attributes: {
924 startTimestamp: 1
925 }
926 })
927
928 await commands[0].updateElement({
929 playlistId: playlistServer1Id,
930 elementId: playlistElementServer1Video5,
931 attributes: {
932 stopTimestamp: null
933 }
934 })
935
936 await waitJobs(servers)
937
938 for (const server of servers) {
939 const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
940
941 expect(elements[0].video.name).to.equal('video 3 server 1')
942 expect(elements[0].position).to.equal(1)
943 expect(elements[0].startTimestamp).to.equal(1)
944 expect(elements[0].stopTimestamp).to.equal(35)
945
946 expect(elements[5].video.name).to.equal('video 4 server 1')
947 expect(elements[5].position).to.equal(6)
948 expect(elements[5].startTimestamp).to.equal(45)
949 expect(elements[5].stopTimestamp).to.be.null
950 }
951 })
952
953 it('Should check videos existence in my playlist', async function () {
954 const videoIds = [
955 servers[0].store.videos[0].id,
956 42000,
957 servers[0].store.videos[3].id,
958 43000,
959 servers[0].store.videos[4].id
960 ]
961 const obj = await commands[0].videosExist({ videoIds })
962
963 {
964 const elem = obj[servers[0].store.videos[0].id]
965 expect(elem).to.have.lengthOf(1)
966 expect(elem[0].playlistElementId).to.exist
967 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
968 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
969 expect(elem[0].playlistId).to.equal(playlistServer1Id)
970 expect(elem[0].startTimestamp).to.equal(15)
971 expect(elem[0].stopTimestamp).to.equal(28)
972 }
973
974 {
975 const elem = obj[servers[0].store.videos[3].id]
976 expect(elem).to.have.lengthOf(1)
977 expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4)
978 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
979 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
980 expect(elem[0].playlistId).to.equal(playlistServer1Id)
981 expect(elem[0].startTimestamp).to.equal(1)
982 expect(elem[0].stopTimestamp).to.equal(35)
983 }
984
985 {
986 const elem = obj[servers[0].store.videos[4].id]
987 expect(elem).to.have.lengthOf(1)
988 expect(elem[0].playlistId).to.equal(playlistServer1Id)
989 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
990 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
991 expect(elem[0].startTimestamp).to.equal(45)
992 expect(elem[0].stopTimestamp).to.equal(null)
993 }
994
995 expect(obj[42000]).to.have.lengthOf(0)
996 expect(obj[43000]).to.have.lengthOf(0)
997 })
998
999 it('Should automatically update updatedAt field of playlists', async function () {
1000 const server = servers[1]
1001 const videoId = servers[1].store.videos[5].id
1002
1003 async function getPlaylistNames () {
1004 const { data } = await server.playlists.listByAccount({ token: server.accessToken, handle: 'root', sort: '-updatedAt' })
1005
1006 return data.map(p => p.displayName)
1007 }
1008
1009 const attributes = { videoId }
1010 const element1 = await server.playlists.addElement({ playlistId: playlistServer2Id1, attributes })
1011 const element2 = await server.playlists.addElement({ playlistId: playlistServer2Id2, attributes })
1012
1013 const names1 = await getPlaylistNames()
1014 expect(names1[0]).to.equal('playlist 3 updated')
1015 expect(names1[1]).to.equal('playlist 2')
1016
1017 await server.playlists.removeElement({ playlistId: playlistServer2Id1, elementId: element1.id })
1018
1019 const names2 = await getPlaylistNames()
1020 expect(names2[0]).to.equal('playlist 2')
1021 expect(names2[1]).to.equal('playlist 3 updated')
1022
1023 await server.playlists.removeElement({ playlistId: playlistServer2Id2, elementId: element2.id })
1024
1025 const names3 = await getPlaylistNames()
1026 expect(names3[0]).to.equal('playlist 3 updated')
1027 expect(names3[1]).to.equal('playlist 2')
1028 })
1029
1030 it('Should delete some elements', async function () {
1031 this.timeout(30000)
1032
1033 await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementServer1Video4 })
1034 await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementNSFW })
1035
1036 await waitJobs(servers)
1037
1038 for (const server of servers) {
1039 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
1040 expect(body.total).to.equal(6)
1041
1042 const elements = body.data
1043 expect(elements).to.have.lengthOf(6)
1044
1045 expect(elements[0].video.name).to.equal('video 0 server 1')
1046 expect(elements[0].position).to.equal(1)
1047
1048 expect(elements[1].video.name).to.equal('video 2 server 3')
1049 expect(elements[1].position).to.equal(2)
1050
1051 expect(elements[2].video.name).to.equal('video 1 server 3')
1052 expect(elements[2].position).to.equal(3)
1053
1054 expect(elements[3].video.name).to.equal('video 4 server 1')
1055 expect(elements[3].position).to.equal(4)
1056
1057 expect(elements[4].video.name).to.equal('NSFW video')
1058 expect(elements[4].position).to.equal(5)
1059
1060 expect(elements[5].video.name).to.equal('NSFW video')
1061 expect(elements[5].position).to.equal(6)
1062 }
1063 })
1064
1065 it('Should be able to create a public playlist, and set it to private', async function () {
1066 this.timeout(30000)
1067
1068 const videoPlaylistIds = await commands[0].create({
1069 attributes: {
1070 displayName: 'my super public playlist',
1071 privacy: VideoPlaylistPrivacy.PUBLIC,
1072 videoChannelId: servers[0].store.channel.id
1073 }
1074 })
1075
1076 await waitJobs(servers)
1077
1078 for (const server of servers) {
1079 await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
1080 }
1081
1082 const attributes = { privacy: VideoPlaylistPrivacy.PRIVATE }
1083 await commands[0].update({ playlistId: videoPlaylistIds.id, attributes })
1084
1085 await waitJobs(servers)
1086
1087 for (const server of [ servers[1], servers[2] ]) {
1088 await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1089 }
1090
1091 await commands[0].get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
1092 await commands[0].get({ token: servers[0].accessToken, playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
1093 })
1094 })
1095
1096 describe('Playlist deletion', function () {
1097
1098 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
1099 this.timeout(30000)
1100
1101 await commands[0].delete({ playlistId: playlistServer1Id })
1102
1103 await waitJobs(servers)
1104
1105 for (const server of servers) {
1106 await server.playlists.get({ playlistId: playlistServer1UUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1107 }
1108 })
1109
1110 it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
1111 this.timeout(30000)
1112
1113 for (const server of servers) {
1114 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server)
1115 }
1116 })
1117
1118 it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
1119 this.timeout(30000)
1120
1121 const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'my super playlist')
1122
1123 {
1124 const body = await servers[2].playlists.list({ start: 0, count: 5 })
1125 expect(body.total).to.equal(3)
1126
1127 expect(finder(body.data)).to.not.be.undefined
1128 }
1129
1130 await servers[2].follows.unfollow({ target: servers[0] })
1131
1132 {
1133 const body = await servers[2].playlists.list({ start: 0, count: 5 })
1134 expect(body.total).to.equal(1)
1135
1136 expect(finder(body.data)).to.be.undefined
1137 }
1138 })
1139
1140 it('Should delete a channel and put the associated playlist in private mode', async function () {
1141 this.timeout(30000)
1142
1143 const channel = await servers[0].channels.create({ attributes: { name: 'super_channel', displayName: 'super channel' } })
1144
1145 const playlistCreated = await commands[0].create({
1146 attributes: {
1147 displayName: 'channel playlist',
1148 privacy: VideoPlaylistPrivacy.PUBLIC,
1149 videoChannelId: channel.id
1150 }
1151 })
1152
1153 await waitJobs(servers)
1154
1155 await servers[0].channels.delete({ channelName: 'super_channel' })
1156
1157 await waitJobs(servers)
1158
1159 const body = await commands[0].get({ token: servers[0].accessToken, playlistId: playlistCreated.uuid })
1160 expect(body.displayName).to.equal('channel playlist')
1161 expect(body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
1162
1163 await servers[1].playlists.get({ playlistId: playlistCreated.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1164 })
1165
1166 it('Should delete an account and delete its playlists', async function () {
1167 this.timeout(30000)
1168
1169 const { userId, token } = await servers[0].users.generate('user_1')
1170
1171 const { videoChannels } = await servers[0].users.getMyInfo({ token })
1172 const userChannel = videoChannels[0]
1173
1174 await commands[0].create({
1175 attributes: {
1176 displayName: 'playlist to be deleted',
1177 privacy: VideoPlaylistPrivacy.PUBLIC,
1178 videoChannelId: userChannel.id
1179 }
1180 })
1181
1182 await waitJobs(servers)
1183
1184 const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'playlist to be deleted')
1185
1186 {
1187 for (const server of [ servers[0], servers[1] ]) {
1188 const body = await server.playlists.list({ start: 0, count: 15 })
1189
1190 expect(finder(body.data)).to.not.be.undefined
1191 }
1192 }
1193
1194 await servers[0].users.remove({ userId })
1195 await waitJobs(servers)
1196
1197 {
1198 for (const server of [ servers[0], servers[1] ]) {
1199 const body = await server.playlists.list({ start: 0, count: 15 })
1200
1201 expect(finder(body.data)).to.be.undefined
1202 }
1203 }
1204 })
1205 })
1206
1207 after(async function () {
1208 await cleanupTests(servers)
1209 })
1210})
diff --git a/packages/tests/src/api/videos/video-privacy.ts b/packages/tests/src/api/videos/video-privacy.ts
new file mode 100644
index 000000000..9171463a4
--- /dev/null
+++ b/packages/tests/src/api/videos/video-privacy.ts
@@ -0,0 +1,294 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test video privacy', function () {
16 const servers: PeerTubeServer[] = []
17 let anotherUserToken: string
18
19 let privateVideoId: number
20 let privateVideoUUID: string
21
22 let internalVideoId: number
23 let internalVideoUUID: string
24
25 let unlistedVideo: VideoCreateResult
26 let nonFederatedUnlistedVideoUUID: string
27
28 let now: number
29
30 const dontFederateUnlistedConfig = {
31 federation: {
32 videos: {
33 federate_unlisted: false
34 }
35 }
36 }
37
38 before(async function () {
39 this.timeout(50000)
40
41 // Run servers
42 servers.push(await createSingleServer(1, dontFederateUnlistedConfig))
43 servers.push(await createSingleServer(2))
44
45 // Get the access tokens
46 await setAccessTokensToServers(servers)
47
48 // Server 1 and server 2 follow each other
49 await doubleFollow(servers[0], servers[1])
50 })
51
52 describe('Private and internal videos', function () {
53
54 it('Should upload a private and internal videos on server 1', async function () {
55 this.timeout(50000)
56
57 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
58 const attributes = { privacy }
59 await servers[0].videos.upload({ attributes })
60 }
61
62 await waitJobs(servers)
63 })
64
65 it('Should not have these private and internal videos on server 2', async function () {
66 const { total, data } = await servers[1].videos.list()
67
68 expect(total).to.equal(0)
69 expect(data).to.have.lengthOf(0)
70 })
71
72 it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () {
73 const { total, data } = await servers[0].videos.list()
74
75 expect(total).to.equal(0)
76 expect(data).to.have.lengthOf(0)
77 })
78
79 it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () {
80 const { total, data } = await servers[0].videos.listWithToken()
81
82 expect(total).to.equal(1)
83 expect(data).to.have.lengthOf(1)
84
85 expect(data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
86 })
87
88 it('Should list my (private and internal) videos', async function () {
89 const { total, data } = await servers[0].videos.listMyVideos()
90
91 expect(total).to.equal(2)
92 expect(data).to.have.lengthOf(2)
93
94 const privateVideo = data.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
95 privateVideoId = privateVideo.id
96 privateVideoUUID = privateVideo.uuid
97
98 const internalVideo = data.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
99 internalVideoId = internalVideo.id
100 internalVideoUUID = internalVideo.uuid
101 })
102
103 it('Should not be able to watch the private/internal video with non authenticated user', async function () {
104 await servers[0].videos.get({ id: privateVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
105 await servers[0].videos.get({ id: internalVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
106 })
107
108 it('Should not be able to watch the private video with another user', async function () {
109 const user = {
110 username: 'hello',
111 password: 'super password'
112 }
113 await servers[0].users.create({ username: user.username, password: user.password })
114
115 anotherUserToken = await servers[0].login.getAccessToken(user)
116
117 await servers[0].videos.getWithToken({
118 token: anotherUserToken,
119 id: privateVideoUUID,
120 expectedStatus: HttpStatusCode.FORBIDDEN_403
121 })
122 })
123
124 it('Should be able to watch the internal video with another user', async function () {
125 await servers[0].videos.getWithToken({ token: anotherUserToken, id: internalVideoUUID })
126 })
127
128 it('Should be able to watch the private video with the correct user', async function () {
129 await servers[0].videos.getWithToken({ id: privateVideoUUID })
130 })
131 })
132
133 describe('Unlisted videos', function () {
134
135 it('Should upload an unlisted video on server 2', async function () {
136 this.timeout(120000)
137
138 const attributes = {
139 name: 'unlisted video',
140 privacy: VideoPrivacy.UNLISTED
141 }
142 await servers[1].videos.upload({ attributes })
143
144 // Server 2 has transcoding enabled
145 await waitJobs(servers)
146 })
147
148 it('Should not have this unlisted video listed on server 1 and 2', async function () {
149 for (const server of servers) {
150 const { total, data } = await server.videos.list()
151
152 expect(total).to.equal(0)
153 expect(data).to.have.lengthOf(0)
154 }
155 })
156
157 it('Should list my (unlisted) videos', async function () {
158 const { total, data } = await servers[1].videos.listMyVideos()
159
160 expect(total).to.equal(1)
161 expect(data).to.have.lengthOf(1)
162
163 unlistedVideo = data[0]
164 })
165
166 it('Should not be able to get this unlisted video using its id', async function () {
167 await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
168 })
169
170 it('Should be able to get this unlisted video using its uuid/shortUUID', async function () {
171 for (const server of servers) {
172 for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) {
173 const video = await server.videos.get({ id })
174
175 expect(video.name).to.equal('unlisted video')
176 }
177 }
178 })
179
180 it('Should upload a non-federating unlisted video to server 1', async function () {
181 this.timeout(30000)
182
183 const attributes = {
184 name: 'unlisted video',
185 privacy: VideoPrivacy.UNLISTED
186 }
187 await servers[0].videos.upload({ attributes })
188
189 await waitJobs(servers)
190 })
191
192 it('Should list my new unlisted video', async function () {
193 const { total, data } = await servers[0].videos.listMyVideos()
194
195 expect(total).to.equal(3)
196 expect(data).to.have.lengthOf(3)
197
198 nonFederatedUnlistedVideoUUID = data[0].uuid
199 })
200
201 it('Should be able to get non-federated unlisted video from origin', async function () {
202 const video = await servers[0].videos.get({ id: nonFederatedUnlistedVideoUUID })
203
204 expect(video.name).to.equal('unlisted video')
205 })
206
207 it('Should not be able to get non-federated unlisted video from federated server', async function () {
208 await servers[1].videos.get({ id: nonFederatedUnlistedVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
209 })
210 })
211
212 describe('Privacy update', function () {
213
214 it('Should update the private and internal videos to public on server 1', async function () {
215 this.timeout(100000)
216
217 now = Date.now()
218
219 {
220 const attributes = {
221 name: 'private video becomes public',
222 privacy: VideoPrivacy.PUBLIC
223 }
224
225 await servers[0].videos.update({ id: privateVideoId, attributes })
226 }
227
228 {
229 const attributes = {
230 name: 'internal video becomes public',
231 privacy: VideoPrivacy.PUBLIC
232 }
233 await servers[0].videos.update({ id: internalVideoId, attributes })
234 }
235
236 await wait(10000)
237 await waitJobs(servers)
238 })
239
240 it('Should have this new public video listed on server 1 and 2', async function () {
241 for (const server of servers) {
242 const { total, data } = await server.videos.list()
243 expect(total).to.equal(2)
244 expect(data).to.have.lengthOf(2)
245
246 const privateVideo = data.find(v => v.name === 'private video becomes public')
247 const internalVideo = data.find(v => v.name === 'internal video becomes public')
248
249 expect(privateVideo).to.not.be.undefined
250 expect(internalVideo).to.not.be.undefined
251
252 expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now)
253 // We don't change the publish date of internal videos
254 expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now)
255
256 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
257 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
258 }
259 })
260
261 it('Should set these videos as private and internal', async function () {
262 await servers[0].videos.update({ id: internalVideoId, attributes: { privacy: VideoPrivacy.PRIVATE } })
263 await servers[0].videos.update({ id: privateVideoId, attributes: { privacy: VideoPrivacy.INTERNAL } })
264
265 await waitJobs(servers)
266
267 for (const server of servers) {
268 const { total, data } = await server.videos.list()
269
270 expect(total).to.equal(0)
271 expect(data).to.have.lengthOf(0)
272 }
273
274 {
275 const { total, data } = await servers[0].videos.listMyVideos()
276 expect(total).to.equal(3)
277 expect(data).to.have.lengthOf(3)
278
279 const privateVideo = data.find(v => v.name === 'private video becomes public')
280 const internalVideo = data.find(v => v.name === 'internal video becomes public')
281
282 expect(privateVideo).to.not.be.undefined
283 expect(internalVideo).to.not.be.undefined
284
285 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL)
286 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE)
287 }
288 })
289 })
290
291 after(async function () {
292 await cleanupTests(servers)
293 })
294})
diff --git a/packages/tests/src/api/videos/video-schedule-update.ts b/packages/tests/src/api/videos/video-schedule-update.ts
new file mode 100644
index 000000000..96d71933e
--- /dev/null
+++ b/packages/tests/src/api/videos/video-schedule-update.ts
@@ -0,0 +1,155 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15function in10Seconds () {
16 const now = new Date()
17 now.setSeconds(now.getSeconds() + 10)
18
19 return now
20}
21
22describe('Test video update scheduler', function () {
23 let servers: PeerTubeServer[] = []
24 let video2UUID: string
25
26 before(async function () {
27 this.timeout(30000)
28
29 // Run servers
30 servers = await createMultipleServers(2)
31
32 await setAccessTokensToServers(servers)
33
34 await doubleFollow(servers[0], servers[1])
35 })
36
37 it('Should upload a video and schedule an update in 10 seconds', async function () {
38 const attributes = {
39 name: 'video 1',
40 privacy: VideoPrivacy.PRIVATE,
41 scheduleUpdate: {
42 updateAt: in10Seconds().toISOString(),
43 privacy: VideoPrivacy.PUBLIC
44 }
45 }
46
47 await servers[0].videos.upload({ attributes })
48
49 await waitJobs(servers)
50 })
51
52 it('Should not list the video (in privacy mode)', async function () {
53 for (const server of servers) {
54 const { total } = await server.videos.list()
55
56 expect(total).to.equal(0)
57 }
58 })
59
60 it('Should have my scheduled video in my account videos', async function () {
61 const { total, data } = await servers[0].videos.listMyVideos()
62 expect(total).to.equal(1)
63
64 const videoFromList = data[0]
65 const videoFromGet = await servers[0].videos.getWithToken({ id: videoFromList.uuid })
66
67 for (const video of [ videoFromList, videoFromGet ]) {
68 expect(video.name).to.equal('video 1')
69 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
70 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
71 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
72 }
73 })
74
75 it('Should wait some seconds and have the video in public privacy', async function () {
76 this.timeout(50000)
77
78 await wait(15000)
79 await waitJobs(servers)
80
81 for (const server of servers) {
82 const { total, data } = await server.videos.list()
83
84 expect(total).to.equal(1)
85 expect(data[0].name).to.equal('video 1')
86 }
87 })
88
89 it('Should upload a video without scheduling an update', async function () {
90 const attributes = {
91 name: 'video 2',
92 privacy: VideoPrivacy.PRIVATE
93 }
94
95 const { uuid } = await servers[0].videos.upload({ attributes })
96 video2UUID = uuid
97
98 await waitJobs(servers)
99 })
100
101 it('Should update a video by scheduling an update', async function () {
102 const attributes = {
103 name: 'video 2 updated',
104 scheduleUpdate: {
105 updateAt: in10Seconds().toISOString(),
106 privacy: VideoPrivacy.PUBLIC
107 }
108 }
109
110 await servers[0].videos.update({ id: video2UUID, attributes })
111 await waitJobs(servers)
112 })
113
114 it('Should not display the updated video', async function () {
115 for (const server of servers) {
116 const { total } = await server.videos.list()
117
118 expect(total).to.equal(1)
119 }
120 })
121
122 it('Should have my scheduled updated video in my account videos', async function () {
123 const { total, data } = await servers[0].videos.listMyVideos()
124 expect(total).to.equal(2)
125
126 const video = data.find(v => v.uuid === video2UUID)
127 expect(video).not.to.be.undefined
128
129 expect(video.name).to.equal('video 2 updated')
130 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
131
132 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
133 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
134 })
135
136 it('Should wait some seconds and have the updated video in public privacy', async function () {
137 this.timeout(20000)
138
139 await wait(15000)
140 await waitJobs(servers)
141
142 for (const server of servers) {
143 const { total, data } = await server.videos.list()
144 expect(total).to.equal(2)
145
146 const video = data.find(v => v.uuid === video2UUID)
147 expect(video).not.to.be.undefined
148 expect(video.name).to.equal('video 2 updated')
149 }
150 })
151
152 after(async function () {
153 await cleanupTests(servers)
154 })
155})
diff --git a/packages/tests/src/api/videos/video-source.ts b/packages/tests/src/api/videos/video-source.ts
new file mode 100644
index 000000000..efe8c3802
--- /dev/null
+++ b/packages/tests/src/api/videos/video-source.ts
@@ -0,0 +1,448 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { expect } from 'chai'
3import { getAllFiles } from '@peertube/peertube-core-utils'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import { expectStartWith } from '@tests/shared/checks.js'
6import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
7import {
8 cleanupTests,
9 createMultipleServers,
10 doubleFollow,
11 makeGetRequest,
12 makeRawRequest,
13 ObjectStorageCommand,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 setDefaultAccountAvatar,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20
21describe('Test a video file replacement', function () {
22 let servers: PeerTubeServer[] = []
23
24 let replaceDate: Date
25 let userToken: string
26 let uuid: string
27
28 before(async function () {
29 this.timeout(50000)
30
31 servers = await createMultipleServers(2)
32
33 // Get the access tokens
34 await setAccessTokensToServers(servers)
35 await setDefaultVideoChannel(servers)
36 await setDefaultAccountAvatar(servers)
37
38 await servers[0].config.enableFileUpdate()
39
40 userToken = await servers[0].users.generateUserAndToken('user1')
41
42 // Server 1 and server 2 follow each other
43 await doubleFollow(servers[0], servers[1])
44 })
45
46 describe('Getting latest video source', () => {
47 const fixture = 'video_short.webm'
48 const uuids: string[] = []
49
50 it('Should get the source filename with legacy upload', async function () {
51 this.timeout(30000)
52
53 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
54 uuids.push(uuid)
55
56 const source = await servers[0].videos.getSource({ id: uuid })
57 expect(source.filename).to.equal(fixture)
58 })
59
60 it('Should get the source filename with resumable upload', async function () {
61 this.timeout(30000)
62
63 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
64 uuids.push(uuid)
65
66 const source = await servers[0].videos.getSource({ id: uuid })
67 expect(source.filename).to.equal(fixture)
68 })
69
70 after(async function () {
71 this.timeout(60000)
72
73 for (const uuid of uuids) {
74 await servers[0].videos.remove({ id: uuid })
75 }
76
77 await waitJobs(servers)
78 })
79 })
80
81 describe('Updating video source', function () {
82
83 describe('Filesystem', function () {
84
85 it('Should replace a video file with transcoding disabled', async function () {
86 this.timeout(120000)
87
88 await servers[0].config.disableTranscoding()
89
90 const { uuid } = await servers[0].videos.quickUpload({ name: 'fs without transcoding', fixture: 'video_short_720p.mp4' })
91 await waitJobs(servers)
92
93 for (const server of servers) {
94 const video = await server.videos.get({ id: uuid })
95
96 const files = getAllFiles(video)
97 expect(files).to.have.lengthOf(1)
98 expect(files[0].resolution.id).to.equal(720)
99 }
100
101 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
102 await waitJobs(servers)
103
104 for (const server of servers) {
105 const video = await server.videos.get({ id: uuid })
106
107 const files = getAllFiles(video)
108 expect(files).to.have.lengthOf(1)
109 expect(files[0].resolution.id).to.equal(360)
110 }
111 })
112
113 it('Should replace a video file with transcoding enabled', async function () {
114 this.timeout(120000)
115
116 const previousPaths: string[] = []
117
118 await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
119
120 const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: 'video_short_720p.mp4' })
121 uuid = videoUUID
122
123 await waitJobs(servers)
124
125 for (const server of servers) {
126 const video = await server.videos.get({ id: uuid })
127 expect(video.inputFileUpdatedAt).to.be.null
128
129 const files = getAllFiles(video)
130 expect(files).to.have.lengthOf(6 * 2)
131
132 // Grab old paths to ensure we'll regenerate
133
134 previousPaths.push(video.previewPath)
135 previousPaths.push(video.thumbnailPath)
136
137 for (const file of files) {
138 previousPaths.push(file.fileUrl)
139 previousPaths.push(file.torrentUrl)
140 previousPaths.push(file.metadataUrl)
141
142 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
143 previousPaths.push(JSON.stringify(metadata))
144 }
145
146 const { storyboards } = await server.storyboard.list({ id: uuid })
147 for (const s of storyboards) {
148 previousPaths.push(s.storyboardPath)
149 }
150 }
151
152 replaceDate = new Date()
153
154 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
155 await waitJobs(servers)
156
157 for (const server of servers) {
158 const video = await server.videos.get({ id: uuid })
159
160 expect(video.inputFileUpdatedAt).to.not.be.null
161 expect(new Date(video.inputFileUpdatedAt)).to.be.above(replaceDate)
162
163 const files = getAllFiles(video)
164 expect(files).to.have.lengthOf(4 * 2)
165
166 expect(previousPaths).to.not.include(video.previewPath)
167 expect(previousPaths).to.not.include(video.thumbnailPath)
168
169 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
170 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
171
172 for (const file of files) {
173 expect(previousPaths).to.not.include(file.fileUrl)
174 expect(previousPaths).to.not.include(file.torrentUrl)
175 expect(previousPaths).to.not.include(file.metadataUrl)
176
177 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
178 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
179
180 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
181 expect(previousPaths).to.not.include(JSON.stringify(metadata))
182 }
183
184 const { storyboards } = await server.storyboard.list({ id: uuid })
185 for (const s of storyboards) {
186 expect(previousPaths).to.not.include(s.storyboardPath)
187
188 await makeGetRequest({ url: server.url, path: s.storyboardPath, expectedStatus: HttpStatusCode.OK_200 })
189 }
190 }
191
192 await servers[0].config.enableMinimumTranscoding()
193 })
194
195 it('Should have cleaned up old files', async function () {
196 {
197 const count = await servers[0].servers.countFiles('storyboards')
198 expect(count).to.equal(2)
199 }
200
201 {
202 const count = await servers[0].servers.countFiles('web-videos')
203 expect(count).to.equal(5 + 1) // +1 for private directory
204 }
205
206 {
207 const count = await servers[0].servers.countFiles('streaming-playlists/hls')
208 expect(count).to.equal(1 + 1) // +1 for private directory
209 }
210
211 {
212 const count = await servers[0].servers.countFiles('torrents')
213 expect(count).to.equal(9)
214 }
215 })
216
217 it('Should have the correct source input', async function () {
218 const source = await servers[0].videos.getSource({ id: uuid })
219
220 expect(source.filename).to.equal('video_short_360p.mp4')
221 expect(new Date(source.createdAt)).to.be.above(replaceDate)
222 })
223
224 it('Should not have regenerated miniatures that were previously uploaded', async function () {
225 this.timeout(120000)
226
227 const { uuid } = await servers[0].videos.upload({
228 attributes: {
229 name: 'custom miniatures',
230 thumbnailfile: 'custom-thumbnail.jpg',
231 previewfile: 'custom-preview.jpg'
232 }
233 })
234
235 await waitJobs(servers)
236
237 const previousPaths: string[] = []
238
239 for (const server of servers) {
240 const video = await server.videos.get({ id: uuid })
241
242 previousPaths.push(video.previewPath)
243 previousPaths.push(video.thumbnailPath)
244
245 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
246 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
247 }
248
249 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
250 await waitJobs(servers)
251
252 for (const server of servers) {
253 const video = await server.videos.get({ id: uuid })
254
255 expect(previousPaths).to.include(video.previewPath)
256 expect(previousPaths).to.include(video.thumbnailPath)
257
258 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
259 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
260 }
261 })
262 })
263
264 describe('Autoblacklist', function () {
265
266 function updateAutoBlacklist (enabled: boolean) {
267 return servers[0].config.updateExistingSubConfig({
268 newConfig: {
269 autoBlacklist: {
270 videos: {
271 ofUsers: {
272 enabled
273 }
274 }
275 }
276 }
277 })
278 }
279
280 async function expectBlacklist (uuid: string, value: boolean) {
281 const video = await servers[0].videos.getWithToken({ id: uuid })
282
283 expect(video.blacklisted).to.equal(value)
284 }
285
286 before(async function () {
287 await updateAutoBlacklist(true)
288 })
289
290 it('Should auto blacklist an unblacklisted video after file replacement', async function () {
291 this.timeout(120000)
292
293 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
294 await waitJobs(servers)
295 await expectBlacklist(uuid, true)
296
297 await servers[0].blacklist.remove({ videoId: uuid })
298 await expectBlacklist(uuid, false)
299
300 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' })
301 await waitJobs(servers)
302
303 await expectBlacklist(uuid, true)
304 })
305
306 it('Should auto blacklist an already blacklisted video after file replacement', async function () {
307 this.timeout(120000)
308
309 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
310 await waitJobs(servers)
311 await expectBlacklist(uuid, true)
312
313 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' })
314 await waitJobs(servers)
315
316 await expectBlacklist(uuid, true)
317 })
318
319 it('Should not auto blacklist if auto blacklist has been disabled between the upload and the replacement', async function () {
320 this.timeout(120000)
321
322 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
323 await waitJobs(servers)
324 await expectBlacklist(uuid, true)
325
326 await servers[0].blacklist.remove({ videoId: uuid })
327 await expectBlacklist(uuid, false)
328
329 await updateAutoBlacklist(false)
330
331 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short1.webm' })
332 await waitJobs(servers)
333
334 await expectBlacklist(uuid, false)
335 })
336 })
337
338 describe('With object storage enabled', function () {
339 if (areMockObjectStorageTestsDisabled()) return
340
341 const objectStorage = new ObjectStorageCommand()
342
343 before(async function () {
344 this.timeout(120000)
345
346 const configOverride = objectStorage.getDefaultMockConfig()
347 await objectStorage.prepareDefaultMockBuckets()
348
349 await servers[0].kill()
350 await servers[0].run(configOverride)
351 })
352
353 it('Should replace a video file with transcoding disabled', async function () {
354 this.timeout(120000)
355
356 await servers[0].config.disableTranscoding()
357
358 const { uuid } = await servers[0].videos.quickUpload({
359 name: 'object storage without transcoding',
360 fixture: 'video_short_720p.mp4'
361 })
362 await waitJobs(servers)
363
364 for (const server of servers) {
365 const video = await server.videos.get({ id: uuid })
366
367 const files = getAllFiles(video)
368 expect(files).to.have.lengthOf(1)
369 expect(files[0].resolution.id).to.equal(720)
370 expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
371 }
372
373 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
374 await waitJobs(servers)
375
376 for (const server of servers) {
377 const video = await server.videos.get({ id: uuid })
378
379 const files = getAllFiles(video)
380 expect(files).to.have.lengthOf(1)
381 expect(files[0].resolution.id).to.equal(360)
382 expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
383 }
384 })
385
386 it('Should replace a video file with transcoding enabled', async function () {
387 this.timeout(120000)
388
389 const previousPaths: string[] = []
390
391 await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
392
393 const { uuid: videoUUID } = await servers[0].videos.quickUpload({
394 name: 'object storage with transcoding',
395 fixture: 'video_short_360p.mp4'
396 })
397 uuid = videoUUID
398
399 await waitJobs(servers)
400
401 for (const server of servers) {
402 const video = await server.videos.get({ id: uuid })
403
404 const files = getAllFiles(video)
405 expect(files).to.have.lengthOf(4 * 2)
406
407 for (const file of files) {
408 previousPaths.push(file.fileUrl)
409 }
410
411 for (const file of video.files) {
412 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
413 }
414
415 for (const file of video.streamingPlaylists[0].files) {
416 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
417 }
418 }
419
420 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_240p.mp4' })
421 await waitJobs(servers)
422
423 for (const server of servers) {
424 const video = await server.videos.get({ id: uuid })
425
426 const files = getAllFiles(video)
427 expect(files).to.have.lengthOf(3 * 2)
428
429 for (const file of files) {
430 expect(previousPaths).to.not.include(file.fileUrl)
431 }
432
433 for (const file of video.files) {
434 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
435 }
436
437 for (const file of video.streamingPlaylists[0].files) {
438 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
439 }
440 }
441 })
442 })
443 })
444
445 after(async function () {
446 await cleanupTests(servers)
447 })
448})
diff --git a/packages/tests/src/api/videos/video-static-file-privacy.ts b/packages/tests/src/api/videos/video-static-file-privacy.ts
new file mode 100644
index 000000000..7c8d14815
--- /dev/null
+++ b/packages/tests/src/api/videos/video-static-file-privacy.ts
@@ -0,0 +1,602 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { decode } from 'magnet-uri'
5import { getAllFiles, wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 createSingleServer,
10 findExternalSavedVideo,
11 makeRawRequest,
12 PeerTubeServer,
13 sendRTMPStream,
14 setAccessTokensToServers,
15 setDefaultVideoChannel,
16 stopFfmpeg,
17 waitJobs
18} from '@peertube/peertube-server-commands'
19import { expectStartWith } from '@tests/shared/checks.js'
20import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
21import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
22
23describe('Test video static file privacy', function () {
24 let server: PeerTubeServer
25 let userToken: string
26
27 before(async function () {
28 this.timeout(50000)
29
30 server = await createSingleServer(1)
31 await setAccessTokensToServers([ server ])
32 await setDefaultVideoChannel([ server ])
33
34 userToken = await server.users.generateUserAndToken('user1')
35 })
36
37 describe('VOD static file path', function () {
38
39 function runSuite () {
40
41 async function checkPrivateFiles (uuid: string) {
42 const video = await server.videos.getWithToken({ id: uuid })
43
44 for (const file of video.files) {
45 expect(file.fileDownloadUrl).to.not.include('/private/')
46 expectStartWith(file.fileUrl, server.url + '/static/web-videos/private/')
47
48 const torrent = await parseTorrentVideo(server, file)
49 expect(torrent.urlList).to.have.lengthOf(0)
50
51 const magnet = decode(file.magnetUri)
52 expect(magnet.urlList).to.have.lengthOf(0)
53
54 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
55 }
56
57 const hls = video.streamingPlaylists[0]
58 if (hls) {
59 expectStartWith(hls.playlistUrl, server.url + '/static/streaming-playlists/hls/private/')
60 expectStartWith(hls.segmentsSha256Url, server.url + '/static/streaming-playlists/hls/private/')
61
62 await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
63 await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
64 }
65 }
66
67 async function checkPublicFiles (uuid: string) {
68 const video = await server.videos.get({ id: uuid })
69
70 for (const file of getAllFiles(video)) {
71 expect(file.fileDownloadUrl).to.not.include('/private/')
72 expect(file.fileUrl).to.not.include('/private/')
73
74 const torrent = await parseTorrentVideo(server, file)
75 expect(torrent.urlList[0]).to.not.include('private')
76
77 const magnet = decode(file.magnetUri)
78 expect(magnet.urlList[0]).to.not.include('private')
79
80 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
81 await makeRawRequest({ url: torrent.urlList[0], expectedStatus: HttpStatusCode.OK_200 })
82 await makeRawRequest({ url: magnet.urlList[0], expectedStatus: HttpStatusCode.OK_200 })
83 }
84
85 const hls = video.streamingPlaylists[0]
86 if (hls) {
87 expect(hls.playlistUrl).to.not.include('private')
88 expect(hls.segmentsSha256Url).to.not.include('private')
89
90 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
91 await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
92 }
93 }
94
95 it('Should upload a private/internal/password protected video and have a private static path', async function () {
96 this.timeout(120000)
97
98 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
99 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy })
100 await waitJobs([ server ])
101
102 await checkPrivateFiles(uuid)
103 }
104
105 const { uuid } = await server.videos.quickUpload({
106 name: 'video',
107 privacy: VideoPrivacy.PASSWORD_PROTECTED,
108 videoPasswords: [ 'my super password' ]
109 })
110 await waitJobs([ server ])
111
112 await checkPrivateFiles(uuid)
113 })
114
115 it('Should upload a public video and update it as private/internal to have a private static path', async function () {
116 this.timeout(120000)
117
118 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
119 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC })
120 await waitJobs([ server ])
121
122 await server.videos.update({ id: uuid, attributes: { privacy } })
123 await waitJobs([ server ])
124
125 await checkPrivateFiles(uuid)
126 }
127 })
128
129 it('Should upload a private video and update it to unlisted to have a public static path', async function () {
130 this.timeout(120000)
131
132 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
133 await waitJobs([ server ])
134
135 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
136 await waitJobs([ server ])
137
138 await checkPublicFiles(uuid)
139 })
140
141 it('Should upload an internal video and update it to public to have a public static path', async function () {
142 this.timeout(120000)
143
144 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL })
145 await waitJobs([ server ])
146
147 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
148 await waitJobs([ server ])
149
150 await checkPublicFiles(uuid)
151 })
152
153 it('Should upload an internal video and schedule a public publish', async function () {
154 this.timeout(120000)
155
156 const attributes = {
157 name: 'video',
158 privacy: VideoPrivacy.PRIVATE,
159 scheduleUpdate: {
160 updateAt: new Date(Date.now() + 1000).toISOString(),
161 privacy: VideoPrivacy.PUBLIC
162 }
163 }
164
165 const { uuid } = await server.videos.upload({ attributes })
166
167 await waitJobs([ server ])
168 await wait(1000)
169 await server.debug.sendCommand({ body: { command: 'process-update-videos-scheduler' } })
170
171 await waitJobs([ server ])
172
173 await checkPublicFiles(uuid)
174 })
175 }
176
177 describe('Without transcoding', function () {
178 runSuite()
179 })
180
181 describe('With transcoding', function () {
182
183 before(async function () {
184 await server.config.enableMinimumTranscoding()
185 })
186
187 runSuite()
188 })
189 })
190
191 describe('VOD static file right check', function () {
192 let unrelatedFileToken: string
193
194 async function checkVideoFiles (options: {
195 id: string
196 expectedStatus: HttpStatusCodeType
197 token: string
198 videoFileToken: string
199 videoPassword?: string
200 }) {
201 const { id, expectedStatus, token, videoFileToken, videoPassword } = options
202
203 const video = await server.videos.getWithToken({ id })
204
205 for (const file of getAllFiles(video)) {
206 await makeRawRequest({ url: file.fileUrl, token, expectedStatus })
207 await makeRawRequest({ url: file.fileDownloadUrl, token, expectedStatus })
208
209 await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus })
210 await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus })
211
212 if (videoPassword) {
213 const headers = { 'x-peertube-video-password': videoPassword }
214 await makeRawRequest({ url: file.fileUrl, headers, expectedStatus })
215 await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus })
216 }
217 }
218
219 const hls = video.streamingPlaylists[0]
220 await makeRawRequest({ url: hls.playlistUrl, token, expectedStatus })
221 await makeRawRequest({ url: hls.segmentsSha256Url, token, expectedStatus })
222
223 await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus })
224 await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus })
225
226 if (videoPassword) {
227 const headers = { 'x-peertube-video-password': videoPassword }
228 await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus })
229 await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus })
230 }
231 }
232
233 before(async function () {
234 await server.config.enableMinimumTranscoding()
235
236 const { uuid } = await server.videos.quickUpload({ name: 'another video' })
237 unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
238 })
239
240 it('Should not be able to access a private video files without OAuth token and file token', async function () {
241 this.timeout(120000)
242
243 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
244 await waitJobs([ server ])
245
246 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null })
247 })
248
249 it('Should not be able to access password protected video files without OAuth token, file token and password', async function () {
250 this.timeout(120000)
251 const videoPassword = 'my super password'
252
253 const { uuid } = await server.videos.quickUpload({
254 name: 'password protected video',
255 privacy: VideoPrivacy.PASSWORD_PROTECTED,
256 videoPasswords: [ videoPassword ]
257 })
258 await waitJobs([ server ])
259
260 await checkVideoFiles({
261 id: uuid,
262 expectedStatus: HttpStatusCode.FORBIDDEN_403,
263 token: null,
264 videoFileToken: null,
265 videoPassword: null
266 })
267 })
268
269 it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () {
270 this.timeout(120000)
271 const videoPassword = 'my super password'
272
273 const { uuid } = await server.videos.quickUpload({
274 name: 'password protected video',
275 privacy: VideoPrivacy.PASSWORD_PROTECTED,
276 videoPasswords: [ videoPassword ]
277 })
278 await waitJobs([ server ])
279
280 await checkVideoFiles({
281 id: uuid,
282 expectedStatus: HttpStatusCode.FORBIDDEN_403,
283 token: userToken,
284 videoFileToken: unrelatedFileToken,
285 videoPassword: 'incorrectPassword'
286 })
287 })
288
289 it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () {
290 this.timeout(120000)
291
292 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
293 await waitJobs([ server ])
294
295 await checkVideoFiles({
296 id: uuid,
297 expectedStatus: HttpStatusCode.FORBIDDEN_403,
298 token: userToken,
299 videoFileToken: unrelatedFileToken
300 })
301 })
302
303 it('Should be able to access a private video files with appropriate OAuth token or file token', async function () {
304 this.timeout(120000)
305
306 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
307 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
308
309 await waitJobs([ server ])
310
311 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken })
312 })
313
314 it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () {
315 this.timeout(120000)
316 const videoPassword = 'my super password'
317
318 const { uuid } = await server.videos.quickUpload({
319 name: 'video',
320 privacy: VideoPrivacy.PASSWORD_PROTECTED,
321 videoPasswords: [ videoPassword ]
322 })
323
324 const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword })
325
326 await waitJobs([ server ])
327
328 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword })
329 })
330
331 it('Should reinject video file token', async function () {
332 this.timeout(120000)
333
334 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
335
336 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
337 await waitJobs([ server ])
338
339 {
340 const video = await server.videos.getWithToken({ id: uuid })
341 const hls = video.streamingPlaylists[0]
342 const query = { videoFileToken }
343 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
344
345 expect(text).to.not.include(videoFileToken)
346 }
347
348 {
349 await checkVideoFileTokenReinjection({
350 server,
351 videoUUID: uuid,
352 videoFileToken,
353 resolutions: [ 240, 720 ],
354 isLive: false
355 })
356 }
357 })
358
359 it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () {
360 this.timeout(120000)
361
362 const { uuid } = await server.videos.quickUpload({ name: 'video', token: userToken, privacy: VideoPrivacy.PRIVATE })
363 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
364
365 await waitJobs([ server ])
366
367 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken })
368 })
369 })
370
371 describe('Live static file path and check', function () {
372 let normalLiveId: string
373 let normalLive: LiveVideo
374
375 let permanentLiveId: string
376 let permanentLive: LiveVideo
377
378 let passwordProtectedLiveId: string
379 let passwordProtectedLive: LiveVideo
380
381 const correctPassword = 'my super password'
382
383 let unrelatedFileToken: string
384
385 async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) {
386 const { live, liveId, videoPassword } = options
387 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
388 await server.live.waitUntilPublished({ videoId: liveId })
389
390 const video = await server.videos.getWithToken({ id: liveId })
391
392 const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
393
394 const hls = video.streamingPlaylists[0]
395
396 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
397 expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/')
398
399 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
400 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
401
402 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
403 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
404 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
405
406 if (videoPassword) {
407 await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 })
408 await makeRawRequest({
409 url,
410 headers: { 'x-peertube-video-password': 'incorrectPassword' },
411 expectedStatus: HttpStatusCode.FORBIDDEN_403
412 })
413 }
414
415 }
416
417 await stopFfmpeg(ffmpegCommand)
418 }
419
420 async function checkReplay (replay: VideoDetails) {
421 const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid })
422
423 const hls = replay.streamingPlaylists[0]
424 expect(hls.files).to.not.have.lengthOf(0)
425
426 for (const file of hls.files) {
427 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
428 await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
429
430 await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
431 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
432 await makeRawRequest({
433 url: file.fileUrl,
434 query: { videoFileToken: unrelatedFileToken },
435 expectedStatus: HttpStatusCode.FORBIDDEN_403
436 })
437 }
438
439 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
440 expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/')
441
442 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
443 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
444
445 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
446 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
447 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
448 }
449 }
450
451 before(async function () {
452 await server.config.enableMinimumTranscoding()
453
454 const { uuid } = await server.videos.quickUpload({ name: 'another video' })
455 unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
456
457 await server.config.enableLive({
458 allowReplay: true,
459 transcoding: true,
460 resolutions: 'min'
461 })
462
463 {
464 const { video, live } = await server.live.quickCreate({
465 saveReplay: true,
466 permanentLive: false,
467 privacy: VideoPrivacy.PRIVATE
468 })
469 normalLiveId = video.uuid
470 normalLive = live
471 }
472
473 {
474 const { video, live } = await server.live.quickCreate({
475 saveReplay: true,
476 permanentLive: true,
477 privacy: VideoPrivacy.PRIVATE
478 })
479 permanentLiveId = video.uuid
480 permanentLive = live
481 }
482
483 {
484 const { video, live } = await server.live.quickCreate({
485 saveReplay: false,
486 permanentLive: false,
487 privacy: VideoPrivacy.PASSWORD_PROTECTED,
488 videoPasswords: [ correctPassword ]
489 })
490 passwordProtectedLiveId = video.uuid
491 passwordProtectedLive = live
492 }
493 })
494
495 it('Should create a private normal live and have a private static path', async function () {
496 this.timeout(240000)
497
498 await checkLiveFiles({ live: normalLive, liveId: normalLiveId })
499 })
500
501 it('Should create a private permanent live and have a private static path', async function () {
502 this.timeout(240000)
503
504 await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId })
505 })
506
507 it('Should create a password protected live and have a private static path', async function () {
508 this.timeout(240000)
509
510 await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword })
511 })
512
513 it('Should reinject video file token on permanent live', async function () {
514 this.timeout(240000)
515
516 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey })
517 await server.live.waitUntilPublished({ videoId: permanentLiveId })
518
519 const video = await server.videos.getWithToken({ id: permanentLiveId })
520 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
521 const hls = video.streamingPlaylists[0]
522
523 {
524 const query = { videoFileToken }
525 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
526
527 expect(text).to.not.include(videoFileToken)
528 }
529
530 {
531 await checkVideoFileTokenReinjection({
532 server,
533 videoUUID: permanentLiveId,
534 videoFileToken,
535 resolutions: [ 720 ],
536 isLive: true
537 })
538 }
539
540 await stopFfmpeg(ffmpegCommand)
541 })
542
543 it('Should have created a replay of the normal live with a private static path', async function () {
544 this.timeout(240000)
545
546 await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId })
547
548 const replay = await server.videos.getWithToken({ id: normalLiveId })
549 await checkReplay(replay)
550 })
551
552 it('Should have created a replay of the permanent live with a private static path', async function () {
553 this.timeout(240000)
554
555 await server.live.waitUntilWaiting({ videoId: permanentLiveId })
556 await waitJobs([ server ])
557
558 const live = await server.videos.getWithToken({ id: permanentLiveId })
559 const replayFromList = await findExternalSavedVideo(server, live)
560 const replay = await server.videos.getWithToken({ id: replayFromList.id })
561
562 await checkReplay(replay)
563 })
564 })
565
566 describe('With static file right check disabled', function () {
567 let videoUUID: string
568
569 before(async function () {
570 this.timeout(240000)
571
572 await server.kill()
573
574 await server.run({
575 static_files: {
576 private_files_require_auth: false
577 }
578 })
579
580 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL })
581 videoUUID = uuid
582
583 await waitJobs([ server ])
584 })
585
586 it('Should not check auth for private static files', async function () {
587 const video = await server.videos.getWithToken({ id: videoUUID })
588
589 for (const file of getAllFiles(video)) {
590 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
591 }
592
593 const hls = video.streamingPlaylists[0]
594 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
595 await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
596 })
597 })
598
599 after(async function () {
600 await cleanupTests([ server ])
601 })
602})
diff --git a/packages/tests/src/api/videos/video-storyboard.ts b/packages/tests/src/api/videos/video-storyboard.ts
new file mode 100644
index 000000000..7d156aa7f
--- /dev/null
+++ b/packages/tests/src/api/videos/video-storyboard.ts
@@ -0,0 +1,213 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readdir } from 'fs/promises'
5import { basename } from 'path'
6import { FIXTURE_URLS } from '@tests/shared/tests.js'
7import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
8import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
9import {
10 cleanupTests,
11 createMultipleServers,
12 doubleFollow,
13 makeGetRequest,
14 PeerTubeServer,
15 sendRTMPStream,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 stopFfmpeg,
19 waitJobs
20} from '@peertube/peertube-server-commands'
21
22async function checkStoryboard (options: {
23 server: PeerTubeServer
24 uuid: string
25 tilesCount?: number
26 minSize?: number
27}) {
28 const { server, uuid, tilesCount, minSize = 1000 } = options
29
30 const { storyboards } = await server.storyboard.list({ id: uuid })
31
32 expect(storyboards).to.have.lengthOf(1)
33
34 const storyboard = storyboards[0]
35
36 expect(storyboard.spriteDuration).to.equal(1)
37 expect(storyboard.spriteHeight).to.equal(108)
38 expect(storyboard.spriteWidth).to.equal(192)
39 expect(storyboard.storyboardPath).to.exist
40
41 if (tilesCount) {
42 expect(storyboard.totalWidth).to.equal(192 * Math.min(tilesCount, 10))
43 expect(storyboard.totalHeight).to.equal(108 * Math.max((tilesCount / 10), 1))
44 }
45
46 const { body } = await makeGetRequest({ url: server.url, path: storyboard.storyboardPath, expectedStatus: HttpStatusCode.OK_200 })
47 expect(body.length).to.be.above(minSize)
48}
49
50describe('Test video storyboard', function () {
51 let servers: PeerTubeServer[]
52
53 let baseUUID: string
54
55 before(async function () {
56 this.timeout(120000)
57
58 servers = await createMultipleServers(2)
59 await setAccessTokensToServers(servers)
60 await setDefaultVideoChannel(servers)
61
62 await doubleFollow(servers[0], servers[1])
63 })
64
65 it('Should generate a storyboard after upload without transcoding', async function () {
66 this.timeout(120000)
67
68 // 5s video
69 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' })
70 baseUUID = uuid
71 await waitJobs(servers)
72
73 for (const server of servers) {
74 await checkStoryboard({ server, uuid, tilesCount: 5 })
75 }
76 })
77
78 it('Should generate a storyboard after upload without transcoding with a long video', async function () {
79 this.timeout(120000)
80
81 // 124s video
82 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_very_long_10p.mp4' })
83 await waitJobs(servers)
84
85 for (const server of servers) {
86 await checkStoryboard({ server, uuid, tilesCount: 100 })
87 }
88 })
89
90 it('Should generate a storyboard after upload with transcoding', async function () {
91 this.timeout(120000)
92
93 await servers[0].config.enableMinimumTranscoding()
94
95 // 5s video
96 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' })
97 await waitJobs(servers)
98
99 for (const server of servers) {
100 await checkStoryboard({ server, uuid, tilesCount: 5 })
101 }
102 })
103
104 it('Should generate a storyboard after an audio upload', async function () {
105 this.timeout(120000)
106
107 // 6s audio
108 const attributes = { name: 'audio', fixture: 'sample.ogg' }
109 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
110 await waitJobs(servers)
111
112 for (const server of servers) {
113 try {
114 await checkStoryboard({ server, uuid, tilesCount: 6, minSize: 250 })
115 } catch { // FIXME: to remove after ffmpeg CI upgrade, ffmpeg CI version (4.3) generates a 7.6s length video
116 await checkStoryboard({ server, uuid, tilesCount: 8, minSize: 250 })
117 }
118 }
119 })
120
121 it('Should generate a storyboard after HTTP import', async function () {
122 this.timeout(120000)
123
124 if (areHttpImportTestsDisabled()) return
125
126 // 3s video
127 const { video } = await servers[0].imports.importVideo({
128 attributes: {
129 targetUrl: FIXTURE_URLS.goodVideo,
130 channelId: servers[0].store.channel.id,
131 privacy: VideoPrivacy.PUBLIC
132 }
133 })
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 await checkStoryboard({ server, uuid: video.uuid, tilesCount: 3 })
138 }
139 })
140
141 it('Should generate a storyboard after torrent import', async function () {
142 this.timeout(120000)
143
144 if (areHttpImportTestsDisabled()) return
145
146 // 10s video
147 const { video } = await servers[0].imports.importVideo({
148 attributes: {
149 magnetUri: FIXTURE_URLS.magnet,
150 channelId: servers[0].store.channel.id,
151 privacy: VideoPrivacy.PUBLIC
152 }
153 })
154 await waitJobs(servers)
155
156 for (const server of servers) {
157 await checkStoryboard({ server, uuid: video.uuid, tilesCount: 10 })
158 }
159 })
160
161 it('Should generate a storyboard after a live', async function () {
162 this.timeout(240000)
163
164 await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
165
166 const { live, video } = await servers[0].live.quickCreate({
167 saveReplay: true,
168 permanentLive: false,
169 privacy: VideoPrivacy.PUBLIC
170 })
171
172 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
173 await servers[0].live.waitUntilPublished({ videoId: video.id })
174
175 await stopFfmpeg(ffmpegCommand)
176
177 await servers[0].live.waitUntilReplacedByReplay({ videoId: video.id })
178 await waitJobs(servers)
179
180 for (const server of servers) {
181 await checkStoryboard({ server, uuid: video.uuid })
182 }
183 })
184
185 it('Should cleanup storyboards on video deletion', async function () {
186 this.timeout(60000)
187
188 const { storyboards } = await servers[0].storyboard.list({ id: baseUUID })
189 const storyboardName = basename(storyboards[0].storyboardPath)
190
191 const listFiles = () => {
192 const storyboardPath = servers[0].getDirectoryPath('storyboards')
193 return readdir(storyboardPath)
194 }
195
196 {
197 const storyboads = await listFiles()
198 expect(storyboads).to.include(storyboardName)
199 }
200
201 await servers[0].videos.remove({ id: baseUUID })
202 await waitJobs(servers)
203
204 {
205 const storyboads = await listFiles()
206 expect(storyboads).to.not.include(storyboardName)
207 }
208 })
209
210 after(async function () {
211 await cleanupTests(servers)
212 })
213})
diff --git a/packages/tests/src/api/videos/videos-common-filters.ts b/packages/tests/src/api/videos/videos-common-filters.ts
new file mode 100644
index 000000000..9e75bd6ca
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-common-filters.ts
@@ -0,0 +1,499 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pick } from '@peertube/peertube-core-utils'
5import {
6 HttpStatusCode,
7 HttpStatusCodeType,
8 UserRole,
9 Video,
10 VideoDetails,
11 VideoInclude,
12 VideoIncludeType,
13 VideoPrivacy,
14 VideoPrivacyType
15} from '@peertube/peertube-models'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 makeGetRequest,
21 PeerTubeServer,
22 setAccessTokensToServers,
23 setDefaultAccountAvatar,
24 setDefaultVideoChannel,
25 waitJobs
26} from '@peertube/peertube-server-commands'
27
28describe('Test videos filter', function () {
29 let servers: PeerTubeServer[]
30 let paths: string[]
31 let remotePaths: string[]
32
33 const subscriptionVideosPath = '/api/v1/users/me/subscriptions/videos'
34
35 // ---------------------------------------------------------------
36
37 before(async function () {
38 this.timeout(240000)
39
40 servers = await createMultipleServers(2)
41
42 await setAccessTokensToServers(servers)
43 await setDefaultVideoChannel(servers)
44 await setDefaultAccountAvatar(servers)
45
46 await servers[1].config.enableMinimumTranscoding()
47
48 for (const server of servers) {
49 const moderator = { username: 'moderator', password: 'my super password' }
50 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
51 server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
52
53 await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
54
55 {
56 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
57 await server.videos.upload({ attributes })
58 }
59
60 {
61 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
62 await server.videos.upload({ attributes })
63 }
64
65 // Subscribing to itself
66 await server.subscriptions.add({ targetUri: 'root_channel@' + server.host })
67 }
68
69 await doubleFollow(servers[0], servers[1])
70
71 paths = [
72 `/api/v1/video-channels/root_channel/videos`,
73 `/api/v1/accounts/root/videos`,
74 '/api/v1/videos',
75 '/api/v1/search/videos',
76 subscriptionVideosPath
77 ]
78
79 remotePaths = [
80 `/api/v1/video-channels/root_channel@${servers[1].host}/videos`,
81 `/api/v1/accounts/root@${servers[1].host}/videos`,
82 '/api/v1/videos',
83 '/api/v1/search/videos'
84 ]
85 })
86
87 describe('Check videos filters', function () {
88
89 async function listVideos (options: {
90 server: PeerTubeServer
91 path: string
92 isLocal?: boolean
93 hasWebVideoFiles?: boolean
94 hasHLSFiles?: boolean
95 include?: VideoIncludeType
96 privacyOneOf?: VideoPrivacyType[]
97 category?: number
98 tagsAllOf?: string[]
99 token?: string
100 expectedStatus?: HttpStatusCodeType
101 excludeAlreadyWatched?: boolean
102 }) {
103 const res = await makeGetRequest({
104 url: options.server.url,
105 path: options.path,
106 token: options.token ?? options.server.accessToken,
107 query: {
108 ...pick(options, [
109 'isLocal',
110 'include',
111 'category',
112 'tagsAllOf',
113 'hasWebVideoFiles',
114 'hasHLSFiles',
115 'privacyOneOf',
116 'excludeAlreadyWatched'
117 ]),
118
119 sort: 'createdAt'
120 },
121 expectedStatus: options.expectedStatus ?? HttpStatusCode.OK_200
122 })
123
124 return res.body.data as Video[]
125 }
126
127 async function getVideosNames (
128 options: {
129 server: PeerTubeServer
130 isLocal?: boolean
131 include?: VideoIncludeType
132 privacyOneOf?: VideoPrivacyType[]
133 token?: string
134 expectedStatus?: HttpStatusCodeType
135 skipSubscription?: boolean
136 excludeAlreadyWatched?: boolean
137 }
138 ) {
139 const { skipSubscription = false } = options
140 const videosResults: string[][] = []
141
142 for (const path of paths) {
143 if (skipSubscription && path === subscriptionVideosPath) continue
144
145 const videos = await listVideos({ ...options, path })
146
147 videosResults.push(videos.map(v => v.name))
148 }
149
150 return videosResults
151 }
152
153 it('Should display local videos', async function () {
154 for (const server of servers) {
155 const namesResults = await getVideosNames({ server, isLocal: true })
156
157 for (const names of namesResults) {
158 expect(names).to.have.lengthOf(1)
159 expect(names[0]).to.equal('public ' + server.serverNumber)
160 }
161 }
162 })
163
164 it('Should display local videos with hidden privacy by the admin or the moderator', async function () {
165 for (const server of servers) {
166 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
167
168 const namesResults = await getVideosNames(
169 {
170 server,
171 token,
172 isLocal: true,
173 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ],
174 skipSubscription: true
175 }
176 )
177
178 for (const names of namesResults) {
179 expect(names).to.have.lengthOf(3)
180
181 expect(names[0]).to.equal('public ' + server.serverNumber)
182 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
183 expect(names[2]).to.equal('private ' + server.serverNumber)
184 }
185 }
186 }
187 })
188
189 it('Should display all videos by the admin or the moderator', async function () {
190 for (const server of servers) {
191 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
192
193 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
194 server,
195 token,
196 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
197 })
198
199 expect(channelVideos).to.have.lengthOf(3)
200 expect(accountVideos).to.have.lengthOf(3)
201
202 expect(videos).to.have.lengthOf(5)
203 expect(searchVideos).to.have.lengthOf(5)
204 }
205 }
206 })
207
208 it('Should display only remote videos', async function () {
209 this.timeout(120000)
210
211 await servers[1].videos.upload({ attributes: { name: 'remote video' } })
212
213 await waitJobs(servers)
214
215 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
216
217 for (const path of remotePaths) {
218 {
219 const videos = await listVideos({ server: servers[0], path })
220 const video = finder(videos)
221 expect(video).to.exist
222 }
223
224 {
225 const videos = await listVideos({ server: servers[0], path, isLocal: false })
226 const video = finder(videos)
227 expect(video).to.exist
228 }
229
230 {
231 const videos = await listVideos({ server: servers[0], path, isLocal: true })
232 const video = finder(videos)
233 expect(video).to.not.exist
234 }
235 }
236 })
237
238 it('Should include not published videos', async function () {
239 await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
240 await servers[0].live.create({ fields: { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } })
241
242 const finder = (videos: Video[]) => videos.find(v => v.name === 'live video')
243
244 for (const path of paths) {
245 {
246 const videos = await listVideos({ server: servers[0], path })
247 const video = finder(videos)
248 expect(video).to.not.exist
249 expect(videos[0].state).to.not.exist
250 expect(videos[0].waitTranscoding).to.not.exist
251 }
252
253 {
254 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.NOT_PUBLISHED_STATE })
255 const video = finder(videos)
256 expect(video).to.exist
257 expect(video.state).to.exist
258 }
259 }
260 })
261
262 it('Should include blacklisted videos', async function () {
263 const { id } = await servers[0].videos.upload({ attributes: { name: 'blacklisted' } })
264
265 await servers[0].blacklist.add({ videoId: id })
266
267 const finder = (videos: Video[]) => videos.find(v => v.name === 'blacklisted')
268
269 for (const path of paths) {
270 {
271 const videos = await listVideos({ server: servers[0], path })
272 const video = finder(videos)
273 expect(video).to.not.exist
274 expect(videos[0].blacklisted).to.not.exist
275 }
276
277 {
278 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLACKLISTED })
279 const video = finder(videos)
280 expect(video).to.exist
281 expect(video.blacklisted).to.be.true
282 }
283 }
284 })
285
286 it('Should include videos from muted account', async function () {
287 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
288
289 await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host })
290
291 for (const path of remotePaths) {
292 {
293 const videos = await listVideos({ server: servers[0], path })
294 const video = finder(videos)
295 expect(video).to.not.exist
296
297 // Some paths won't have videos
298 if (videos[0]) {
299 expect(videos[0].blockedOwner).to.not.exist
300 expect(videos[0].blockedServer).to.not.exist
301 }
302 }
303
304 {
305 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
306
307 const video = finder(videos)
308 expect(video).to.exist
309 expect(video.blockedServer).to.be.false
310 expect(video.blockedOwner).to.be.true
311 }
312 }
313
314 await servers[0].blocklist.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
315 })
316
317 it('Should include videos from muted server', async function () {
318 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
319
320 await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
321
322 for (const path of remotePaths) {
323 {
324 const videos = await listVideos({ server: servers[0], path })
325 const video = finder(videos)
326 expect(video).to.not.exist
327
328 // Some paths won't have videos
329 if (videos[0]) {
330 expect(videos[0].blockedOwner).to.not.exist
331 expect(videos[0].blockedServer).to.not.exist
332 }
333 }
334
335 {
336 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
337 const video = finder(videos)
338 expect(video).to.exist
339 expect(video.blockedServer).to.be.true
340 expect(video.blockedOwner).to.be.false
341 }
342 }
343
344 await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
345 })
346
347 it('Should include video files', async function () {
348 for (const path of paths) {
349 {
350 const videos = await listVideos({ server: servers[0], path })
351
352 for (const video of videos) {
353 const videoWithFiles = video as VideoDetails
354
355 expect(videoWithFiles.files).to.not.exist
356 expect(videoWithFiles.streamingPlaylists).to.not.exist
357 }
358 }
359
360 {
361 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES })
362
363 for (const video of videos) {
364 const videoWithFiles = video as VideoDetails
365
366 expect(videoWithFiles.files).to.exist
367 expect(videoWithFiles.files).to.have.length.at.least(1)
368 }
369 }
370 }
371 })
372
373 it('Should filter by tags and category', async function () {
374 await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
375 await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })
376
377 for (const path of paths) {
378 {
379 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
380 expect(videos).to.have.lengthOf(1)
381 expect(videos[0].name).to.equal('tag filter')
382 }
383
384 {
385 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag3' ] })
386 expect(videos).to.have.lengthOf(0)
387 }
388
389 {
390 const { data, total } = await servers[0].videos.list({ tagsAllOf: [ 'tag3' ], categoryOneOf: [ 4 ] })
391 expect(total).to.equal(1)
392 expect(data[0].name).to.equal('tag filter with category')
393 }
394
395 {
396 const { total } = await servers[0].videos.list({ tagsAllOf: [ 'tag4' ], categoryOneOf: [ 4 ] })
397 expect(total).to.equal(0)
398 }
399 }
400 })
401
402 it('Should filter by HLS or Web Video files', async function () {
403 this.timeout(360000)
404
405 const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name)
406
407 await servers[0].config.enableTranscoding({ hls: false, webVideo: true })
408 await servers[0].videos.upload({ attributes: { name: 'web video' } })
409 const hasWebVideo = finderFactory('web video')
410
411 await waitJobs(servers)
412
413 await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
414 await servers[0].videos.upload({ attributes: { name: 'hls video' } })
415 const hasHLS = finderFactory('hls video')
416
417 await waitJobs(servers)
418
419 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
420 await servers[0].videos.upload({ attributes: { name: 'hls and web video' } })
421 const hasBoth = finderFactory('hls and web video')
422
423 await waitJobs(servers)
424
425 for (const path of paths) {
426 {
427 const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: true })
428
429 expect(hasWebVideo(videos)).to.be.true
430 expect(hasHLS(videos)).to.be.false
431 expect(hasBoth(videos)).to.be.true
432 }
433
434 {
435 const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: false })
436
437 expect(hasWebVideo(videos)).to.be.false
438 expect(hasHLS(videos)).to.be.true
439 expect(hasBoth(videos)).to.be.false
440 }
441
442 {
443 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true })
444
445 expect(hasWebVideo(videos)).to.be.false
446 expect(hasHLS(videos)).to.be.true
447 expect(hasBoth(videos)).to.be.true
448 }
449
450 {
451 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false })
452
453 expect(hasWebVideo(videos)).to.be.true
454 expect(hasHLS(videos)).to.be.false
455 expect(hasBoth(videos)).to.be.false
456 }
457
458 {
459 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebVideoFiles: false })
460
461 expect(hasWebVideo(videos)).to.be.false
462 expect(hasHLS(videos)).to.be.false
463 expect(hasBoth(videos)).to.be.false
464 }
465
466 {
467 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebVideoFiles: true })
468
469 expect(hasWebVideo(videos)).to.be.false
470 expect(hasHLS(videos)).to.be.false
471 expect(hasBoth(videos)).to.be.true
472 }
473 }
474 })
475
476 it('Should filter already watched videos by the user', async function () {
477 const { id } = await servers[0].videos.upload({ attributes: { name: 'video for history' } })
478
479 for (const path of paths) {
480 const videos = await listVideos({ server: servers[0], path, isLocal: true, excludeAlreadyWatched: true })
481 const foundVideo = videos.find(video => video.id === id)
482
483 expect(foundVideo).to.not.be.undefined
484 }
485 await servers[0].views.view({ id, currentTime: 1, token: servers[0].accessToken })
486
487 for (const path of paths) {
488 const videos = await listVideos({ server: servers[0], path, excludeAlreadyWatched: true })
489 const foundVideo = videos.find(video => video.id === id)
490
491 expect(foundVideo).to.be.undefined
492 }
493 })
494 })
495
496 after(async function () {
497 await cleanupTests(servers)
498 })
499})
diff --git a/packages/tests/src/api/videos/videos-history.ts b/packages/tests/src/api/videos/videos-history.ts
new file mode 100644
index 000000000..75c0fcebd
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-history.ts
@@ -0,0 +1,230 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { Video } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 killallServers,
10 PeerTubeServer,
11 setAccessTokensToServers
12} from '@peertube/peertube-server-commands'
13
14describe('Test videos history', function () {
15 let server: PeerTubeServer = null
16 let video1Id: number
17 let video1UUID: string
18 let video2UUID: string
19 let video3UUID: string
20 let video3WatchedDate: Date
21 let userAccessToken: string
22
23 before(async function () {
24 this.timeout(120000)
25
26 server = await createSingleServer(1)
27
28 await setAccessTokensToServers([ server ])
29
30 // 10 seconds long
31 const fixture = 'video_short1.webm'
32
33 {
34 const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1', fixture } })
35 video1UUID = uuid
36 video1Id = id
37 }
38
39 {
40 const { uuid } = await server.videos.upload({ attributes: { name: 'video 2', fixture } })
41 video2UUID = uuid
42 }
43
44 {
45 const { uuid } = await server.videos.upload({ attributes: { name: 'video 3', fixture } })
46 video3UUID = uuid
47 }
48
49 userAccessToken = await server.users.generateUserAndToken('user_1')
50 })
51
52 it('Should get videos, without watching history', async function () {
53 const { data } = await server.videos.listWithToken()
54
55 for (const video of data) {
56 const videoDetails = await server.videos.getWithToken({ id: video.id })
57
58 expect(video.userHistory).to.be.undefined
59 expect(videoDetails.userHistory).to.be.undefined
60 }
61 })
62
63 it('Should watch the first and second video', async function () {
64 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
65 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 3 })
66 })
67
68 it('Should return the correct history when listing, searching and getting videos', async function () {
69 const videosOfVideos: Video[][] = []
70
71 {
72 const { data } = await server.videos.listWithToken()
73 videosOfVideos.push(data)
74 }
75
76 {
77 const body = await server.search.searchVideos({ token: server.accessToken, search: 'video' })
78 videosOfVideos.push(body.data)
79 }
80
81 for (const videos of videosOfVideos) {
82 const video1 = videos.find(v => v.uuid === video1UUID)
83 const video2 = videos.find(v => v.uuid === video2UUID)
84 const video3 = videos.find(v => v.uuid === video3UUID)
85
86 expect(video1.userHistory).to.not.be.undefined
87 expect(video1.userHistory.currentTime).to.equal(3)
88
89 expect(video2.userHistory).to.not.be.undefined
90 expect(video2.userHistory.currentTime).to.equal(8)
91
92 expect(video3.userHistory).to.be.undefined
93 }
94
95 {
96 const videoDetails = await server.videos.getWithToken({ id: video1UUID })
97
98 expect(videoDetails.userHistory).to.not.be.undefined
99 expect(videoDetails.userHistory.currentTime).to.equal(3)
100 }
101
102 {
103 const videoDetails = await server.videos.getWithToken({ id: video2UUID })
104
105 expect(videoDetails.userHistory).to.not.be.undefined
106 expect(videoDetails.userHistory.currentTime).to.equal(8)
107 }
108
109 {
110 const videoDetails = await server.videos.getWithToken({ id: video3UUID })
111
112 expect(videoDetails.userHistory).to.be.undefined
113 }
114 })
115
116 it('Should have these videos when listing my history', async function () {
117 video3WatchedDate = new Date()
118 await server.views.view({ id: video3UUID, token: server.accessToken, currentTime: 2 })
119
120 const body = await server.history.list()
121
122 expect(body.total).to.equal(3)
123
124 const videos = body.data
125 expect(videos[0].name).to.equal('video 3')
126 expect(videos[1].name).to.equal('video 1')
127 expect(videos[2].name).to.equal('video 2')
128 })
129
130 it('Should not have videos history on another user', async function () {
131 const body = await server.history.list({ token: userAccessToken })
132
133 expect(body.total).to.equal(0)
134 expect(body.data).to.have.lengthOf(0)
135 })
136
137 it('Should be able to search through videos in my history', async function () {
138 const body = await server.history.list({ search: '2' })
139 expect(body.total).to.equal(1)
140
141 const videos = body.data
142 expect(videos[0].name).to.equal('video 2')
143 })
144
145 it('Should clear my history', async function () {
146 await server.history.removeAll({ beforeDate: video3WatchedDate.toISOString() })
147 })
148
149 it('Should have my history cleared', async function () {
150 const body = await server.history.list()
151 expect(body.total).to.equal(1)
152
153 const videos = body.data
154 expect(videos[0].name).to.equal('video 3')
155 })
156
157 it('Should disable videos history', async function () {
158 await server.users.updateMe({
159 videosHistoryEnabled: false
160 })
161
162 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
163
164 const { data } = await server.history.list()
165 expect(data[0].name).to.not.equal('video 2')
166 })
167
168 it('Should re-enable videos history', async function () {
169 await server.users.updateMe({
170 videosHistoryEnabled: true
171 })
172
173 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
174
175 const { data } = await server.history.list()
176 expect(data[0].name).to.equal('video 2')
177 })
178
179 it('Should not clean old history', async function () {
180 this.timeout(50000)
181
182 await killallServers([ server ])
183
184 await server.run({ history: { videos: { max_age: '10 days' } } })
185
186 await wait(6000)
187
188 // Should still have history
189
190 const body = await server.history.list()
191 expect(body.total).to.equal(2)
192 })
193
194 it('Should clean old history', async function () {
195 this.timeout(50000)
196
197 await killallServers([ server ])
198
199 await server.run({ history: { videos: { max_age: '5 seconds' } } })
200
201 await wait(6000)
202
203 const body = await server.history.list()
204 expect(body.total).to.equal(0)
205 })
206
207 it('Should delete a specific history element', async function () {
208 {
209 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 4 })
210 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
211 }
212
213 {
214 const body = await server.history.list()
215 expect(body.total).to.equal(2)
216 }
217
218 {
219 await server.history.removeElement({ videoId: video1Id })
220
221 const body = await server.history.list()
222 expect(body.total).to.equal(1)
223 expect(body.data[0].uuid).to.equal(video2UUID)
224 }
225 })
226
227 after(async function () {
228 await cleanupTests([ server ])
229 })
230})
diff --git a/packages/tests/src/api/videos/videos-overview.ts b/packages/tests/src/api/videos/videos-overview.ts
new file mode 100644
index 000000000..7d74d6db2
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-overview.ts
@@ -0,0 +1,129 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideosOverview } from '@peertube/peertube-models'
6import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
7
8describe('Test a videos overview', function () {
9 let server: PeerTubeServer = null
10
11 function testOverviewCount (overview: VideosOverview, expected: number) {
12 expect(overview.tags).to.have.lengthOf(expected)
13 expect(overview.categories).to.have.lengthOf(expected)
14 expect(overview.channels).to.have.lengthOf(expected)
15 }
16
17 before(async function () {
18 this.timeout(30000)
19
20 server = await createSingleServer(1)
21
22 await setAccessTokensToServers([ server ])
23 })
24
25 it('Should send empty overview', async function () {
26 const body = await server.overviews.getVideos({ page: 1 })
27
28 testOverviewCount(body, 0)
29 })
30
31 it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () {
32 this.timeout(60000)
33
34 await wait(3000)
35
36 await server.videos.upload({
37 attributes: {
38 name: 'video 0',
39 category: 3,
40 tags: [ 'coucou1', 'coucou2' ]
41 }
42 })
43
44 const body = await server.overviews.getVideos({ page: 1 })
45
46 testOverviewCount(body, 0)
47 })
48
49 it('Should upload another video and include all videos in the overview', async function () {
50 this.timeout(120000)
51
52 {
53 for (let i = 1; i < 6; i++) {
54 await server.videos.upload({
55 attributes: {
56 name: 'video ' + i,
57 category: 3,
58 tags: [ 'coucou1', 'coucou2' ]
59 }
60 })
61 }
62
63 await wait(3000)
64 }
65
66 {
67 const body = await server.overviews.getVideos({ page: 1 })
68
69 testOverviewCount(body, 1)
70 }
71
72 {
73 const overview = await server.overviews.getVideos({ page: 2 })
74
75 expect(overview.tags).to.have.lengthOf(1)
76 expect(overview.categories).to.have.lengthOf(0)
77 expect(overview.channels).to.have.lengthOf(0)
78 }
79 })
80
81 it('Should have the correct overview', async function () {
82 const overview1 = await server.overviews.getVideos({ page: 1 })
83 const overview2 = await server.overviews.getVideos({ page: 2 })
84
85 for (const arr of [ overview1.tags, overview1.categories, overview1.channels, overview2.tags ]) {
86 expect(arr).to.have.lengthOf(1)
87
88 const obj = arr[0]
89
90 expect(obj.videos).to.have.lengthOf(6)
91 expect(obj.videos[0].name).to.equal('video 5')
92 expect(obj.videos[1].name).to.equal('video 4')
93 expect(obj.videos[2].name).to.equal('video 3')
94 expect(obj.videos[3].name).to.equal('video 2')
95 expect(obj.videos[4].name).to.equal('video 1')
96 expect(obj.videos[5].name).to.equal('video 0')
97 }
98
99 const tags = [ overview1.tags[0].tag, overview2.tags[0].tag ]
100 expect(tags.find(t => t === 'coucou1')).to.not.be.undefined
101 expect(tags.find(t => t === 'coucou2')).to.not.be.undefined
102
103 expect(overview1.categories[0].category.id).to.equal(3)
104
105 expect(overview1.channels[0].channel.name).to.equal('root_channel')
106 })
107
108 it('Should hide muted accounts', async function () {
109 const token = await server.users.generateUserAndToken('choco')
110
111 await server.blocklist.addToMyBlocklist({ token, account: 'root@' + server.host })
112
113 {
114 const body = await server.overviews.getVideos({ page: 1 })
115
116 testOverviewCount(body, 1)
117 }
118
119 {
120 const body = await server.overviews.getVideos({ page: 1, token })
121
122 testOverviewCount(body, 0)
123 }
124 })
125
126 after(async function () {
127 await cleanupTests([ server ])
128 })
129})