Add avatar to prune script
[oweals/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, 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) {
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   for (const directory of directories) {
312     const directoryPath = buildServerDirectory(serverNumber, 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.support !== undefined) {
364     req.field('support', attributes.support)
365   }
366
367   if (attributes.description !== undefined) {
368     req.field('description', attributes.description)
369   }
370   if (attributes.language !== undefined) {
371     req.field('language', attributes.language.toString())
372   }
373   if (attributes.category !== undefined) {
374     req.field('category', attributes.category.toString())
375   }
376   if (attributes.licence !== undefined) {
377     req.field('licence', attributes.licence.toString())
378   }
379
380   const tags = attributes.tags || []
381   for (let i = 0; i < tags.length; i++) {
382     req.field('tags[' + i + ']', attributes.tags[i])
383   }
384
385   if (attributes.thumbnailfile !== undefined) {
386     req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
387   }
388   if (attributes.previewfile !== undefined) {
389     req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
390   }
391
392   if (attributes.scheduleUpdate) {
393     req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
394
395     if (attributes.scheduleUpdate.privacy) {
396       req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
397     }
398   }
399
400   if (attributes.originallyPublishedAt !== undefined) {
401     req.field('originallyPublishedAt', attributes.originallyPublishedAt)
402   }
403
404   return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
405             .expect(specialStatus)
406 }
407
408 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
409   const path = '/api/v1/videos/' + id
410   const body = {}
411
412   if (attributes.name) body['name'] = attributes.name
413   if (attributes.category) body['category'] = attributes.category
414   if (attributes.licence) body['licence'] = attributes.licence
415   if (attributes.language) body['language'] = attributes.language
416   if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
417   if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
418   if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
419   if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
420   if (attributes.description) body['description'] = attributes.description
421   if (attributes.tags) body['tags'] = attributes.tags
422   if (attributes.privacy) body['privacy'] = attributes.privacy
423   if (attributes.channelId) body['channelId'] = attributes.channelId
424   if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
425
426   // Upload request
427   if (attributes.thumbnailfile || attributes.previewfile) {
428     const attaches: any = {}
429     if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
430     if (attributes.previewfile) attaches.previewfile = attributes.previewfile
431
432     return makeUploadRequest({
433       url,
434       method: 'PUT',
435       path,
436       token: accessToken,
437       fields: body,
438       attaches,
439       statusCodeExpected
440     })
441   }
442
443   return makePutBodyRequest({
444     url,
445     path,
446     fields: body,
447     token: accessToken,
448     statusCodeExpected
449   })
450 }
451
452 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
453   const path = '/api/v1/videos/' + id + '/rate'
454
455   return request(url)
456           .put(path)
457           .set('Accept', 'application/json')
458           .set('Authorization', 'Bearer ' + accessToken)
459           .send({ rating })
460           .expect(specialStatus)
461 }
462
463 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
464   return new Promise<any>((res, rej) => {
465     const torrentName = videoUUID + '-' + resolution + '.torrent'
466     const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
467     readFile(torrentPath, (err, data) => {
468       if (err) return rej(err)
469
470       return res(parseTorrent(data))
471     })
472   })
473 }
474
475 async function completeVideoCheck (
476   url: string,
477   video: any,
478   attributes: {
479     name: string
480     category: number
481     licence: number
482     language: string
483     nsfw: boolean
484     commentsEnabled: boolean
485     downloadEnabled: boolean
486     description: string
487     publishedAt?: string
488     support: string
489     originallyPublishedAt?: string,
490     account: {
491       name: string
492       host: string
493     }
494     isLocal: boolean
495     tags: string[]
496     privacy: number
497     likes?: number
498     dislikes?: number
499     duration: number
500     channel: {
501       displayName: string
502       name: string
503       description
504       isLocal: boolean
505     }
506     fixture: string
507     files: {
508       resolution: number
509       size: number
510     }[],
511     thumbnailfile?: string
512     previewfile?: string
513   }
514 ) {
515   if (!attributes.likes) attributes.likes = 0
516   if (!attributes.dislikes) attributes.dislikes = 0
517
518   expect(video.name).to.equal(attributes.name)
519   expect(video.category.id).to.equal(attributes.category)
520   expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
521   expect(video.licence.id).to.equal(attributes.licence)
522   expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
523   expect(video.language.id).to.equal(attributes.language)
524   expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
525   expect(video.privacy.id).to.deep.equal(attributes.privacy)
526   expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
527   expect(video.nsfw).to.equal(attributes.nsfw)
528   expect(video.description).to.equal(attributes.description)
529   expect(video.account.id).to.be.a('number')
530   expect(video.account.host).to.equal(attributes.account.host)
531   expect(video.account.name).to.equal(attributes.account.name)
532   expect(video.channel.displayName).to.equal(attributes.channel.displayName)
533   expect(video.channel.name).to.equal(attributes.channel.name)
534   expect(video.likes).to.equal(attributes.likes)
535   expect(video.dislikes).to.equal(attributes.dislikes)
536   expect(video.isLocal).to.equal(attributes.isLocal)
537   expect(video.duration).to.equal(attributes.duration)
538   expect(dateIsValid(video.createdAt)).to.be.true
539   expect(dateIsValid(video.publishedAt)).to.be.true
540   expect(dateIsValid(video.updatedAt)).to.be.true
541
542   if (attributes.publishedAt) {
543     expect(video.publishedAt).to.equal(attributes.publishedAt)
544   }
545
546   if (attributes.originallyPublishedAt) {
547     expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
548   } else {
549     expect(video.originallyPublishedAt).to.be.null
550   }
551
552   const res = await getVideo(url, video.uuid)
553   const videoDetails: VideoDetails = res.body
554
555   expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
556   expect(videoDetails.tags).to.deep.equal(attributes.tags)
557   expect(videoDetails.account.name).to.equal(attributes.account.name)
558   expect(videoDetails.account.host).to.equal(attributes.account.host)
559   expect(video.channel.displayName).to.equal(attributes.channel.displayName)
560   expect(video.channel.name).to.equal(attributes.channel.name)
561   expect(videoDetails.channel.host).to.equal(attributes.account.host)
562   expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
563   expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
564   expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
565   expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
566   expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
567
568   for (const attributeFile of attributes.files) {
569     const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
570     expect(file).not.to.be.undefined
571
572     let extension = extname(attributes.fixture)
573     // Transcoding enabled: extension will always be .mp4
574     if (attributes.files.length > 1) extension = '.mp4'
575
576     const magnetUri = file.magnetUri
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(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 }