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