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