]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/videos.ts
Use dns cache for requests
[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 { expect } from 'chai'
4 import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
5 import got, { Response as GotResponse } from 'got/dist/source'
6 import * as parseTorrent from 'parse-torrent'
7 import { join } from 'path'
8 import * as request from 'supertest'
9 import validator from 'validator'
10 import { getLowercaseExtension } from '@server/helpers/core-utils'
11 import { buildUUID } from '@server/helpers/uuid'
12 import { HttpStatusCode } from '@shared/core-utils'
13 import { VideosCommonQuery } from '@shared/models'
14 import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
15 import { VideoDetails, VideoPrivacy } from '../../models/videos'
16 import {
17 buildAbsoluteFixturePath,
18 buildServerDirectory,
19 dateIsValid,
20 immutableAssign,
21 testImage,
22 wait,
23 webtorrentAdd
24 } from '../miscs/miscs'
25 import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
26 import { waitJobs } from '../server/jobs'
27 import { ServerInfo } from '../server/servers'
28 import { getMyUserInformation } from '../users/users'
29
30 loadLanguages()
31
32 type VideoAttributes = {
33 name?: string
34 category?: number
35 licence?: number
36 language?: string
37 nsfw?: boolean
38 commentsEnabled?: boolean
39 downloadEnabled?: boolean
40 waitTranscoding?: boolean
41 description?: string
42 originallyPublishedAt?: string
43 tags?: string[]
44 channelId?: number
45 privacy?: VideoPrivacy
46 fixture?: string
47 support?: string
48 thumbnailfile?: string
49 previewfile?: string
50 scheduleUpdate?: {
51 updateAt: string
52 privacy?: VideoPrivacy
53 }
54 }
55
56 function getVideoCategories (url: string) {
57 const path = '/api/v1/videos/categories'
58
59 return makeGetRequest({
60 url,
61 path,
62 statusCodeExpected: HttpStatusCode.OK_200
63 })
64 }
65
66 function getVideoLicences (url: string) {
67 const path = '/api/v1/videos/licences'
68
69 return makeGetRequest({
70 url,
71 path,
72 statusCodeExpected: HttpStatusCode.OK_200
73 })
74 }
75
76 function getVideoLanguages (url: string) {
77 const path = '/api/v1/videos/languages'
78
79 return makeGetRequest({
80 url,
81 path,
82 statusCodeExpected: HttpStatusCode.OK_200
83 })
84 }
85
86 function getVideoPrivacies (url: string) {
87 const path = '/api/v1/videos/privacies'
88
89 return makeGetRequest({
90 url,
91 path,
92 statusCodeExpected: HttpStatusCode.OK_200
93 })
94 }
95
96 function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
97 const path = '/api/v1/videos/' + id
98
99 return request(url)
100 .get(path)
101 .set('Accept', 'application/json')
102 .expect(expectedStatus)
103 }
104
105 async function getVideoIdFromUUID (url: string, uuid: string) {
106 const res = await getVideo(url, uuid)
107
108 return res.body.id
109 }
110
111 function getVideoFileMetadataUrl (url: string) {
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
115 .expect(HttpStatusCode.OK_200)
116 .expect('Content-Type', /json/)
117 }
118
119 function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
120 const path = '/api/v1/videos/' + id + '/views'
121
122 const req = request(url)
123 .post(path)
124 .set('Accept', 'application/json')
125
126 if (xForwardedFor) {
127 req.set('X-Forwarded-For', xForwardedFor)
128 }
129
130 return req.expect(expectedStatus)
131 }
132
133 function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
134 const path = '/api/v1/videos/' + id
135
136 return request(url)
137 .get(path)
138 .set('Authorization', 'Bearer ' + token)
139 .set('Accept', 'application/json')
140 .expect(expectedStatus)
141 }
142
143 function getVideoDescription (url: string, descriptionPath: string) {
144 return request(url)
145 .get(descriptionPath)
146 .set('Accept', 'application/json')
147 .expect(HttpStatusCode.OK_200)
148 .expect('Content-Type', /json/)
149 }
150
151 function getVideosList (url: string) {
152 const path = '/api/v1/videos'
153
154 return request(url)
155 .get(path)
156 .query({ sort: 'name' })
157 .set('Accept', 'application/json')
158 .expect(HttpStatusCode.OK_200)
159 .expect('Content-Type', /json/)
160 }
161
162 function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .set('Authorization', 'Bearer ' + token)
168 .query(immutableAssign(query, { sort: 'name' }))
169 .set('Accept', 'application/json')
170 .expect(HttpStatusCode.OK_200)
171 .expect('Content-Type', /json/)
172 }
173
174 function getLocalVideos (url: string) {
175 const path = '/api/v1/videos'
176
177 return request(url)
178 .get(path)
179 .query({ sort: 'name', filter: 'local' })
180 .set('Accept', 'application/json')
181 .expect(HttpStatusCode.OK_200)
182 .expect('Content-Type', /json/)
183 }
184
185 function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
186 const path = '/api/v1/users/me/videos'
187
188 const req = request(url)
189 .get(path)
190 .query({ start: start })
191 .query({ count: count })
192 .query({ search: search })
193
194 if (sort) req.query({ sort })
195
196 return req.set('Accept', 'application/json')
197 .set('Authorization', 'Bearer ' + accessToken)
198 .expect(HttpStatusCode.OK_200)
199 .expect('Content-Type', /json/)
200 }
201
202 function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
203 const path = '/api/v1/users/me/videos'
204
205 return makeGetRequest({
206 url,
207 path,
208 token: accessToken,
209 query,
210 statusCodeExpected: HttpStatusCode.OK_200
211 })
212 }
213
214 function getAccountVideos (
215 url: string,
216 accessToken: string,
217 accountName: string,
218 start: number,
219 count: number,
220 sort?: string,
221 query: {
222 nsfw?: boolean
223 search?: string
224 } = {}
225 ) {
226 const path = '/api/v1/accounts/' + accountName + '/videos'
227
228 return makeGetRequest({
229 url,
230 path,
231 query: immutableAssign(query, {
232 start,
233 count,
234 sort
235 }),
236 token: accessToken,
237 statusCodeExpected: HttpStatusCode.OK_200
238 })
239 }
240
241 function getVideoChannelVideos (
242 url: string,
243 accessToken: string,
244 videoChannelName: string,
245 start: number,
246 count: number,
247 sort?: string,
248 query: { nsfw?: boolean } = {}
249 ) {
250 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
251
252 return makeGetRequest({
253 url,
254 path,
255 query: immutableAssign(query, {
256 start,
257 count,
258 sort
259 }),
260 token: accessToken,
261 statusCodeExpected: HttpStatusCode.OK_200
262 })
263 }
264
265 function getPlaylistVideos (
266 url: string,
267 accessToken: string,
268 playlistId: number | string,
269 start: number,
270 count: number,
271 query: { nsfw?: boolean } = {}
272 ) {
273 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
274
275 return makeGetRequest({
276 url,
277 path,
278 query: immutableAssign(query, {
279 start,
280 count
281 }),
282 token: accessToken,
283 statusCodeExpected: HttpStatusCode.OK_200
284 })
285 }
286
287 function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
288 const path = '/api/v1/videos'
289
290 const req = request(url)
291 .get(path)
292 .query({ start: start })
293 .query({ count: count })
294
295 if (sort) req.query({ sort })
296 if (skipCount) req.query({ skipCount })
297
298 return req.set('Accept', 'application/json')
299 .expect(HttpStatusCode.OK_200)
300 .expect('Content-Type', /json/)
301 }
302
303 function getVideosListSort (url: string, sort: string) {
304 const path = '/api/v1/videos'
305
306 return request(url)
307 .get(path)
308 .query({ sort: sort })
309 .set('Accept', 'application/json')
310 .expect(HttpStatusCode.OK_200)
311 .expect('Content-Type', /json/)
312 }
313
314 function getVideosWithFilters (url: string, query: VideosCommonQuery) {
315 const path = '/api/v1/videos'
316
317 return request(url)
318 .get(path)
319 .query(query)
320 .set('Accept', 'application/json')
321 .expect(HttpStatusCode.OK_200)
322 .expect('Content-Type', /json/)
323 }
324
325 function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
326 const path = '/api/v1/videos'
327
328 return request(url)
329 .delete(path + '/' + id)
330 .set('Accept', 'application/json')
331 .set('Authorization', 'Bearer ' + token)
332 .expect(expectedStatus)
333 }
334
335 async function removeAllVideos (server: ServerInfo) {
336 const resVideos = await getVideosList(server.url)
337
338 for (const v of resVideos.body.data) {
339 await removeVideo(server.url, server.accessToken, v.id)
340 }
341 }
342
343 async function checkVideoFilesWereRemoved (
344 videoUUID: string,
345 serverNumber: number,
346 directories = [
347 'redundancy',
348 'videos',
349 'thumbnails',
350 'torrents',
351 'previews',
352 'captions',
353 join('playlists', 'hls'),
354 join('redundancy', 'hls')
355 ]
356 ) {
357 for (const directory of directories) {
358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
359
360 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue
362
363 const files = await readdir(directoryPath)
364 for (const file of files) {
365 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID)
366 }
367 }
368 }
369
370 async function uploadVideo (
371 url: string,
372 accessToken: string,
373 videoAttributesArg: VideoAttributes,
374 specialStatus = HttpStatusCode.OK_200,
375 mode: 'legacy' | 'resumable' = 'legacy'
376 ) {
377 let defaultChannelId = '1'
378
379 try {
380 const res = await getMyUserInformation(url, accessToken)
381 defaultChannelId = res.body.videoChannels[0].id
382 } catch (e) { /* empty */ }
383
384 // Override default attributes
385 const attributes = Object.assign({
386 name: 'my super video',
387 category: 5,
388 licence: 4,
389 language: 'zh',
390 channelId: defaultChannelId,
391 nsfw: true,
392 waitTranscoding: false,
393 description: 'my super description',
394 support: 'my super support text',
395 tags: [ 'tag' ],
396 privacy: VideoPrivacy.PUBLIC,
397 commentsEnabled: true,
398 downloadEnabled: true,
399 fixture: 'video_short.webm'
400 }, videoAttributesArg)
401
402 const res = mode === 'legacy'
403 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
404 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
405
406 // Wait torrent generation
407 if (specialStatus === HttpStatusCode.OK_200) {
408 let video: VideoDetails
409 do {
410 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
411 video = resVideo.body
412
413 await wait(50)
414 } while (!video.files[0].torrentUrl)
415 }
416
417 return res
418 }
419
420 function checkUploadVideoParam (
421 url: string,
422 token: string,
423 attributes: Partial<VideoAttributes>,
424 specialStatus = HttpStatusCode.OK_200,
425 mode: 'legacy' | 'resumable' = 'legacy'
426 ) {
427 return mode === 'legacy'
428 ? buildLegacyUpload(url, token, attributes, specialStatus)
429 : buildResumeUpload(url, token, attributes, specialStatus)
430 }
431
432 async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
433 const path = '/api/v1/videos/upload'
434 const req = request(url)
435 .post(path)
436 .set('Accept', 'application/json')
437 .set('Authorization', 'Bearer ' + token)
438
439 buildUploadReq(req, attributes)
440
441 if (attributes.fixture !== undefined) {
442 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
443 }
444
445 return req.expect(specialStatus)
446 }
447
448 async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
449 let size = 0
450 let videoFilePath: string
451 let mimetype = 'video/mp4'
452
453 if (attributes.fixture) {
454 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
455 size = (await stat(videoFilePath)).size
456
457 if (videoFilePath.endsWith('.mkv')) {
458 mimetype = 'video/x-matroska'
459 } else if (videoFilePath.endsWith('.webm')) {
460 mimetype = 'video/webm'
461 }
462 }
463
464 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
465 const initStatus = initializeSessionRes.status
466
467 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
468 const locationHeader = initializeSessionRes.header['location']
469 expect(locationHeader).to.not.be.undefined
470
471 const pathUploadId = locationHeader.split('?')[1]
472
473 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
474 }
475
476 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
477 ? HttpStatusCode.CREATED_201
478 : specialStatus
479
480 expect(initStatus).to.equal(expectedInitStatus)
481
482 return initializeSessionRes
483 }
484
485 async function prepareResumableUpload (options: {
486 url: string
487 token: string
488 attributes: VideoAttributes
489 size: number
490 mimetype: string
491 }) {
492 const { url, token, attributes, size, mimetype } = options
493
494 const path = '/api/v1/videos/upload-resumable'
495
496 const req = request(url)
497 .post(path)
498 .set('Authorization', 'Bearer ' + token)
499 .set('X-Upload-Content-Type', mimetype)
500 .set('X-Upload-Content-Length', size.toString())
501
502 buildUploadReq(req, attributes)
503
504 if (attributes.fixture) {
505 req.field('filename', attributes.fixture)
506 }
507
508 return req
509 }
510
511 function sendResumableChunks (options: {
512 url: string
513 token: string
514 pathUploadId: string
515 videoFilePath: string
516 size: number
517 specialStatus?: HttpStatusCode
518 contentLength?: number
519 contentRangeBuilder?: (start: number, chunk: any) => string
520 }) {
521 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
522
523 const expectedStatus = specialStatus || HttpStatusCode.OK_200
524
525 const path = '/api/v1/videos/upload-resumable'
526 let start = 0
527
528 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
529 return new Promise<GotResponse>((resolve, reject) => {
530 readable.on('data', async function onData (chunk) {
531 readable.pause()
532
533 const headers = {
534 'Authorization': 'Bearer ' + token,
535 'Content-Type': 'application/octet-stream',
536 'Content-Range': contentRangeBuilder
537 ? contentRangeBuilder(start, chunk)
538 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
539 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
540 }
541
542 const res = await got({
543 url,
544 method: 'put',
545 headers,
546 path: path + '?' + pathUploadId,
547 body: chunk,
548 responseType: 'json',
549 throwHttpErrors: false
550 })
551
552 start += chunk.length
553
554 if (res.statusCode === expectedStatus) {
555 return resolve(res)
556 }
557
558 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
559 readable.off('data', onData)
560 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
561 }
562
563 readable.resume()
564 })
565 })
566 }
567
568 function updateVideo (
569 url: string,
570 accessToken: string,
571 id: number | string,
572 attributes: VideoAttributes,
573 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
574 ) {
575 const path = '/api/v1/videos/' + id
576 const body = {}
577
578 if (attributes.name) body['name'] = attributes.name
579 if (attributes.category) body['category'] = attributes.category
580 if (attributes.licence) body['licence'] = attributes.licence
581 if (attributes.language) body['language'] = attributes.language
582 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
583 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
584 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
585 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
586 if (attributes.description) body['description'] = attributes.description
587 if (attributes.tags) body['tags'] = attributes.tags
588 if (attributes.privacy) body['privacy'] = attributes.privacy
589 if (attributes.channelId) body['channelId'] = attributes.channelId
590 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
591
592 // Upload request
593 if (attributes.thumbnailfile || attributes.previewfile) {
594 const attaches: any = {}
595 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
596 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
597
598 return makeUploadRequest({
599 url,
600 method: 'PUT',
601 path,
602 token: accessToken,
603 fields: body,
604 attaches,
605 statusCodeExpected
606 })
607 }
608
609 return makePutBodyRequest({
610 url,
611 path,
612 fields: body,
613 token: accessToken,
614 statusCodeExpected
615 })
616 }
617
618 function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
619 const path = '/api/v1/videos/' + id + '/rate'
620
621 return request(url)
622 .put(path)
623 .set('Accept', 'application/json')
624 .set('Authorization', 'Bearer ' + accessToken)
625 .send({ rating })
626 .expect(specialStatus)
627 }
628
629 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
630 return new Promise<any>((res, rej) => {
631 const torrentName = videoUUID + '-' + resolution + '.torrent'
632 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
633
634 readFile(torrentPath, (err, data) => {
635 if (err) return rej(err)
636
637 return res(parseTorrent(data))
638 })
639 })
640 }
641
642 async function completeVideoCheck (
643 url: string,
644 video: any,
645 attributes: {
646 name: string
647 category: number
648 licence: number
649 language: string
650 nsfw: boolean
651 commentsEnabled: boolean
652 downloadEnabled: boolean
653 description: string
654 publishedAt?: string
655 support: string
656 originallyPublishedAt?: string
657 account: {
658 name: string
659 host: string
660 }
661 isLocal: boolean
662 tags: string[]
663 privacy: number
664 likes?: number
665 dislikes?: number
666 duration: number
667 channel: {
668 displayName: string
669 name: string
670 description
671 isLocal: boolean
672 }
673 fixture: string
674 files: {
675 resolution: number
676 size: number
677 }[]
678 thumbnailfile?: string
679 previewfile?: string
680 }
681 ) {
682 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0
684
685 const host = new URL(url).host
686 const originHost = attributes.account.host
687
688 expect(video.name).to.equal(attributes.name)
689 expect(video.category.id).to.equal(attributes.category)
690 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
691 expect(video.licence.id).to.equal(attributes.licence)
692 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
693 expect(video.language.id).to.equal(attributes.language)
694 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
695 expect(video.privacy.id).to.deep.equal(attributes.privacy)
696 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
697 expect(video.nsfw).to.equal(attributes.nsfw)
698 expect(video.description).to.equal(attributes.description)
699 expect(video.account.id).to.be.a('number')
700 expect(video.account.host).to.equal(attributes.account.host)
701 expect(video.account.name).to.equal(attributes.account.name)
702 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
703 expect(video.channel.name).to.equal(attributes.channel.name)
704 expect(video.likes).to.equal(attributes.likes)
705 expect(video.dislikes).to.equal(attributes.dislikes)
706 expect(video.isLocal).to.equal(attributes.isLocal)
707 expect(video.duration).to.equal(attributes.duration)
708 expect(dateIsValid(video.createdAt)).to.be.true
709 expect(dateIsValid(video.publishedAt)).to.be.true
710 expect(dateIsValid(video.updatedAt)).to.be.true
711
712 if (attributes.publishedAt) {
713 expect(video.publishedAt).to.equal(attributes.publishedAt)
714 }
715
716 if (attributes.originallyPublishedAt) {
717 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
718 } else {
719 expect(video.originallyPublishedAt).to.be.null
720 }
721
722 const res = await getVideo(url, video.uuid)
723 const videoDetails: VideoDetails = res.body
724
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags)
727 expect(videoDetails.account.name).to.equal(attributes.account.name)
728 expect(videoDetails.account.host).to.equal(attributes.account.host)
729 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
730 expect(video.channel.name).to.equal(attributes.channel.name)
731 expect(videoDetails.channel.host).to.equal(attributes.account.host)
732 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
733 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
734 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
735 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
736 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
737
738 for (const attributeFile of attributes.files) {
739 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
740 expect(file).not.to.be.undefined
741
742 let extension = getLowercaseExtension(attributes.fixture)
743 // Transcoding enabled: extension will always be .mp4
744 if (attributes.files.length > 1) extension = '.mp4'
745
746 expect(file.magnetUri).to.have.lengthOf.above(2)
747
748 expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
749 expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
750
751 expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
752 expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`)
753
754 await Promise.all([
755 makeRawRequest(file.torrentUrl, 200),
756 makeRawRequest(file.torrentDownloadUrl, 200),
757 makeRawRequest(file.metadataUrl, 200),
758 // Backward compatibility
759 makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
760 ])
761
762 expect(file.resolution.id).to.equal(attributeFile.resolution)
763 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
764
765 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
766 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
767 expect(
768 file.size,
769 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
770 ).to.be.above(minSize).and.below(maxSize)
771
772 const torrent = await webtorrentAdd(file.magnetUri, true)
773 expect(torrent.files).to.be.an('array')
774 expect(torrent.files.length).to.equal(1)
775 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
776 }
777
778 expect(videoDetails.thumbnailPath).to.exist
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780
781 if (attributes.previewfile) {
782 expect(videoDetails.previewPath).to.exist
783 await testImage(url, attributes.previewfile, videoDetails.previewPath)
784 }
785 }
786
787 async function videoUUIDToId (url: string, id: number | string) {
788 if (validator.isUUID('' + id) === false) return id
789
790 const res = await getVideo(url, id)
791 return res.body.id
792 }
793
794 async function uploadVideoAndGetId (options: {
795 server: ServerInfo
796 videoName: string
797 nsfw?: boolean
798 privacy?: VideoPrivacy
799 token?: string
800 fixture?: string
801 }) {
802 const videoAttrs: any = { name: options.videoName }
803 if (options.nsfw) videoAttrs.nsfw = options.nsfw
804 if (options.privacy) videoAttrs.privacy = options.privacy
805 if (options.fixture) videoAttrs.fixture = options.fixture
806
807 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
808
809 return res.body.video as { id: number, uuid: string, shortUUID: string }
810 }
811
812 async function getLocalIdByUUID (url: string, uuid: string) {
813 const res = await getVideo(url, uuid)
814
815 return res.body.id
816 }
817
818 // serverNumber starts from 1
819 async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
820 const server = servers.find(s => s.serverNumber === serverNumber)
821 const res = await uploadRandomVideo(server, false, additionalParams)
822
823 await waitJobs(servers)
824
825 return res
826 }
827
828 async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
829 const prefixName = additionalParams.prefixName || ''
830 const name = prefixName + buildUUID()
831
832 const data = Object.assign({ name }, additionalParams)
833 const res = await uploadVideo(server.url, server.accessToken, data)
834
835 if (wait) await waitJobs([ server ])
836
837 return { uuid: res.body.video.uuid, name }
838 }
839
840 // ---------------------------------------------------------------------------
841
842 export {
843 getVideoDescription,
844 getVideoCategories,
845 uploadRandomVideo,
846 getVideoLicences,
847 videoUUIDToId,
848 getVideoPrivacies,
849 getVideoLanguages,
850 getMyVideos,
851 getAccountVideos,
852 getVideoChannelVideos,
853 getVideo,
854 getVideoFileMetadataUrl,
855 getVideoWithToken,
856 getVideosList,
857 removeAllVideos,
858 checkUploadVideoParam,
859 getVideosListPagination,
860 getVideosListSort,
861 removeVideo,
862 getVideosListWithToken,
863 uploadVideo,
864 sendResumableChunks,
865 getVideosWithFilters,
866 uploadRandomVideoOnServers,
867 updateVideo,
868 rateVideo,
869 viewVideo,
870 parseTorrentVideo,
871 getLocalVideos,
872 completeVideoCheck,
873 checkVideoFilesWereRemoved,
874 getPlaylistVideos,
875 getMyVideosWithFilter,
876 uploadVideoAndGetId,
877 getLocalIdByUUID,
878 getVideoIdFromUUID,
879 prepareResumableUpload
880 }
881
882 // ---------------------------------------------------------------------------
883
884 function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
885
886 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
887 if (attributes[key] !== undefined) {
888 req.field(key, attributes[key])
889 }
890 }
891
892 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
893 if (attributes[key] !== undefined) {
894 req.field(key, JSON.stringify(attributes[key]))
895 }
896 }
897
898 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
899 if (attributes[key] !== undefined) {
900 req.field(key, attributes[key].toString())
901 }
902 }
903
904 const tags = attributes.tags || []
905 for (let i = 0; i < tags.length; i++) {
906 req.field('tags[' + i + ']', attributes.tags[i])
907 }
908
909 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
910 if (attributes[key] !== undefined) {
911 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
912 }
913 }
914
915 if (attributes.scheduleUpdate) {
916 if (attributes.scheduleUpdate.updateAt) {
917 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
918 }
919
920 if (attributes.scheduleUpdate.privacy) {
921 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
922 }
923 }
924 }