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