]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/utils/videos/videos.ts
add NOCLIENT option to install only server dependencies
[github/Chocobozzz/PeerTube.git] / shared / utils / videos / videos.ts
1 /* tslint:disable:no-unused-expression */
2
3 import { expect } from 'chai'
4 import { existsSync, 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
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 getVideosListPagination (url: string, start: number, count: number, sort?: string) {
227 const path = '/api/v1/videos'
228
229 const req = request(url)
230 .get(path)
231 .query({ start: start })
232 .query({ count: count })
233
234 if (sort) req.query({ sort })
235
236 return req.set('Accept', 'application/json')
237 .expect(200)
238 .expect('Content-Type', /json/)
239 }
240
241 function getVideosListSort (url: string, sort: string) {
242 const path = '/api/v1/videos'
243
244 return request(url)
245 .get(path)
246 .query({ sort: sort })
247 .set('Accept', 'application/json')
248 .expect(200)
249 .expect('Content-Type', /json/)
250 }
251
252 function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
253 const path = '/api/v1/videos'
254
255 return request(url)
256 .get(path)
257 .query(query)
258 .set('Accept', 'application/json')
259 .expect(200)
260 .expect('Content-Type', /json/)
261 }
262
263 function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
264 const path = '/api/v1/videos'
265
266 return request(url)
267 .delete(path + '/' + id)
268 .set('Accept', 'application/json')
269 .set('Authorization', 'Bearer ' + token)
270 .expect(expectedStatus)
271 }
272
273 async function checkVideoFilesWereRemoved (
274 videoUUID: string,
275 serverNumber: number,
276 directories = [
277 'redundancy',
278 'videos',
279 'thumbnails',
280 'torrents',
281 'previews',
282 'captions',
283 join('playlists', 'hls'),
284 join('redundancy', 'hls')
285 ]
286 ) {
287 const testDirectory = 'test' + serverNumber
288
289 for (const directory of directories) {
290 const directoryPath = join(root(), testDirectory, directory)
291
292 const directoryExists = existsSync(directoryPath)
293 if (!directoryExists) continue
294
295 const files = await readdir(directoryPath)
296 for (const file of files) {
297 expect(file).to.not.contain(videoUUID)
298 }
299 }
300 }
301
302 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
303 const path = '/api/v1/videos/upload'
304 let defaultChannelId = '1'
305
306 try {
307 const res = await getMyUserInformation(url, accessToken)
308 defaultChannelId = res.body.videoChannels[0].id
309 } catch (e) { /* empty */ }
310
311 // Override default attributes
312 const attributes = Object.assign({
313 name: 'my super video',
314 category: 5,
315 licence: 4,
316 language: 'zh',
317 channelId: defaultChannelId,
318 nsfw: true,
319 waitTranscoding: false,
320 description: 'my super description',
321 support: 'my super support text',
322 tags: [ 'tag' ],
323 privacy: VideoPrivacy.PUBLIC,
324 commentsEnabled: true,
325 downloadEnabled: true,
326 fixture: 'video_short.webm'
327 }, videoAttributesArg)
328
329 const req = request(url)
330 .post(path)
331 .set('Accept', 'application/json')
332 .set('Authorization', 'Bearer ' + accessToken)
333 .field('name', attributes.name)
334 .field('nsfw', JSON.stringify(attributes.nsfw))
335 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
336 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
337 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
338 .field('privacy', attributes.privacy.toString())
339 .field('channelId', attributes.channelId)
340
341 if (attributes.description !== undefined) {
342 req.field('description', attributes.description)
343 }
344 if (attributes.language !== undefined) {
345 req.field('language', attributes.language.toString())
346 }
347 if (attributes.category !== undefined) {
348 req.field('category', attributes.category.toString())
349 }
350 if (attributes.licence !== undefined) {
351 req.field('licence', attributes.licence.toString())
352 }
353
354 for (let i = 0; i < attributes.tags.length; i++) {
355 req.field('tags[' + i + ']', attributes.tags[i])
356 }
357
358 if (attributes.thumbnailfile !== undefined) {
359 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
360 }
361 if (attributes.previewfile !== undefined) {
362 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
363 }
364
365 if (attributes.scheduleUpdate) {
366 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
367
368 if (attributes.scheduleUpdate.privacy) {
369 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
370 }
371 }
372
373 if (attributes.originallyPublishedAt !== undefined) {
374 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
375 }
376
377 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
378 .expect(specialStatus)
379 }
380
381 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
382 const path = '/api/v1/videos/' + id
383 const body = {}
384
385 if (attributes.name) body['name'] = attributes.name
386 if (attributes.category) body['category'] = attributes.category
387 if (attributes.licence) body['licence'] = attributes.licence
388 if (attributes.language) body['language'] = attributes.language
389 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
390 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
391 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
392 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
393 if (attributes.description) body['description'] = attributes.description
394 if (attributes.tags) body['tags'] = attributes.tags
395 if (attributes.privacy) body['privacy'] = attributes.privacy
396 if (attributes.channelId) body['channelId'] = attributes.channelId
397 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
398
399 // Upload request
400 if (attributes.thumbnailfile || attributes.previewfile) {
401 const attaches: any = {}
402 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
403 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
404
405 return makeUploadRequest({
406 url,
407 method: 'PUT',
408 path,
409 token: accessToken,
410 fields: body,
411 attaches,
412 statusCodeExpected
413 })
414 }
415
416 return makePutBodyRequest({
417 url,
418 path,
419 fields: body,
420 token: accessToken,
421 statusCodeExpected
422 })
423 }
424
425 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
426 const path = '/api/v1/videos/' + id + '/rate'
427
428 return request(url)
429 .put(path)
430 .set('Accept', 'application/json')
431 .set('Authorization', 'Bearer ' + accessToken)
432 .send({ rating })
433 .expect(specialStatus)
434 }
435
436 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
437 return new Promise<any>((res, rej) => {
438 const torrentName = videoUUID + '-' + resolution + '.torrent'
439 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
440 readFile(torrentPath, (err, data) => {
441 if (err) return rej(err)
442
443 return res(parseTorrent(data))
444 })
445 })
446 }
447
448 async function completeVideoCheck (
449 url: string,
450 video: any,
451 attributes: {
452 name: string
453 category: number
454 licence: number
455 language: string
456 nsfw: boolean
457 commentsEnabled: boolean
458 downloadEnabled: boolean
459 description: string
460 publishedAt?: string
461 support: string
462 originallyPublishedAt?: string,
463 account: {
464 name: string
465 host: string
466 }
467 isLocal: boolean
468 tags: string[]
469 privacy: number
470 likes?: number
471 dislikes?: number
472 duration: number
473 channel: {
474 displayName: string
475 name: string
476 description
477 isLocal: boolean
478 }
479 fixture: string
480 files: {
481 resolution: number
482 size: number
483 }[],
484 thumbnailfile?: string
485 previewfile?: string
486 }
487 ) {
488 if (!attributes.likes) attributes.likes = 0
489 if (!attributes.dislikes) attributes.dislikes = 0
490
491 expect(video.name).to.equal(attributes.name)
492 expect(video.category.id).to.equal(attributes.category)
493 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
494 expect(video.licence.id).to.equal(attributes.licence)
495 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
496 expect(video.language.id).to.equal(attributes.language)
497 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
498 expect(video.privacy.id).to.deep.equal(attributes.privacy)
499 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
500 expect(video.nsfw).to.equal(attributes.nsfw)
501 expect(video.description).to.equal(attributes.description)
502 expect(video.account.id).to.be.a('number')
503 expect(video.account.uuid).to.be.a('string')
504 expect(video.account.host).to.equal(attributes.account.host)
505 expect(video.account.name).to.equal(attributes.account.name)
506 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
507 expect(video.channel.name).to.equal(attributes.channel.name)
508 expect(video.likes).to.equal(attributes.likes)
509 expect(video.dislikes).to.equal(attributes.dislikes)
510 expect(video.isLocal).to.equal(attributes.isLocal)
511 expect(video.duration).to.equal(attributes.duration)
512 expect(dateIsValid(video.createdAt)).to.be.true
513 expect(dateIsValid(video.publishedAt)).to.be.true
514 expect(dateIsValid(video.updatedAt)).to.be.true
515
516 if (attributes.publishedAt) {
517 expect(video.publishedAt).to.equal(attributes.publishedAt)
518 }
519
520 if (attributes.originallyPublishedAt) {
521 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
522 } else {
523 expect(video.originallyPublishedAt).to.be.null
524 }
525
526 const res = await getVideo(url, video.uuid)
527 const videoDetails: VideoDetails = res.body
528
529 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
530 expect(videoDetails.tags).to.deep.equal(attributes.tags)
531 expect(videoDetails.account.name).to.equal(attributes.account.name)
532 expect(videoDetails.account.host).to.equal(attributes.account.host)
533 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
534 expect(video.channel.name).to.equal(attributes.channel.name)
535 expect(videoDetails.channel.host).to.equal(attributes.account.host)
536 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
537 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
538 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
539 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
540 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
541
542 for (const attributeFile of attributes.files) {
543 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
544 expect(file).not.to.be.undefined
545
546 let extension = extname(attributes.fixture)
547 // Transcoding enabled on server 2, extension will always be .mp4
548 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
549
550 const magnetUri = file.magnetUri
551 expect(file.magnetUri).to.have.lengthOf.above(2)
552 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
553 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
554 expect(file.resolution.id).to.equal(attributeFile.resolution)
555 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
556
557 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
558 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
559 expect(file.size,
560 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
561 .to.be.above(minSize).and.below(maxSize)
562
563 {
564 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
565 }
566
567 if (attributes.previewfile) {
568 await testImage(url, attributes.previewfile, videoDetails.previewPath)
569 }
570
571 const torrent = await webtorrentAdd(magnetUri, true)
572 expect(torrent.files).to.be.an('array')
573 expect(torrent.files.length).to.equal(1)
574 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
575 }
576 }
577
578 // ---------------------------------------------------------------------------
579
580 export {
581 getVideoDescription,
582 getVideoCategories,
583 getVideoLicences,
584 getVideoPrivacies,
585 getVideoLanguages,
586 getMyVideos,
587 getAccountVideos,
588 getVideoChannelVideos,
589 getVideo,
590 getVideoWithToken,
591 getVideosList,
592 getVideosListPagination,
593 getVideosListSort,
594 removeVideo,
595 getVideosListWithToken,
596 uploadVideo,
597 getVideosWithFilters,
598 updateVideo,
599 rateVideo,
600 viewVideo,
601 parseTorrentVideo,
602 getLocalVideos,
603 completeVideoCheck,
604 checkVideoFilesWereRemoved
605 }