]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/utils/videos/videos.ts
16ecbfe848851493ee3c8fdade13badc63c67a86
[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 if (attributes.originallyPublishedAt !== undefined) {
354 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
355 }
356
357 for (let i = 0; i < attributes.tags.length; i++) {
358 req.field('tags[' + i + ']', attributes.tags[i])
359 }
360
361 if (attributes.thumbnailfile !== undefined) {
362 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
363 }
364 if (attributes.previewfile !== undefined) {
365 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
366 }
367
368 if (attributes.scheduleUpdate) {
369 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
370
371 if (attributes.scheduleUpdate.privacy) {
372 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
373 }
374 }
375
376 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
377 .expect(specialStatus)
378 }
379
380 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
381 const path = '/api/v1/videos/' + id
382 const body = {}
383
384 if (attributes.name) body['name'] = attributes.name
385 if (attributes.category) body['category'] = attributes.category
386 if (attributes.licence) body['licence'] = attributes.licence
387 if (attributes.language) body['language'] = attributes.language
388 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
389 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
390 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
391 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
392 if (attributes.description) body['description'] = attributes.description
393 if (attributes.tags) body['tags'] = attributes.tags
394 if (attributes.privacy) body['privacy'] = attributes.privacy
395 if (attributes.channelId) body['channelId'] = attributes.channelId
396 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
397
398 // Upload request
399 if (attributes.thumbnailfile || attributes.previewfile) {
400 const attaches: any = {}
401 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
402 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
403
404 return makeUploadRequest({
405 url,
406 method: 'PUT',
407 path,
408 token: accessToken,
409 fields: body,
410 attaches,
411 statusCodeExpected
412 })
413 }
414
415 return makePutBodyRequest({
416 url,
417 path,
418 fields: body,
419 token: accessToken,
420 statusCodeExpected
421 })
422 }
423
424 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
425 const path = '/api/v1/videos/' + id + '/rate'
426
427 return request(url)
428 .put(path)
429 .set('Accept', 'application/json')
430 .set('Authorization', 'Bearer ' + accessToken)
431 .send({ rating })
432 .expect(specialStatus)
433 }
434
435 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
436 return new Promise<any>((res, rej) => {
437 const torrentName = videoUUID + '-' + resolution + '.torrent'
438 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
439 readFile(torrentPath, (err, data) => {
440 if (err) return rej(err)
441
442 return res(parseTorrent(data))
443 })
444 })
445 }
446
447 async function completeVideoCheck (
448 url: string,
449 video: any,
450 attributes: {
451 name: string
452 category: number
453 licence: number
454 language: string
455 nsfw: boolean
456 commentsEnabled: boolean
457 downloadEnabled: boolean
458 description: string
459 publishedAt?: string
460 support: string
461 originallyPublishedAt?: string,
462 account: {
463 name: string
464 host: string
465 }
466 isLocal: boolean
467 tags: string[]
468 privacy: number
469 likes?: number
470 dislikes?: number
471 duration: number
472 channel: {
473 displayName: string
474 name: string
475 description
476 isLocal: boolean
477 }
478 fixture: string
479 files: {
480 resolution: number
481 size: number
482 }[],
483 thumbnailfile?: string
484 previewfile?: string
485 }
486 ) {
487 if (!attributes.likes) attributes.likes = 0
488 if (!attributes.dislikes) attributes.dislikes = 0
489
490 expect(video.name).to.equal(attributes.name)
491 expect(video.category.id).to.equal(attributes.category)
492 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
493 expect(video.licence.id).to.equal(attributes.licence)
494 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
495 expect(video.language.id).to.equal(attributes.language)
496 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
497 expect(video.privacy.id).to.deep.equal(attributes.privacy)
498 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
499 expect(video.nsfw).to.equal(attributes.nsfw)
500 expect(video.description).to.equal(attributes.description)
501 expect(video.account.id).to.be.a('number')
502 expect(video.account.uuid).to.be.a('string')
503 expect(video.account.host).to.equal(attributes.account.host)
504 expect(video.account.name).to.equal(attributes.account.name)
505 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
506 expect(video.channel.name).to.equal(attributes.channel.name)
507 expect(video.likes).to.equal(attributes.likes)
508 expect(video.dislikes).to.equal(attributes.dislikes)
509 expect(video.isLocal).to.equal(attributes.isLocal)
510 expect(video.duration).to.equal(attributes.duration)
511 expect(dateIsValid(video.createdAt)).to.be.true
512 expect(dateIsValid(video.publishedAt)).to.be.true
513 expect(dateIsValid(video.updatedAt)).to.be.true
514
515 if (attributes.publishedAt) {
516 expect(video.publishedAt).to.equal(attributes.publishedAt)
517 }
518
519 if (attributes.originallyPublishedAt) {
520 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
521 } else {
522 expect(video.originallyPublishedAt).to.be.null
523 }
524
525 const res = await getVideo(url, video.uuid)
526 const videoDetails: VideoDetails = res.body
527
528 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
529 expect(videoDetails.tags).to.deep.equal(attributes.tags)
530 expect(videoDetails.account.name).to.equal(attributes.account.name)
531 expect(videoDetails.account.host).to.equal(attributes.account.host)
532 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
533 expect(video.channel.name).to.equal(attributes.channel.name)
534 expect(videoDetails.channel.host).to.equal(attributes.account.host)
535 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
536 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
537 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
538 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
539 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
540
541 for (const attributeFile of attributes.files) {
542 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
543 expect(file).not.to.be.undefined
544
545 let extension = extname(attributes.fixture)
546 // Transcoding enabled on server 2, extension will always be .mp4
547 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
548
549 const magnetUri = file.magnetUri
550 expect(file.magnetUri).to.have.lengthOf.above(2)
551 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
552 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
553 expect(file.resolution.id).to.equal(attributeFile.resolution)
554 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
555
556 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
557 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
558 expect(file.size,
559 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
560 .to.be.above(minSize).and.below(maxSize)
561
562 {
563 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
564 }
565
566 if (attributes.previewfile) {
567 await testImage(url, attributes.previewfile, videoDetails.previewPath)
568 }
569
570 const torrent = await webtorrentAdd(magnetUri, true)
571 expect(torrent.files).to.be.an('array')
572 expect(torrent.files.length).to.equal(1)
573 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
574 }
575 }
576
577 // ---------------------------------------------------------------------------
578
579 export {
580 getVideoDescription,
581 getVideoCategories,
582 getVideoLicences,
583 getVideoPrivacies,
584 getVideoLanguages,
585 getMyVideos,
586 getAccountVideos,
587 getVideoChannelVideos,
588 getVideo,
589 getVideoWithToken,
590 getVideosList,
591 getVideosListPagination,
592 getVideosListSort,
593 removeVideo,
594 getVideosListWithToken,
595 uploadVideo,
596 getVideosWithFilters,
597 updateVideo,
598 rateVideo,
599 viewVideo,
600 parseTorrentVideo,
601 getLocalVideos,
602 completeVideoCheck,
603 checkVideoFilesWereRemoved
604 }