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