]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/videos.ts
WIP plugins: move plugin CLI in peertube script
[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 * as 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 } 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) {
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
172 if (sort) req.query({ sort })
173
174 return req.set('Accept', 'application/json')
175 .set('Authorization', 'Bearer ' + accessToken)
176 .expect(200)
177 .expect('Content-Type', /json/)
178 }
179
180 function getAccountVideos (
181 url: string,
182 accessToken: string,
183 accountName: string,
184 start: number,
185 count: number,
186 sort?: string,
187 query: { nsfw?: boolean } = {}
188 ) {
189 const path = '/api/v1/accounts/' + accountName + '/videos'
190
191 return makeGetRequest({
192 url,
193 path,
194 query: immutableAssign(query, {
195 start,
196 count,
197 sort
198 }),
199 token: accessToken,
200 statusCodeExpected: 200
201 })
202 }
203
204 function getVideoChannelVideos (
205 url: string,
206 accessToken: string,
207 videoChannelName: string,
208 start: number,
209 count: number,
210 sort?: string,
211 query: { nsfw?: boolean } = {}
212 ) {
213 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
214
215 return makeGetRequest({
216 url,
217 path,
218 query: immutableAssign(query, {
219 start,
220 count,
221 sort
222 }),
223 token: accessToken,
224 statusCodeExpected: 200
225 })
226 }
227
228 function getPlaylistVideos (
229 url: string,
230 accessToken: string,
231 playlistId: number | string,
232 start: number,
233 count: number,
234 query: { nsfw?: boolean } = {}
235 ) {
236 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
237
238 return makeGetRequest({
239 url,
240 path,
241 query: immutableAssign(query, {
242 start,
243 count
244 }),
245 token: accessToken,
246 statusCodeExpected: 200
247 })
248 }
249
250 function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
251 const path = '/api/v1/videos'
252
253 const req = request(url)
254 .get(path)
255 .query({ start: start })
256 .query({ count: count })
257
258 if (sort) req.query({ sort })
259
260 return req.set('Accept', 'application/json')
261 .expect(200)
262 .expect('Content-Type', /json/)
263 }
264
265 function getVideosListSort (url: string, sort: string) {
266 const path = '/api/v1/videos'
267
268 return request(url)
269 .get(path)
270 .query({ sort: sort })
271 .set('Accept', 'application/json')
272 .expect(200)
273 .expect('Content-Type', /json/)
274 }
275
276 function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
277 const path = '/api/v1/videos'
278
279 return request(url)
280 .get(path)
281 .query(query)
282 .set('Accept', 'application/json')
283 .expect(200)
284 .expect('Content-Type', /json/)
285 }
286
287 function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
288 const path = '/api/v1/videos'
289
290 return request(url)
291 .delete(path + '/' + id)
292 .set('Accept', 'application/json')
293 .set('Authorization', 'Bearer ' + token)
294 .expect(expectedStatus)
295 }
296
297 async function checkVideoFilesWereRemoved (
298 videoUUID: string,
299 serverNumber: number,
300 directories = [
301 'redundancy',
302 'videos',
303 'thumbnails',
304 'torrents',
305 'previews',
306 'captions',
307 join('playlists', 'hls'),
308 join('redundancy', 'hls')
309 ]
310 ) {
311 const testDirectory = 'test' + serverNumber
312
313 for (const directory of directories) {
314 const directoryPath = join(root(), testDirectory, directory)
315
316 const directoryExists = await pathExists(directoryPath)
317 if (directoryExists === false) continue
318
319 const files = await readdir(directoryPath)
320 for (const file of files) {
321 expect(file).to.not.contain(videoUUID)
322 }
323 }
324 }
325
326 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
327 const path = '/api/v1/videos/upload'
328 let defaultChannelId = '1'
329
330 try {
331 const res = await getMyUserInformation(url, accessToken)
332 defaultChannelId = res.body.videoChannels[0].id
333 } catch (e) { /* empty */ }
334
335 // Override default attributes
336 const attributes = Object.assign({
337 name: 'my super video',
338 category: 5,
339 licence: 4,
340 language: 'zh',
341 channelId: defaultChannelId,
342 nsfw: true,
343 waitTranscoding: false,
344 description: 'my super description',
345 support: 'my super support text',
346 tags: [ 'tag' ],
347 privacy: VideoPrivacy.PUBLIC,
348 commentsEnabled: true,
349 downloadEnabled: true,
350 fixture: 'video_short.webm'
351 }, videoAttributesArg)
352
353 const req = request(url)
354 .post(path)
355 .set('Accept', 'application/json')
356 .set('Authorization', 'Bearer ' + accessToken)
357 .field('name', attributes.name)
358 .field('nsfw', JSON.stringify(attributes.nsfw))
359 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
360 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
361 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
362 .field('privacy', attributes.privacy.toString())
363 .field('channelId', attributes.channelId)
364
365 if (attributes.support !== undefined) {
366 req.field('support', attributes.support)
367 }
368
369 if (attributes.description !== undefined) {
370 req.field('description', attributes.description)
371 }
372 if (attributes.language !== undefined) {
373 req.field('language', attributes.language.toString())
374 }
375 if (attributes.category !== undefined) {
376 req.field('category', attributes.category.toString())
377 }
378 if (attributes.licence !== undefined) {
379 req.field('licence', attributes.licence.toString())
380 }
381
382 for (let i = 0; i < attributes.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 const magnetUri = file.magnetUri
578 expect(file.magnetUri).to.have.lengthOf.above(2)
579 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
580 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
581 expect(file.resolution.id).to.equal(attributeFile.resolution)
582 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
583
584 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
585 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
586 expect(file.size,
587 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
588 .to.be.above(minSize).and.below(maxSize)
589
590 {
591 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
592 }
593
594 if (attributes.previewfile) {
595 await testImage(url, attributes.previewfile, videoDetails.previewPath)
596 }
597
598 const torrent = await webtorrentAdd(magnetUri, true)
599 expect(torrent.files).to.be.an('array')
600 expect(torrent.files.length).to.equal(1)
601 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
602 }
603 }
604
605 async function videoUUIDToId (url: string, id: number | string) {
606 if (validator.isUUID('' + id) === false) return id
607
608 const res = await getVideo(url, id)
609 return res.body.id
610 }
611
612 async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
613 const videoAttrs: any = { name: options.videoName }
614 if (options.nsfw) videoAttrs.nsfw = options.nsfw
615
616 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
617
618 return { id: res.body.video.id, uuid: res.body.video.uuid }
619 }
620
621 // ---------------------------------------------------------------------------
622
623 export {
624 getVideoDescription,
625 getVideoCategories,
626 getVideoLicences,
627 videoUUIDToId,
628 getVideoPrivacies,
629 getVideoLanguages,
630 getMyVideos,
631 getAccountVideos,
632 getVideoChannelVideos,
633 getVideo,
634 getVideoWithToken,
635 getVideosList,
636 getVideosListPagination,
637 getVideosListSort,
638 removeVideo,
639 getVideosListWithToken,
640 uploadVideo,
641 getVideosWithFilters,
642 updateVideo,
643 rateVideo,
644 viewVideo,
645 parseTorrentVideo,
646 getLocalVideos,
647 completeVideoCheck,
648 checkVideoFilesWereRemoved,
649 getPlaylistVideos,
650 uploadVideoAndGetId
651 }