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