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