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