]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/utils/videos/videos.ts
Fixing #626 with ffmpeg's low default audio bitrate
[github/Chocobozzz/PeerTube.git] / server / tests / utils / videos / videos.ts
1 /* tslint:disable:no-unused-expression */
2
3 import { expect } from 'chai'
4 import { existsSync, readFile } from 'fs'
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, immutableAssign,
11 makeGetRequest,
12 makePutBodyRequest,
13 makeUploadRequest,
14 root,
15 ServerInfo,
16 testImage
17 } from '../'
18 import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
19 import { readdirPromise } from '../../../helpers/core-utils'
20 import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers'
21 import { dateIsValid, webtorrentAdd } from '../index'
22
23 type VideoAttributes = {
24 name?: string
25 category?: number
26 licence?: number
27 language?: string
28 nsfw?: boolean
29 commentsEnabled?: boolean
30 waitTranscoding?: boolean
31 description?: 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: 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: 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: 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: 200
81 })
82 }
83
84 function getVideo (url: string, id: number | string, expectedStatus = 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 function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
94 const path = '/api/v1/videos/' + id + '/views'
95
96 const req = request(url)
97 .post(path)
98 .set('Accept', 'application/json')
99
100 if (xForwardedFor) {
101 req.set('X-Forwarded-For', xForwardedFor)
102 }
103
104 return req.expect(expectedStatus)
105 }
106
107 function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
108 const path = '/api/v1/videos/' + id
109
110 return request(url)
111 .get(path)
112 .set('Authorization', 'Bearer ' + token)
113 .set('Accept', 'application/json')
114 .expect(expectedStatus)
115 }
116
117 function getVideoDescription (url: string, descriptionPath: string) {
118 return request(url)
119 .get(descriptionPath)
120 .set('Accept', 'application/json')
121 .expect(200)
122 .expect('Content-Type', /json/)
123 }
124
125 function getVideosList (url: string) {
126 const path = '/api/v1/videos'
127
128 return request(url)
129 .get(path)
130 .query({ sort: 'name' })
131 .set('Accept', 'application/json')
132 .expect(200)
133 .expect('Content-Type', /json/)
134 }
135
136 function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
137 const path = '/api/v1/videos'
138
139 return request(url)
140 .get(path)
141 .set('Authorization', 'Bearer ' + token)
142 .query(immutableAssign(query, { sort: 'name' }))
143 .set('Accept', 'application/json')
144 .expect(200)
145 .expect('Content-Type', /json/)
146 }
147
148 function getLocalVideos (url: string) {
149 const path = '/api/v1/videos'
150
151 return request(url)
152 .get(path)
153 .query({ sort: 'name', filter: 'local' })
154 .set('Accept', 'application/json')
155 .expect(200)
156 .expect('Content-Type', /json/)
157 }
158
159 function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
160 const path = '/api/v1/users/me/videos'
161
162 const req = request(url)
163 .get(path)
164 .query({ start: start })
165 .query({ count: count })
166
167 if (sort) req.query({ sort })
168
169 return req.set('Accept', 'application/json')
170 .set('Authorization', 'Bearer ' + accessToken)
171 .expect(200)
172 .expect('Content-Type', /json/)
173 }
174
175 function getAccountVideos (
176 url: string,
177 accessToken: string,
178 accountName: string,
179 start: number,
180 count: number,
181 sort?: string,
182 query: { nsfw?: boolean } = {}
183 ) {
184 const path = '/api/v1/accounts/' + accountName + '/videos'
185
186 return makeGetRequest({
187 url,
188 path,
189 query: immutableAssign(query, {
190 start,
191 count,
192 sort
193 }),
194 token: accessToken,
195 statusCodeExpected: 200
196 })
197 }
198
199 function getVideoChannelVideos (
200 url: string,
201 accessToken: string,
202 videoChannelId: number | string,
203 start: number,
204 count: number,
205 sort?: string,
206 query: { nsfw?: boolean } = {}
207 ) {
208 const path = '/api/v1/video-channels/' + videoChannelId + '/videos'
209
210 return makeGetRequest({
211 url,
212 path,
213 query: immutableAssign(query, {
214 start,
215 count,
216 sort
217 }),
218 token: accessToken,
219 statusCodeExpected: 200
220 })
221 }
222
223 function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
224 const path = '/api/v1/videos'
225
226 const req = request(url)
227 .get(path)
228 .query({ start: start })
229 .query({ count: count })
230
231 if (sort) req.query({ sort })
232
233 return req.set('Accept', 'application/json')
234 .expect(200)
235 .expect('Content-Type', /json/)
236 }
237
238 function getVideosListSort (url: string, sort: string) {
239 const path = '/api/v1/videos'
240
241 return request(url)
242 .get(path)
243 .query({ sort: sort })
244 .set('Accept', 'application/json')
245 .expect(200)
246 .expect('Content-Type', /json/)
247 }
248
249 function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
250 const path = '/api/v1/videos'
251
252 return request(url)
253 .get(path)
254 .query(query)
255 .set('Accept', 'application/json')
256 .expect(200)
257 .expect('Content-Type', /json/)
258 }
259
260 function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
261 const path = '/api/v1/videos'
262
263 return request(url)
264 .delete(path + '/' + id)
265 .set('Accept', 'application/json')
266 .set('Authorization', 'Bearer ' + token)
267 .expect(expectedStatus)
268 }
269
270 async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) {
271 const testDirectory = 'test' + serverNumber
272
273 for (const directory of [ 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ]) {
274 const directoryPath = join(root(), testDirectory, directory)
275
276 const directoryExists = existsSync(directoryPath)
277 expect(directoryExists).to.be.true
278
279 const files = await readdirPromise(directoryPath)
280 for (const file of files) {
281 expect(file).to.not.contain(videoUUID)
282 }
283 }
284 }
285
286 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
287 const path = '/api/v1/videos/upload'
288 let defaultChannelId = '1'
289
290 try {
291 const res = await getMyUserInformation(url, accessToken)
292 defaultChannelId = res.body.videoChannels[0].id
293 } catch (e) { /* empty */ }
294
295 // Override default attributes
296 const attributes = Object.assign({
297 name: 'my super video',
298 category: 5,
299 licence: 4,
300 language: 'zh',
301 channelId: defaultChannelId,
302 nsfw: true,
303 waitTranscoding: false,
304 description: 'my super description',
305 support: 'my super support text',
306 tags: [ 'tag' ],
307 privacy: VideoPrivacy.PUBLIC,
308 commentsEnabled: true,
309 fixture: 'video_short.webm'
310 }, videoAttributesArg)
311
312 const req = request(url)
313 .post(path)
314 .set('Accept', 'application/json')
315 .set('Authorization', 'Bearer ' + accessToken)
316 .field('name', attributes.name)
317 .field('nsfw', JSON.stringify(attributes.nsfw))
318 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
319 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
320 .field('privacy', attributes.privacy.toString())
321 .field('channelId', attributes.channelId)
322
323 if (attributes.description !== undefined) {
324 req.field('description', attributes.description)
325 }
326 if (attributes.language !== undefined) {
327 req.field('language', attributes.language.toString())
328 }
329 if (attributes.category !== undefined) {
330 req.field('category', attributes.category.toString())
331 }
332 if (attributes.licence !== undefined) {
333 req.field('licence', attributes.licence.toString())
334 }
335
336 for (let i = 0; i < attributes.tags.length; i++) {
337 req.field('tags[' + i + ']', attributes.tags[i])
338 }
339
340 if (attributes.thumbnailfile !== undefined) {
341 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
342 }
343 if (attributes.previewfile !== undefined) {
344 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
345 }
346
347 if (attributes.scheduleUpdate) {
348 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
349
350 if (attributes.scheduleUpdate.privacy) {
351 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
352 }
353 }
354
355 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
356 .expect(specialStatus)
357 }
358
359 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
360 const path = '/api/v1/videos/' + id
361 const body = {}
362
363 if (attributes.name) body['name'] = attributes.name
364 if (attributes.category) body['category'] = attributes.category
365 if (attributes.licence) body['licence'] = attributes.licence
366 if (attributes.language) body['language'] = attributes.language
367 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
368 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
369 if (attributes.description) body['description'] = attributes.description
370 if (attributes.tags) body['tags'] = attributes.tags
371 if (attributes.privacy) body['privacy'] = attributes.privacy
372 if (attributes.channelId) body['channelId'] = attributes.channelId
373 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
374
375 // Upload request
376 if (attributes.thumbnailfile || attributes.previewfile) {
377 const attaches: any = {}
378 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
379 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
380
381 return makeUploadRequest({
382 url,
383 method: 'PUT',
384 path,
385 token: accessToken,
386 fields: body,
387 attaches,
388 statusCodeExpected
389 })
390 }
391
392 return makePutBodyRequest({
393 url,
394 path,
395 fields: body,
396 token: accessToken,
397 statusCodeExpected
398 })
399 }
400
401 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
402 const path = '/api/v1/videos/' + id + '/rate'
403
404 return request(url)
405 .put(path)
406 .set('Accept', 'application/json')
407 .set('Authorization', 'Bearer ' + accessToken)
408 .send({ rating })
409 .expect(specialStatus)
410 }
411
412 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
413 return new Promise<any>((res, rej) => {
414 const torrentName = videoUUID + '-' + resolution + '.torrent'
415 const torrentPath = join(__dirname, '..', '..', '..', '..', 'test' + server.serverNumber, 'torrents', torrentName)
416 readFile(torrentPath, (err, data) => {
417 if (err) return rej(err)
418
419 return res(parseTorrent(data))
420 })
421 })
422 }
423
424 async function completeVideoCheck (
425 url: string,
426 video: any,
427 attributes: {
428 name: string
429 category: number
430 licence: number
431 language: string
432 nsfw: boolean
433 commentsEnabled: boolean
434 description: string
435 publishedAt?: string
436 support: string
437 account: {
438 name: string
439 host: string
440 }
441 isLocal: boolean,
442 tags: string[],
443 privacy: number,
444 likes?: number,
445 dislikes?: number,
446 duration: number,
447 channel: {
448 name: string,
449 description
450 isLocal: boolean
451 }
452 fixture: string,
453 files: {
454 resolution: number
455 size: number
456 }[],
457 thumbnailfile?: string
458 previewfile?: string
459 }
460 ) {
461 if (!attributes.likes) attributes.likes = 0
462 if (!attributes.dislikes) attributes.dislikes = 0
463
464 expect(video.name).to.equal(attributes.name)
465 expect(video.category.id).to.equal(attributes.category)
466 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
467 expect(video.licence.id).to.equal(attributes.licence)
468 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
469 expect(video.language.id).to.equal(attributes.language)
470 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
471 expect(video.privacy.id).to.deep.equal(attributes.privacy)
472 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
473 expect(video.nsfw).to.equal(attributes.nsfw)
474 expect(video.description).to.equal(attributes.description)
475 expect(video.account.id).to.be.a('number')
476 expect(video.account.uuid).to.be.a('string')
477 expect(video.account.host).to.equal(attributes.account.host)
478 expect(video.account.name).to.equal(attributes.account.name)
479 expect(video.channel.displayName).to.equal(attributes.channel.name)
480 expect(video.channel.name).to.have.lengthOf(36)
481 expect(video.likes).to.equal(attributes.likes)
482 expect(video.dislikes).to.equal(attributes.dislikes)
483 expect(video.isLocal).to.equal(attributes.isLocal)
484 expect(video.duration).to.equal(attributes.duration)
485 expect(dateIsValid(video.createdAt)).to.be.true
486 expect(dateIsValid(video.publishedAt)).to.be.true
487 expect(dateIsValid(video.updatedAt)).to.be.true
488
489 if (attributes.publishedAt) {
490 expect(video.publishedAt).to.equal(attributes.publishedAt)
491 }
492
493 const res = await getVideo(url, video.uuid)
494 const videoDetails: VideoDetails = res.body
495
496 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
497 expect(videoDetails.tags).to.deep.equal(attributes.tags)
498 expect(videoDetails.account.name).to.equal(attributes.account.name)
499 expect(videoDetails.account.host).to.equal(attributes.account.host)
500 expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
501 expect(videoDetails.channel.name).to.have.lengthOf(36)
502 expect(videoDetails.channel.host).to.equal(attributes.account.host)
503 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
504 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
505 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
506 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
507
508 for (const attributeFile of attributes.files) {
509 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
510 expect(file).not.to.be.undefined
511
512 let extension = extname(attributes.fixture)
513 // Transcoding enabled on server 2, extension will always be .mp4
514 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
515
516 const magnetUri = file.magnetUri
517 expect(file.magnetUri).to.have.lengthOf.above(2)
518 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
519 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
520 expect(file.resolution.id).to.equal(attributeFile.resolution)
521 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
522
523 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
524 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
525 expect(file.size,
526 'File size for resolution ' + file.resolution.label + ' outside confidence interval.')
527 .to.be.above(minSize).and.below(maxSize)
528
529 {
530 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
531 }
532
533 if (attributes.previewfile) {
534 await testImage(url, attributes.previewfile, videoDetails.previewPath)
535 }
536
537 const torrent = await webtorrentAdd(magnetUri, true)
538 expect(torrent.files).to.be.an('array')
539 expect(torrent.files.length).to.equal(1)
540 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
541 }
542 }
543
544 // ---------------------------------------------------------------------------
545
546 export {
547 getVideoDescription,
548 getVideoCategories,
549 getVideoLicences,
550 getVideoPrivacies,
551 getVideoLanguages,
552 getMyVideos,
553 getAccountVideos,
554 getVideoChannelVideos,
555 getVideo,
556 getVideoWithToken,
557 getVideosList,
558 getVideosListPagination,
559 getVideosListSort,
560 removeVideo,
561 getVideosListWithToken,
562 uploadVideo,
563 getVideosWithFilters,
564 updateVideo,
565 rateVideo,
566 viewVideo,
567 parseTorrentVideo,
568 getLocalVideos,
569 completeVideoCheck,
570 checkVideoFilesWereRemoved
571 }