]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/utils/videos/videos.ts
Add playlist rest tests
[github/Chocobozzz/PeerTube.git] / shared / utils / videos / videos.ts
1 /* tslint:disable:no-unused-expression */
2
3 import { expect } from 'chai'
4 import { pathExists, readdir, readFile } from 'fs-extra'
5 import * as parseTorrent from 'parse-torrent'
6 import { extname, join } from 'path'
7 import * as request from 'supertest'
8 import {
9 buildAbsoluteFixturePath,
10 getMyUserInformation,
11 immutableAssign,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 root,
16 ServerInfo,
17 testImage
18 } from '../'
19 import * as validator from 'validator'
20 import { VideoDetails, VideoPrivacy } from '../../models/videos'
21 import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
22 import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
23
24 type VideoAttributes = {
25 name?: string
26 category?: number
27 licence?: number
28 language?: string
29 nsfw?: boolean
30 commentsEnabled?: boolean
31 downloadEnabled?: boolean
32 waitTranscoding?: boolean
33 description?: string
34 originallyPublishedAt?: string
35 tags?: string[]
36 channelId?: number
37 privacy?: VideoPrivacy
38 fixture?: string
39 thumbnailfile?: string
40 previewfile?: string
41 scheduleUpdate?: {
42 updateAt: string
43 privacy?: VideoPrivacy
44 }
45 }
46
47 function getVideoCategories (url: string) {
48 const path = '/api/v1/videos/categories'
49
50 return makeGetRequest({
51 url,
52 path,
53 statusCodeExpected: 200
54 })
55 }
56
57 function getVideoLicences (url: string) {
58 const path = '/api/v1/videos/licences'
59
60 return makeGetRequest({
61 url,
62 path,
63 statusCodeExpected: 200
64 })
65 }
66
67 function getVideoLanguages (url: string) {
68 const path = '/api/v1/videos/languages'
69
70 return makeGetRequest({
71 url,
72 path,
73 statusCodeExpected: 200
74 })
75 }
76
77 function getVideoPrivacies (url: string) {
78 const path = '/api/v1/videos/privacies'
79
80 return makeGetRequest({
81 url,
82 path,
83 statusCodeExpected: 200
84 })
85 }
86
87 function getVideo (url: string, id: number | string, expectedStatus = 200) {
88 const path = '/api/v1/videos/' + id
89
90 return request(url)
91 .get(path)
92 .set('Accept', 'application/json')
93 .expect(expectedStatus)
94 }
95
96 function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
97 const path = '/api/v1/videos/' + id + '/views'
98
99 const req = request(url)
100 .post(path)
101 .set('Accept', 'application/json')
102
103 if (xForwardedFor) {
104 req.set('X-Forwarded-For', xForwardedFor)
105 }
106
107 return req.expect(expectedStatus)
108 }
109
110 function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
111 const path = '/api/v1/videos/' + id
112
113 return request(url)
114 .get(path)
115 .set('Authorization', 'Bearer ' + token)
116 .set('Accept', 'application/json')
117 .expect(expectedStatus)
118 }
119
120 function getVideoDescription (url: string, descriptionPath: string) {
121 return request(url)
122 .get(descriptionPath)
123 .set('Accept', 'application/json')
124 .expect(200)
125 .expect('Content-Type', /json/)
126 }
127
128 function getVideosList (url: string) {
129 const path = '/api/v1/videos'
130
131 return request(url)
132 .get(path)
133 .query({ sort: 'name' })
134 .set('Accept', 'application/json')
135 .expect(200)
136 .expect('Content-Type', /json/)
137 }
138
139 function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
140 const path = '/api/v1/videos'
141
142 return request(url)
143 .get(path)
144 .set('Authorization', 'Bearer ' + token)
145 .query(immutableAssign(query, { sort: 'name' }))
146 .set('Accept', 'application/json')
147 .expect(200)
148 .expect('Content-Type', /json/)
149 }
150
151 function getLocalVideos (url: string) {
152 const path = '/api/v1/videos'
153
154 return request(url)
155 .get(path)
156 .query({ sort: 'name', filter: 'local' })
157 .set('Accept', 'application/json')
158 .expect(200)
159 .expect('Content-Type', /json/)
160 }
161
162 function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
163 const path = '/api/v1/users/me/videos'
164
165 const req = request(url)
166 .get(path)
167 .query({ start: start })
168 .query({ count: count })
169
170 if (sort) req.query({ sort })
171
172 return req.set('Accept', 'application/json')
173 .set('Authorization', 'Bearer ' + accessToken)
174 .expect(200)
175 .expect('Content-Type', /json/)
176 }
177
178 function getAccountVideos (
179 url: string,
180 accessToken: string,
181 accountName: string,
182 start: number,
183 count: number,
184 sort?: string,
185 query: { nsfw?: boolean } = {}
186 ) {
187 const path = '/api/v1/accounts/' + accountName + '/videos'
188
189 return makeGetRequest({
190 url,
191 path,
192 query: immutableAssign(query, {
193 start,
194 count,
195 sort
196 }),
197 token: accessToken,
198 statusCodeExpected: 200
199 })
200 }
201
202 function getVideoChannelVideos (
203 url: string,
204 accessToken: string,
205 videoChannelName: string,
206 start: number,
207 count: number,
208 sort?: string,
209 query: { nsfw?: boolean } = {}
210 ) {
211 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
212
213 return makeGetRequest({
214 url,
215 path,
216 query: immutableAssign(query, {
217 start,
218 count,
219 sort
220 }),
221 token: accessToken,
222 statusCodeExpected: 200
223 })
224 }
225
226 function getPlaylistVideos (
227 url: string,
228 accessToken: string,
229 playlistId: number | string,
230 start: number,
231 count: number,
232 query: { nsfw?: boolean } = {}
233 ) {
234 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
235
236 return makeGetRequest({
237 url,
238 path,
239 query: immutableAssign(query, {
240 start,
241 count
242 }),
243 token: accessToken,
244 statusCodeExpected: 200
245 })
246 }
247
248 function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
249 const path = '/api/v1/videos'
250
251 const req = request(url)
252 .get(path)
253 .query({ start: start })
254 .query({ count: count })
255
256 if (sort) req.query({ sort })
257
258 return req.set('Accept', 'application/json')
259 .expect(200)
260 .expect('Content-Type', /json/)
261 }
262
263 function getVideosListSort (url: string, sort: string) {
264 const path = '/api/v1/videos'
265
266 return request(url)
267 .get(path)
268 .query({ sort: sort })
269 .set('Accept', 'application/json')
270 .expect(200)
271 .expect('Content-Type', /json/)
272 }
273
274 function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
275 const path = '/api/v1/videos'
276
277 return request(url)
278 .get(path)
279 .query(query)
280 .set('Accept', 'application/json')
281 .expect(200)
282 .expect('Content-Type', /json/)
283 }
284
285 function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
286 const path = '/api/v1/videos'
287
288 return request(url)
289 .delete(path + '/' + id)
290 .set('Accept', 'application/json')
291 .set('Authorization', 'Bearer ' + token)
292 .expect(expectedStatus)
293 }
294
295 async function checkVideoFilesWereRemoved (
296 videoUUID: string,
297 serverNumber: number,
298 directories = [
299 'redundancy',
300 'videos',
301 'thumbnails',
302 'torrents',
303 'previews',
304 'captions',
305 join('playlists', 'hls'),
306 join('redundancy', 'hls')
307 ]
308 ) {
309 const testDirectory = 'test' + serverNumber
310
311 for (const directory of directories) {
312 const directoryPath = join(root(), testDirectory, directory)
313
314 const directoryExists = await pathExists(directoryPath)
315 if (directoryExists === false) continue
316
317 const files = await readdir(directoryPath)
318 for (const file of files) {
319 expect(file).to.not.contain(videoUUID)
320 }
321 }
322 }
323
324 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
325 const path = '/api/v1/videos/upload'
326 let defaultChannelId = '1'
327
328 try {
329 const res = await getMyUserInformation(url, accessToken)
330 defaultChannelId = res.body.videoChannels[0].id
331 } catch (e) { /* empty */ }
332
333 // Override default attributes
334 const attributes = Object.assign({
335 name: 'my super video',
336 category: 5,
337 licence: 4,
338 language: 'zh',
339 channelId: defaultChannelId,
340 nsfw: true,
341 waitTranscoding: false,
342 description: 'my super description',
343 support: 'my super support text',
344 tags: [ 'tag' ],
345 privacy: VideoPrivacy.PUBLIC,
346 commentsEnabled: true,
347 downloadEnabled: true,
348 fixture: 'video_short.webm'
349 }, videoAttributesArg)
350
351 const req = request(url)
352 .post(path)
353 .set('Accept', 'application/json')
354 .set('Authorization', 'Bearer ' + accessToken)
355 .field('name', attributes.name)
356 .field('nsfw', JSON.stringify(attributes.nsfw))
357 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
358 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
359 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
360 .field('privacy', attributes.privacy.toString())
361 .field('channelId', attributes.channelId)
362
363 if (attributes.description !== undefined) {
364 req.field('description', attributes.description)
365 }
366 if (attributes.language !== undefined) {
367 req.field('language', attributes.language.toString())
368 }
369 if (attributes.category !== undefined) {
370 req.field('category', attributes.category.toString())
371 }
372 if (attributes.licence !== undefined) {
373 req.field('licence', attributes.licence.toString())
374 }
375
376 for (let i = 0; i < attributes.tags.length; i++) {
377 req.field('tags[' + i + ']', attributes.tags[i])
378 }
379
380 if (attributes.thumbnailfile !== undefined) {
381 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
382 }
383 if (attributes.previewfile !== undefined) {
384 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
385 }
386
387 if (attributes.scheduleUpdate) {
388 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
389
390 if (attributes.scheduleUpdate.privacy) {
391 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
392 }
393 }
394
395 if (attributes.originallyPublishedAt !== undefined) {
396 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
397 }
398
399 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
400 .expect(specialStatus)
401 }
402
403 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
404 const path = '/api/v1/videos/' + id
405 const body = {}
406
407 if (attributes.name) body['name'] = attributes.name
408 if (attributes.category) body['category'] = attributes.category
409 if (attributes.licence) body['licence'] = attributes.licence
410 if (attributes.language) body['language'] = attributes.language
411 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
412 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
413 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
414 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
415 if (attributes.description) body['description'] = attributes.description
416 if (attributes.tags) body['tags'] = attributes.tags
417 if (attributes.privacy) body['privacy'] = attributes.privacy
418 if (attributes.channelId) body['channelId'] = attributes.channelId
419 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
420
421 // Upload request
422 if (attributes.thumbnailfile || attributes.previewfile) {
423 const attaches: any = {}
424 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
425 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
426
427 return makeUploadRequest({
428 url,
429 method: 'PUT',
430 path,
431 token: accessToken,
432 fields: body,
433 attaches,
434 statusCodeExpected
435 })
436 }
437
438 return makePutBodyRequest({
439 url,
440 path,
441 fields: body,
442 token: accessToken,
443 statusCodeExpected
444 })
445 }
446
447 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
448 const path = '/api/v1/videos/' + id + '/rate'
449
450 return request(url)
451 .put(path)
452 .set('Accept', 'application/json')
453 .set('Authorization', 'Bearer ' + accessToken)
454 .send({ rating })
455 .expect(specialStatus)
456 }
457
458 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
459 return new Promise<any>((res, rej) => {
460 const torrentName = videoUUID + '-' + resolution + '.torrent'
461 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
462 readFile(torrentPath, (err, data) => {
463 if (err) return rej(err)
464
465 return res(parseTorrent(data))
466 })
467 })
468 }
469
470 async function completeVideoCheck (
471 url: string,
472 video: any,
473 attributes: {
474 name: string
475 category: number
476 licence: number
477 language: string
478 nsfw: boolean
479 commentsEnabled: boolean
480 downloadEnabled: boolean
481 description: string
482 publishedAt?: string
483 support: string
484 originallyPublishedAt?: string,
485 account: {
486 name: string
487 host: string
488 }
489 isLocal: boolean
490 tags: string[]
491 privacy: number
492 likes?: number
493 dislikes?: number
494 duration: number
495 channel: {
496 displayName: string
497 name: string
498 description
499 isLocal: boolean
500 }
501 fixture: string
502 files: {
503 resolution: number
504 size: number
505 }[],
506 thumbnailfile?: string
507 previewfile?: string
508 }
509 ) {
510 if (!attributes.likes) attributes.likes = 0
511 if (!attributes.dislikes) attributes.dislikes = 0
512
513 expect(video.name).to.equal(attributes.name)
514 expect(video.category.id).to.equal(attributes.category)
515 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
516 expect(video.licence.id).to.equal(attributes.licence)
517 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
518 expect(video.language.id).to.equal(attributes.language)
519 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
520 expect(video.privacy.id).to.deep.equal(attributes.privacy)
521 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
522 expect(video.nsfw).to.equal(attributes.nsfw)
523 expect(video.description).to.equal(attributes.description)
524 expect(video.account.id).to.be.a('number')
525 expect(video.account.uuid).to.be.a('string')
526 expect(video.account.host).to.equal(attributes.account.host)
527 expect(video.account.name).to.equal(attributes.account.name)
528 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
529 expect(video.channel.name).to.equal(attributes.channel.name)
530 expect(video.likes).to.equal(attributes.likes)
531 expect(video.dislikes).to.equal(attributes.dislikes)
532 expect(video.isLocal).to.equal(attributes.isLocal)
533 expect(video.duration).to.equal(attributes.duration)
534 expect(dateIsValid(video.createdAt)).to.be.true
535 expect(dateIsValid(video.publishedAt)).to.be.true
536 expect(dateIsValid(video.updatedAt)).to.be.true
537
538 if (attributes.publishedAt) {
539 expect(video.publishedAt).to.equal(attributes.publishedAt)
540 }
541
542 if (attributes.originallyPublishedAt) {
543 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
544 } else {
545 expect(video.originallyPublishedAt).to.be.null
546 }
547
548 const res = await getVideo(url, video.uuid)
549 const videoDetails: VideoDetails = res.body
550
551 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
552 expect(videoDetails.tags).to.deep.equal(attributes.tags)
553 expect(videoDetails.account.name).to.equal(attributes.account.name)
554 expect(videoDetails.account.host).to.equal(attributes.account.host)
555 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
556 expect(video.channel.name).to.equal(attributes.channel.name)
557 expect(videoDetails.channel.host).to.equal(attributes.account.host)
558 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
559 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
560 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
561 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
562 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
563
564 for (const attributeFile of attributes.files) {
565 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
566 expect(file).not.to.be.undefined
567
568 let extension = extname(attributes.fixture)
569 // Transcoding enabled on server 2, extension will always be .mp4
570 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
571
572 const magnetUri = file.magnetUri
573 expect(file.magnetUri).to.have.lengthOf.above(2)
574 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
575 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
576 expect(file.resolution.id).to.equal(attributeFile.resolution)
577 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
578
579 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
580 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
581 expect(file.size,
582 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
583 .to.be.above(minSize).and.below(maxSize)
584
585 {
586 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
587 }
588
589 if (attributes.previewfile) {
590 await testImage(url, attributes.previewfile, videoDetails.previewPath)
591 }
592
593 const torrent = await webtorrentAdd(magnetUri, true)
594 expect(torrent.files).to.be.an('array')
595 expect(torrent.files.length).to.equal(1)
596 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
597 }
598 }
599
600 async function videoUUIDToId (url: string, id: number | string) {
601 if (validator.isUUID('' + id) === false) return id
602
603 const res = await getVideo(url, id)
604 return res.body.id
605 }
606
607 async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
608 const videoAttrs: any = { name: options.videoName }
609 if (options.nsfw) videoAttrs.nsfw = options.nsfw
610
611
612 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
613
614 return { id: res.body.video.id, uuid: res.body.video.uuid }
615 }
616
617 // ---------------------------------------------------------------------------
618
619 export {
620 getVideoDescription,
621 getVideoCategories,
622 getVideoLicences,
623 videoUUIDToId,
624 getVideoPrivacies,
625 getVideoLanguages,
626 getMyVideos,
627 getAccountVideos,
628 getVideoChannelVideos,
629 getVideo,
630 getVideoWithToken,
631 getVideosList,
632 getVideosListPagination,
633 getVideosListSort,
634 removeVideo,
635 getVideosListWithToken,
636 uploadVideo,
637 getVideosWithFilters,
638 updateVideo,
639 rateVideo,
640 viewVideo,
641 parseTorrentVideo,
642 getLocalVideos,
643 completeVideoCheck,
644 checkVideoFilesWereRemoved,
645 getPlaylistVideos,
646 uploadVideoAndGetId
647 }