]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - shared/extra-utils/videos/videos.ts
Hide schedule privacy if private does not exist
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / videos.ts
CommitLineData
a1587156 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
a20399c9
C
2
3import { expect } from 'chai'
f6d6e7f8 4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source'
fdbda9e3 6import * as parseTorrent from 'parse-torrent'
ea54cd04 7import { join } from 'path'
c5d31dba 8import * as request from 'supertest'
8eb07b01 9import validator from 'validator'
ea54cd04 10import { getLowercaseExtension } from '@server/helpers/core-utils'
d4a8e7a6 11import { buildUUID } from '@server/helpers/uuid'
d61893f7 12import { HttpStatusCode } from '@shared/core-utils'
1fd61899 13import { VideosCommonQuery } from '@shared/models'
8eb07b01
C
14import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
15import { VideoDetails, VideoPrivacy } from '../../models/videos'
d61893f7
C
16import {
17 buildAbsoluteFixturePath,
18 buildServerDirectory,
19 dateIsValid,
20 immutableAssign,
21 testImage,
22 wait,
23 webtorrentAdd
24} from '../miscs/miscs'
90a8bd30 25import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
8eb07b01
C
26import { waitJobs } from '../server/jobs'
27import { ServerInfo } from '../server/servers'
28import { getMyUserInformation } from '../users/users'
0e1dc3e7 29
8f0bc73d
C
30loadLanguages()
31
0e1dc3e7
C
32type VideoAttributes = {
33 name?: string
34 category?: number
35 licence?: number
9d3ef9fe 36 language?: string
0e1dc3e7 37 nsfw?: boolean
47564bbe 38 commentsEnabled?: boolean
7f2cfe3a 39 downloadEnabled?: boolean
2186386c 40 waitTranscoding?: boolean
0e1dc3e7 41 description?: string
7519127b 42 originallyPublishedAt?: string
0e1dc3e7 43 tags?: string[]
5f04dd2f 44 channelId?: number
11474c3c 45 privacy?: VideoPrivacy
0e1dc3e7 46 fixture?: string
f6d6e7f8 47 support?: string
ac81d1a0
C
48 thumbnailfile?: string
49 previewfile?: string
2baea0c7
C
50 scheduleUpdate?: {
51 updateAt: string
52 privacy?: VideoPrivacy
53 }
0e1dc3e7
C
54}
55
56function getVideoCategories (url: string) {
57 const path = '/api/v1/videos/categories'
58
eec63bbc
C
59 return makeGetRequest({
60 url,
59651eee 61 path,
2d53be02 62 statusCodeExpected: HttpStatusCode.OK_200
eec63bbc 63 })
0e1dc3e7
C
64}
65
66function getVideoLicences (url: string) {
67 const path = '/api/v1/videos/licences'
68
eec63bbc
C
69 return makeGetRequest({
70 url,
59651eee 71 path,
2d53be02 72 statusCodeExpected: HttpStatusCode.OK_200
eec63bbc 73 })
0e1dc3e7
C
74}
75
76function getVideoLanguages (url: string) {
77 const path = '/api/v1/videos/languages'
78
eec63bbc
C
79 return makeGetRequest({
80 url,
59651eee 81 path,
2d53be02 82 statusCodeExpected: HttpStatusCode.OK_200
eec63bbc 83 })
0e1dc3e7
C
84}
85
11474c3c
C
86function getVideoPrivacies (url: string) {
87 const path = '/api/v1/videos/privacies'
88
eec63bbc
C
89 return makeGetRequest({
90 url,
59651eee 91 path,
2d53be02 92 statusCodeExpected: HttpStatusCode.OK_200
eec63bbc 93 })
11474c3c
C
94}
95
2d53be02 96function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
0e1dc3e7
C
97 const path = '/api/v1/videos/' + id
98
99 return request(url)
100 .get(path)
101 .set('Accept', 'application/json')
11474c3c
C
102 .expect(expectedStatus)
103}
104
696d83fd
C
105async function getVideoIdFromUUID (url: string, uuid: string) {
106 const res = await getVideo(url, uuid)
107
108 return res.body.id
109}
110
8319d6ae
RK
111function getVideoFileMetadataUrl (url: string) {
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
2d53be02 115 .expect(HttpStatusCode.OK_200)
8319d6ae
RK
116 .expect('Content-Type', /json/)
117}
118
2d53be02 119function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
1f3e9fec
C
120 const path = '/api/v1/videos/' + id + '/views'
121
490b595a 122 const req = request(url)
1f3e9fec
C
123 .post(path)
124 .set('Accept', 'application/json')
490b595a
C
125
126 if (xForwardedFor) {
127 req.set('X-Forwarded-For', xForwardedFor)
128 }
129
130 return req.expect(expectedStatus)
1f3e9fec
C
131}
132
2d53be02 133function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
11474c3c
C
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)
0e1dc3e7
C
141}
142
9567011b
C
143function getVideoDescription (url: string, descriptionPath: string) {
144 return request(url)
145 .get(descriptionPath)
146 .set('Accept', 'application/json')
2d53be02 147 .expect(HttpStatusCode.OK_200)
9567011b
C
148 .expect('Content-Type', /json/)
149}
150
0e1dc3e7
C
151function 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')
2d53be02 158 .expect(HttpStatusCode.OK_200)
0e1dc3e7
C
159 .expect('Content-Type', /json/)
160}
161
d525fc39 162function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
0883b324
C
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .set('Authorization', 'Bearer ' + token)
d525fc39 168 .query(immutableAssign(query, { sort: 'name' }))
0883b324 169 .set('Accept', 'application/json')
f2eb23cd 170 .expect(HttpStatusCode.OK_200)
0883b324
C
171 .expect('Content-Type', /json/)
172}
173
066e94c5
C
174function 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')
f2eb23cd 181 .expect(HttpStatusCode.OK_200)
066e94c5
C
182 .expect('Content-Type', /json/)
183}
184
cca1e13b 185function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
11474c3c
C
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 })
cca1e13b 192 .query({ search: search })
11474c3c
C
193
194 if (sort) req.query({ sort })
195
196 return req.set('Accept', 'application/json')
197 .set('Authorization', 'Bearer ' + accessToken)
2d53be02 198 .expect(HttpStatusCode.OK_200)
11474c3c
C
199 .expect('Content-Type', /json/)
200}
201
1fd61899
C
202function 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
d525fc39
C
214function getAccountVideos (
215 url: string,
216 accessToken: string,
217 accountName: string,
218 start: number,
219 count: number,
220 sort?: string,
37024082
RK
221 query: {
222 nsfw?: boolean
223 search?: string
224 } = {}
d525fc39 225) {
ad9e39fb 226 const path = '/api/v1/accounts/' + accountName + '/videos'
6b738c7a
C
227
228 return makeGetRequest({
229 url,
230 path,
d525fc39 231 query: immutableAssign(query, {
6b738c7a
C
232 start,
233 count,
234 sort
d525fc39 235 }),
6b738c7a 236 token: accessToken,
2d53be02 237 statusCodeExpected: HttpStatusCode.OK_200
6b738c7a
C
238 })
239}
240
241function getVideoChannelVideos (
242 url: string,
243 accessToken: string,
8a19bee1 244 videoChannelName: string,
6b738c7a
C
245 start: number,
246 count: number,
d525fc39
C
247 sort?: string,
248 query: { nsfw?: boolean } = {}
6b738c7a 249) {
8a19bee1 250 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
6b738c7a
C
251
252 return makeGetRequest({
253 url,
254 path,
d525fc39 255 query: immutableAssign(query, {
6b738c7a
C
256 start,
257 count,
258 sort
d525fc39 259 }),
6b738c7a 260 token: accessToken,
2d53be02 261 statusCodeExpected: HttpStatusCode.OK_200
6b738c7a
C
262 })
263}
264
418d092a
C
265function 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,
2d53be02 283 statusCodeExpected: HttpStatusCode.OK_200
418d092a
C
284 })
285}
286
fe987656 287function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
0e1dc3e7
C
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 })
fe987656 296 if (skipCount) req.query({ skipCount })
0e1dc3e7
C
297
298 return req.set('Accept', 'application/json')
2d53be02 299 .expect(HttpStatusCode.OK_200)
0e1dc3e7
C
300 .expect('Content-Type', /json/)
301}
302
303function 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')
2d53be02 310 .expect(HttpStatusCode.OK_200)
0e1dc3e7
C
311 .expect('Content-Type', /json/)
312}
313
1fd61899 314function getVideosWithFilters (url: string, query: VideosCommonQuery) {
0e1dc3e7
C
315 const path = '/api/v1/videos'
316
317 return request(url)
57c36b27 318 .get(path)
d525fc39 319 .query(query)
f3aaa9a9 320 .set('Accept', 'application/json')
2d53be02 321 .expect(HttpStatusCode.OK_200)
f3aaa9a9 322 .expect('Content-Type', /json/)
0e1dc3e7
C
323}
324
2d53be02 325function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
0883b324 326 const path = '/api/v1/videos'
0e1dc3e7
C
327
328 return request(url)
d525fc39 329 .delete(path + '/' + id)
0e1dc3e7 330 .set('Accept', 'application/json')
d525fc39
C
331 .set('Authorization', 'Bearer ' + token)
332 .expect(expectedStatus)
0e1dc3e7
C
333}
334
68e70a74
C
335async 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
25378bc8
C
343async function checkVideoFilesWereRemoved (
344 videoUUID: string,
345 serverNumber: number,
09209296
C
346 directories = [
347 'redundancy',
348 'videos',
349 'thumbnails',
350 'torrents',
351 'previews',
352 'captions',
353 join('playlists', 'hls'),
354 join('redundancy', 'hls')
355 ]
25378bc8 356) {
25378bc8 357 for (const directory of directories) {
ca5c612b 358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
f05a1c30 359
df0b219d
C
360 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue
f05a1c30 362
62689b94 363 const files = await readdir(directoryPath)
f05a1c30 364 for (const file of files) {
7448551f 365 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID)
f05a1c30
C
366 }
367 }
368}
369
f6d6e7f8 370async function uploadVideo (
371 url: string,
372 accessToken: string,
373 videoAttributesArg: VideoAttributes,
374 specialStatus = HttpStatusCode.OK_200,
375 mode: 'legacy' | 'resumable' = 'legacy'
376) {
5f04dd2f
C
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 */ }
0e1dc3e7 383
ac81d1a0
C
384 // Override default attributes
385 const attributes = Object.assign({
0e1dc3e7
C
386 name: 'my super video',
387 category: 5,
388 licence: 4,
9d3ef9fe 389 language: 'zh',
5f04dd2f 390 channelId: defaultChannelId,
0e1dc3e7 391 nsfw: true,
2186386c 392 waitTranscoding: false,
0e1dc3e7 393 description: 'my super description',
2422c46b 394 support: 'my super support text',
0e1dc3e7 395 tags: [ 'tag' ],
11474c3c 396 privacy: VideoPrivacy.PUBLIC,
47564bbe 397 commentsEnabled: true,
7f2cfe3a 398 downloadEnabled: true,
0e1dc3e7 399 fixture: 'video_short.webm'
ac81d1a0 400 }, videoAttributesArg)
0e1dc3e7 401
f6d6e7f8 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
420function 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
432async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
433 const path = '/api/v1/videos/upload'
0e1dc3e7
C
434 const req = request(url)
435 .post(path)
436 .set('Accept', 'application/json')
f6d6e7f8 437 .set('Authorization', 'Bearer ' + token)
fc8c024a 438
f6d6e7f8 439 buildUploadReq(req, attributes)
8df87ce7 440
f6d6e7f8 441 if (attributes.fixture !== undefined) {
442 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
2422c46b
C
443 }
444
f6d6e7f8 445 return req.expect(specialStatus)
446}
0e1dc3e7 447
f6d6e7f8 448async 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'
2baea0c7 452
f6d6e7f8 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'
2baea0c7
C
461 }
462 }
463
f6d6e7f8 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 })
84929846
AM
474 }
475
f6d6e7f8 476 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
477 ? HttpStatusCode.CREATED_201
478 : specialStatus
d61893f7 479
f6d6e7f8 480 expect(initStatus).to.equal(expectedInitStatus)
d61893f7 481
f6d6e7f8 482 return initializeSessionRes
483}
484
485async 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)
d61893f7
C
506 }
507
f6d6e7f8 508 return req
509}
510
511function 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 })
0e1dc3e7
C
566}
567
2d53be02
RK
568function updateVideo (
569 url: string,
570 accessToken: string,
571 id: number | string,
572 attributes: VideoAttributes,
573 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
574) {
0e1dc3e7
C
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
47564bbe
C
582 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
583 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
7f2cfe3a 584 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
7519127b 585 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
0e1dc3e7
C
586 if (attributes.description) body['description'] = attributes.description
587 if (attributes.tags) body['tags'] = attributes.tags
11474c3c 588 if (attributes.privacy) body['privacy'] = attributes.privacy
0f320037 589 if (attributes.channelId) body['channelId'] = attributes.channelId
2baea0c7 590 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
0e1dc3e7 591
ac81d1a0
C
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 })
0e1dc3e7
C
616}
617
74d249bc 618function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
0e1dc3e7
C
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
14d3270f 629function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
fdbda9e3 630 return new Promise<any>((res, rej) => {
14d3270f 631 const torrentName = videoUUID + '-' + resolution + '.torrent'
ca5c612b
C
632 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
633
fdbda9e3
C
634 readFile(torrentPath, (err, data) => {
635 if (err) return rej(err)
636
637 return res(parseTorrent(data))
638 })
639 })
640}
641
a20399c9
C
642async function completeVideoCheck (
643 url: string,
644 video: any,
645 attributes: {
646 name: string
647 category: number
648 licence: number
9d3ef9fe 649 language: string
a20399c9 650 nsfw: boolean
47564bbe 651 commentsEnabled: boolean
7f2cfe3a 652 downloadEnabled: boolean
a20399c9 653 description: string
53a61317 654 publishedAt?: string
2422c46b 655 support: string
a1587156 656 originallyPublishedAt?: string
b64c950a
C
657 account: {
658 name: string
659 host: string
660 }
f6eebcb3
C
661 isLocal: boolean
662 tags: string[]
663 privacy: number
664 likes?: number
665 dislikes?: number
666 duration: number
a20399c9 667 channel: {
f6eebcb3
C
668 displayName: string
669 name: string
b1f5b93e 670 description
a20399c9
C
671 isLocal: boolean
672 }
f6eebcb3 673 fixture: string
a20399c9
C
674 files: {
675 resolution: number
676 size: number
a1587156 677 }[]
ac81d1a0
C
678 thumbnailfile?: string
679 previewfile?: string
a20399c9
C
680 }
681) {
b1f5b93e
C
682 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0
684
90a8bd30
C
685 const host = new URL(url).host
686 const originHost = attributes.account.host
687
a20399c9 688 expect(video.name).to.equal(attributes.name)
09700934 689 expect(video.category.id).to.equal(attributes.category)
9d3ef9fe 690 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
09700934 691 expect(video.licence.id).to.equal(attributes.licence)
9d3ef9fe 692 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
09700934 693 expect(video.language.id).to.equal(attributes.language)
9d3ef9fe 694 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
2243730c
C
695 expect(video.privacy.id).to.deep.equal(attributes.privacy)
696 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
a20399c9
C
697 expect(video.nsfw).to.equal(attributes.nsfw)
698 expect(video.description).to.equal(attributes.description)
03e12d7c 699 expect(video.account.id).to.be.a('number')
b64c950a
C
700 expect(video.account.host).to.equal(attributes.account.host)
701 expect(video.account.name).to.equal(attributes.account.name)
f6eebcb3
C
702 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
703 expect(video.channel.name).to.equal(attributes.channel.name)
b1f5b93e
C
704 expect(video.likes).to.equal(attributes.likes)
705 expect(video.dislikes).to.equal(attributes.dislikes)
a20399c9 706 expect(video.isLocal).to.equal(attributes.isLocal)
b1f5b93e 707 expect(video.duration).to.equal(attributes.duration)
a20399c9 708 expect(dateIsValid(video.createdAt)).to.be.true
c49db162 709 expect(dateIsValid(video.publishedAt)).to.be.true
a20399c9
C
710 expect(dateIsValid(video.updatedAt)).to.be.true
711
53a61317 712 if (attributes.publishedAt) {
53a61317
C
713 expect(video.publishedAt).to.equal(attributes.publishedAt)
714 }
715
7519127b
C
716 if (attributes.originallyPublishedAt) {
717 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
718 } else {
719 expect(video.originallyPublishedAt).to.be.null
720 }
721
66b16caf 722 const res = await getVideo(url, video.uuid)
0f320037 723 const videoDetails: VideoDetails = res.body
a20399c9
C
724
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags)
19a3b914
C
727 expect(videoDetails.account.name).to.equal(attributes.account.name)
728 expect(videoDetails.account.host).to.equal(attributes.account.host)
f6eebcb3
C
729 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
730 expect(video.channel.name).to.equal(attributes.channel.name)
0f320037 731 expect(videoDetails.channel.host).to.equal(attributes.account.host)
a20399c9 732 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
0f320037
C
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)
7f2cfe3a 736 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
a20399c9
C
737
738 for (const attributeFile of attributes.files) {
5d00a3d7 739 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
a20399c9
C
740 expect(file).not.to.be.undefined
741
ea54cd04 742 let extension = getLowercaseExtension(attributes.fixture)
48f07b4a
C
743 // Transcoding enabled: extension will always be .mp4
744 if (attributes.files.length > 1) extension = '.mp4'
b1f5b93e 745
a20399c9 746 expect(file.magnetUri).to.have.lengthOf.above(2)
90a8bd30
C
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
09700934
C
762 expect(file.resolution.id).to.equal(attributeFile.resolution)
763 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
b1f5b93e
C
764
765 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
766 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
a1587156
C
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)
a20399c9 771
d7a25329 772 const torrent = await webtorrentAdd(file.magnetUri, true)
a20399c9
C
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 }
44d4ee4f 777
08a47c75 778 expect(videoDetails.thumbnailPath).to.exist
44d4ee4f
C
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780
781 if (attributes.previewfile) {
08a47c75 782 expect(videoDetails.previewPath).to.exist
44d4ee4f
C
783 await testImage(url, attributes.previewfile, videoDetails.previewPath)
784 }
a20399c9
C
785}
786
df0b219d
C
787async 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
b764380a 794async function uploadVideoAndGetId (options: {
a1587156
C
795 server: ServerInfo
796 videoName: string
797 nsfw?: boolean
798 privacy?: VideoPrivacy
b764380a 799 token?: string
a8537c62 800 fixture?: string
b764380a 801}) {
df0b219d
C
802 const videoAttrs: any = { name: options.videoName }
803 if (options.nsfw) videoAttrs.nsfw = options.nsfw
b764380a 804 if (options.privacy) videoAttrs.privacy = options.privacy
a8537c62 805 if (options.fixture) videoAttrs.fixture = options.fixture
df0b219d 806
df0b219d
C
807 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
808
d4a8e7a6 809 return res.body.video as { id: number, uuid: string, shortUUID: string }
df0b219d
C
810}
811
b764380a
C
812async function getLocalIdByUUID (url: string, uuid: string) {
813 const res = await getVideo(url, uuid)
814
815 return res.body.id
816}
817
8eb07b01
C
818// serverNumber starts from 1
819async 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
828async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
829 const prefixName = additionalParams.prefixName || ''
d4a8e7a6 830 const name = prefixName + buildUUID()
8eb07b01
C
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
0e1dc3e7
C
840// ---------------------------------------------------------------------------
841
842export {
9567011b 843 getVideoDescription,
0e1dc3e7 844 getVideoCategories,
8eb07b01 845 uploadRandomVideo,
0e1dc3e7 846 getVideoLicences,
df0b219d 847 videoUUIDToId,
11474c3c 848 getVideoPrivacies,
0e1dc3e7 849 getVideoLanguages,
11474c3c 850 getMyVideos,
6b738c7a
C
851 getAccountVideos,
852 getVideoChannelVideos,
0e1dc3e7 853 getVideo,
8319d6ae 854 getVideoFileMetadataUrl,
11474c3c 855 getVideoWithToken,
0e1dc3e7 856 getVideosList,
68e70a74 857 removeAllVideos,
f6d6e7f8 858 checkUploadVideoParam,
0e1dc3e7
C
859 getVideosListPagination,
860 getVideosListSort,
861 removeVideo,
0883b324 862 getVideosListWithToken,
0e1dc3e7 863 uploadVideo,
f6d6e7f8 864 sendResumableChunks,
d525fc39 865 getVideosWithFilters,
8eb07b01 866 uploadRandomVideoOnServers,
0e1dc3e7 867 updateVideo,
fdbda9e3 868 rateVideo,
1f3e9fec 869 viewVideo,
a20399c9 870 parseTorrentVideo,
066e94c5 871 getLocalVideos,
f05a1c30 872 completeVideoCheck,
418d092a 873 checkVideoFilesWereRemoved,
df0b219d 874 getPlaylistVideos,
1fd61899 875 getMyVideosWithFilter,
b764380a 876 uploadVideoAndGetId,
696d83fd 877 getLocalIdByUUID,
f6d6e7f8 878 getVideoIdFromUUID,
879 prepareResumableUpload
880}
881
882// ---------------------------------------------------------------------------
883
884function 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 }
0e1dc3e7 924}