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