]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - shared/extra-utils/videos/videos.ts
Introduce history command
[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'
af971e06 13import { BooleanBothQuery, 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
af971e06 162function getVideosListWithToken (url: string, token: string, query: { nsfw?: BooleanBothQuery } = {}) {
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 221 query: {
af971e06 222 nsfw?: BooleanBothQuery
37024082
RK
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 247 sort?: string,
af971e06 248 query: { nsfw?: BooleanBothQuery } = {}
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
fe987656 265function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
0e1dc3e7
C
266 const path = '/api/v1/videos'
267
268 const req = request(url)
269 .get(path)
270 .query({ start: start })
271 .query({ count: count })
272
273 if (sort) req.query({ sort })
fe987656 274 if (skipCount) req.query({ skipCount })
0e1dc3e7
C
275
276 return req.set('Accept', 'application/json')
2d53be02 277 .expect(HttpStatusCode.OK_200)
0e1dc3e7
C
278 .expect('Content-Type', /json/)
279}
280
281function getVideosListSort (url: string, sort: string) {
282 const path = '/api/v1/videos'
283
284 return request(url)
285 .get(path)
286 .query({ sort: sort })
287 .set('Accept', 'application/json')
2d53be02 288 .expect(HttpStatusCode.OK_200)
0e1dc3e7
C
289 .expect('Content-Type', /json/)
290}
291
1fd61899 292function getVideosWithFilters (url: string, query: VideosCommonQuery) {
0e1dc3e7
C
293 const path = '/api/v1/videos'
294
295 return request(url)
57c36b27 296 .get(path)
d525fc39 297 .query(query)
f3aaa9a9 298 .set('Accept', 'application/json')
2d53be02 299 .expect(HttpStatusCode.OK_200)
f3aaa9a9 300 .expect('Content-Type', /json/)
0e1dc3e7
C
301}
302
2d53be02 303function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
0883b324 304 const path = '/api/v1/videos'
0e1dc3e7
C
305
306 return request(url)
d525fc39 307 .delete(path + '/' + id)
0e1dc3e7 308 .set('Accept', 'application/json')
d525fc39
C
309 .set('Authorization', 'Bearer ' + token)
310 .expect(expectedStatus)
0e1dc3e7
C
311}
312
68e70a74
C
313async function removeAllVideos (server: ServerInfo) {
314 const resVideos = await getVideosList(server.url)
315
316 for (const v of resVideos.body.data) {
317 await removeVideo(server.url, server.accessToken, v.id)
318 }
319}
320
25378bc8
C
321async function checkVideoFilesWereRemoved (
322 videoUUID: string,
323 serverNumber: number,
09209296
C
324 directories = [
325 'redundancy',
326 'videos',
327 'thumbnails',
328 'torrents',
329 'previews',
330 'captions',
331 join('playlists', 'hls'),
332 join('redundancy', 'hls')
333 ]
25378bc8 334) {
25378bc8 335 for (const directory of directories) {
ca5c612b 336 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
f05a1c30 337
df0b219d
C
338 const directoryExists = await pathExists(directoryPath)
339 if (directoryExists === false) continue
f05a1c30 340
62689b94 341 const files = await readdir(directoryPath)
f05a1c30 342 for (const file of files) {
7448551f 343 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID)
f05a1c30
C
344 }
345 }
346}
347
f6d6e7f8 348async function uploadVideo (
349 url: string,
350 accessToken: string,
351 videoAttributesArg: VideoAttributes,
352 specialStatus = HttpStatusCode.OK_200,
353 mode: 'legacy' | 'resumable' = 'legacy'
354) {
5f04dd2f
C
355 let defaultChannelId = '1'
356
357 try {
358 const res = await getMyUserInformation(url, accessToken)
359 defaultChannelId = res.body.videoChannels[0].id
360 } catch (e) { /* empty */ }
0e1dc3e7 361
ac81d1a0
C
362 // Override default attributes
363 const attributes = Object.assign({
0e1dc3e7
C
364 name: 'my super video',
365 category: 5,
366 licence: 4,
9d3ef9fe 367 language: 'zh',
5f04dd2f 368 channelId: defaultChannelId,
0e1dc3e7 369 nsfw: true,
2186386c 370 waitTranscoding: false,
0e1dc3e7 371 description: 'my super description',
2422c46b 372 support: 'my super support text',
0e1dc3e7 373 tags: [ 'tag' ],
11474c3c 374 privacy: VideoPrivacy.PUBLIC,
47564bbe 375 commentsEnabled: true,
7f2cfe3a 376 downloadEnabled: true,
0e1dc3e7 377 fixture: 'video_short.webm'
ac81d1a0 378 }, videoAttributesArg)
0e1dc3e7 379
f6d6e7f8 380 const res = mode === 'legacy'
381 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
382 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
383
384 // Wait torrent generation
385 if (specialStatus === HttpStatusCode.OK_200) {
386 let video: VideoDetails
387 do {
388 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
389 video = resVideo.body
390
391 await wait(50)
392 } while (!video.files[0].torrentUrl)
393 }
394
395 return res
396}
397
398function checkUploadVideoParam (
399 url: string,
400 token: string,
401 attributes: Partial<VideoAttributes>,
402 specialStatus = HttpStatusCode.OK_200,
403 mode: 'legacy' | 'resumable' = 'legacy'
404) {
405 return mode === 'legacy'
406 ? buildLegacyUpload(url, token, attributes, specialStatus)
407 : buildResumeUpload(url, token, attributes, specialStatus)
408}
409
410async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
411 const path = '/api/v1/videos/upload'
0e1dc3e7
C
412 const req = request(url)
413 .post(path)
414 .set('Accept', 'application/json')
f6d6e7f8 415 .set('Authorization', 'Bearer ' + token)
fc8c024a 416
f6d6e7f8 417 buildUploadReq(req, attributes)
8df87ce7 418
f6d6e7f8 419 if (attributes.fixture !== undefined) {
420 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
2422c46b
C
421 }
422
f6d6e7f8 423 return req.expect(specialStatus)
424}
0e1dc3e7 425
f6d6e7f8 426async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
427 let size = 0
428 let videoFilePath: string
429 let mimetype = 'video/mp4'
2baea0c7 430
f6d6e7f8 431 if (attributes.fixture) {
432 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
433 size = (await stat(videoFilePath)).size
434
435 if (videoFilePath.endsWith('.mkv')) {
436 mimetype = 'video/x-matroska'
437 } else if (videoFilePath.endsWith('.webm')) {
438 mimetype = 'video/webm'
2baea0c7
C
439 }
440 }
441
f6d6e7f8 442 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
443 const initStatus = initializeSessionRes.status
444
445 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
446 const locationHeader = initializeSessionRes.header['location']
447 expect(locationHeader).to.not.be.undefined
448
449 const pathUploadId = locationHeader.split('?')[1]
450
451 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
84929846
AM
452 }
453
f6d6e7f8 454 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
455 ? HttpStatusCode.CREATED_201
456 : specialStatus
d61893f7 457
f6d6e7f8 458 expect(initStatus).to.equal(expectedInitStatus)
d61893f7 459
f6d6e7f8 460 return initializeSessionRes
461}
462
463async function prepareResumableUpload (options: {
464 url: string
465 token: string
466 attributes: VideoAttributes
467 size: number
468 mimetype: string
469}) {
470 const { url, token, attributes, size, mimetype } = options
471
472 const path = '/api/v1/videos/upload-resumable'
473
474 const req = request(url)
475 .post(path)
476 .set('Authorization', 'Bearer ' + token)
477 .set('X-Upload-Content-Type', mimetype)
478 .set('X-Upload-Content-Length', size.toString())
479
480 buildUploadReq(req, attributes)
481
482 if (attributes.fixture) {
483 req.field('filename', attributes.fixture)
d61893f7
C
484 }
485
f6d6e7f8 486 return req
487}
488
489function sendResumableChunks (options: {
490 url: string
491 token: string
492 pathUploadId: string
493 videoFilePath: string
494 size: number
495 specialStatus?: HttpStatusCode
496 contentLength?: number
497 contentRangeBuilder?: (start: number, chunk: any) => string
498}) {
499 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
500
501 const expectedStatus = specialStatus || HttpStatusCode.OK_200
502
503 const path = '/api/v1/videos/upload-resumable'
504 let start = 0
505
506 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
507 return new Promise<GotResponse>((resolve, reject) => {
508 readable.on('data', async function onData (chunk) {
509 readable.pause()
510
511 const headers = {
512 'Authorization': 'Bearer ' + token,
513 'Content-Type': 'application/octet-stream',
514 'Content-Range': contentRangeBuilder
515 ? contentRangeBuilder(start, chunk)
516 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
517 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
518 }
519
520 const res = await got({
521 url,
522 method: 'put',
523 headers,
524 path: path + '?' + pathUploadId,
525 body: chunk,
526 responseType: 'json',
527 throwHttpErrors: false
528 })
529
530 start += chunk.length
531
532 if (res.statusCode === expectedStatus) {
533 return resolve(res)
534 }
535
536 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
537 readable.off('data', onData)
538 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
539 }
540
541 readable.resume()
542 })
543 })
0e1dc3e7
C
544}
545
2d53be02
RK
546function updateVideo (
547 url: string,
548 accessToken: string,
549 id: number | string,
550 attributes: VideoAttributes,
551 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
552) {
0e1dc3e7
C
553 const path = '/api/v1/videos/' + id
554 const body = {}
555
556 if (attributes.name) body['name'] = attributes.name
557 if (attributes.category) body['category'] = attributes.category
558 if (attributes.licence) body['licence'] = attributes.licence
559 if (attributes.language) body['language'] = attributes.language
47564bbe
C
560 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
561 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
7f2cfe3a 562 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
7519127b 563 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
0e1dc3e7
C
564 if (attributes.description) body['description'] = attributes.description
565 if (attributes.tags) body['tags'] = attributes.tags
11474c3c 566 if (attributes.privacy) body['privacy'] = attributes.privacy
0f320037 567 if (attributes.channelId) body['channelId'] = attributes.channelId
2baea0c7 568 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
0e1dc3e7 569
ac81d1a0
C
570 // Upload request
571 if (attributes.thumbnailfile || attributes.previewfile) {
572 const attaches: any = {}
573 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
574 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
575
576 return makeUploadRequest({
577 url,
578 method: 'PUT',
579 path,
580 token: accessToken,
581 fields: body,
582 attaches,
583 statusCodeExpected
584 })
585 }
586
587 return makePutBodyRequest({
588 url,
589 path,
590 fields: body,
591 token: accessToken,
592 statusCodeExpected
593 })
0e1dc3e7
C
594}
595
74d249bc 596function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
0e1dc3e7
C
597 const path = '/api/v1/videos/' + id + '/rate'
598
599 return request(url)
600 .put(path)
601 .set('Accept', 'application/json')
602 .set('Authorization', 'Bearer ' + accessToken)
603 .send({ rating })
604 .expect(specialStatus)
605}
606
14d3270f 607function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
fdbda9e3 608 return new Promise<any>((res, rej) => {
14d3270f 609 const torrentName = videoUUID + '-' + resolution + '.torrent'
ca5c612b
C
610 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
611
fdbda9e3
C
612 readFile(torrentPath, (err, data) => {
613 if (err) return rej(err)
614
615 return res(parseTorrent(data))
616 })
617 })
618}
619
a20399c9
C
620async function completeVideoCheck (
621 url: string,
622 video: any,
623 attributes: {
624 name: string
625 category: number
626 licence: number
9d3ef9fe 627 language: string
a20399c9 628 nsfw: boolean
47564bbe 629 commentsEnabled: boolean
7f2cfe3a 630 downloadEnabled: boolean
a20399c9 631 description: string
53a61317 632 publishedAt?: string
2422c46b 633 support: string
a1587156 634 originallyPublishedAt?: string
b64c950a
C
635 account: {
636 name: string
637 host: string
638 }
f6eebcb3
C
639 isLocal: boolean
640 tags: string[]
641 privacy: number
642 likes?: number
643 dislikes?: number
644 duration: number
a20399c9 645 channel: {
f6eebcb3
C
646 displayName: string
647 name: string
b1f5b93e 648 description
a20399c9
C
649 isLocal: boolean
650 }
f6eebcb3 651 fixture: string
a20399c9
C
652 files: {
653 resolution: number
654 size: number
a1587156 655 }[]
ac81d1a0
C
656 thumbnailfile?: string
657 previewfile?: string
a20399c9
C
658 }
659) {
b1f5b93e
C
660 if (!attributes.likes) attributes.likes = 0
661 if (!attributes.dislikes) attributes.dislikes = 0
662
90a8bd30
C
663 const host = new URL(url).host
664 const originHost = attributes.account.host
665
a20399c9 666 expect(video.name).to.equal(attributes.name)
09700934 667 expect(video.category.id).to.equal(attributes.category)
9d3ef9fe 668 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
09700934 669 expect(video.licence.id).to.equal(attributes.licence)
9d3ef9fe 670 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
09700934 671 expect(video.language.id).to.equal(attributes.language)
9d3ef9fe 672 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
2243730c
C
673 expect(video.privacy.id).to.deep.equal(attributes.privacy)
674 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
a20399c9
C
675 expect(video.nsfw).to.equal(attributes.nsfw)
676 expect(video.description).to.equal(attributes.description)
03e12d7c 677 expect(video.account.id).to.be.a('number')
b64c950a
C
678 expect(video.account.host).to.equal(attributes.account.host)
679 expect(video.account.name).to.equal(attributes.account.name)
f6eebcb3
C
680 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
681 expect(video.channel.name).to.equal(attributes.channel.name)
b1f5b93e
C
682 expect(video.likes).to.equal(attributes.likes)
683 expect(video.dislikes).to.equal(attributes.dislikes)
a20399c9 684 expect(video.isLocal).to.equal(attributes.isLocal)
b1f5b93e 685 expect(video.duration).to.equal(attributes.duration)
a20399c9 686 expect(dateIsValid(video.createdAt)).to.be.true
c49db162 687 expect(dateIsValid(video.publishedAt)).to.be.true
a20399c9
C
688 expect(dateIsValid(video.updatedAt)).to.be.true
689
53a61317 690 if (attributes.publishedAt) {
53a61317
C
691 expect(video.publishedAt).to.equal(attributes.publishedAt)
692 }
693
7519127b
C
694 if (attributes.originallyPublishedAt) {
695 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
696 } else {
697 expect(video.originallyPublishedAt).to.be.null
698 }
699
66b16caf 700 const res = await getVideo(url, video.uuid)
0f320037 701 const videoDetails: VideoDetails = res.body
a20399c9
C
702
703 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
704 expect(videoDetails.tags).to.deep.equal(attributes.tags)
19a3b914
C
705 expect(videoDetails.account.name).to.equal(attributes.account.name)
706 expect(videoDetails.account.host).to.equal(attributes.account.host)
f6eebcb3
C
707 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
708 expect(video.channel.name).to.equal(attributes.channel.name)
0f320037 709 expect(videoDetails.channel.host).to.equal(attributes.account.host)
a20399c9 710 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
0f320037
C
711 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
712 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
713 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
7f2cfe3a 714 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
a20399c9
C
715
716 for (const attributeFile of attributes.files) {
5d00a3d7 717 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
a20399c9
C
718 expect(file).not.to.be.undefined
719
ea54cd04 720 let extension = getLowercaseExtension(attributes.fixture)
48f07b4a
C
721 // Transcoding enabled: extension will always be .mp4
722 if (attributes.files.length > 1) extension = '.mp4'
b1f5b93e 723
a20399c9 724 expect(file.magnetUri).to.have.lengthOf.above(2)
90a8bd30
C
725
726 expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
727 expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
728
729 expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
730 expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`)
731
732 await Promise.all([
733 makeRawRequest(file.torrentUrl, 200),
734 makeRawRequest(file.torrentDownloadUrl, 200),
735 makeRawRequest(file.metadataUrl, 200),
736 // Backward compatibility
737 makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
738 ])
739
09700934
C
740 expect(file.resolution.id).to.equal(attributeFile.resolution)
741 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
b1f5b93e
C
742
743 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
744 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
a1587156
C
745 expect(
746 file.size,
747 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
748 ).to.be.above(minSize).and.below(maxSize)
a20399c9 749
d7a25329 750 const torrent = await webtorrentAdd(file.magnetUri, true)
a20399c9
C
751 expect(torrent.files).to.be.an('array')
752 expect(torrent.files.length).to.equal(1)
753 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
754 }
44d4ee4f 755
08a47c75 756 expect(videoDetails.thumbnailPath).to.exist
44d4ee4f
C
757 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
758
759 if (attributes.previewfile) {
08a47c75 760 expect(videoDetails.previewPath).to.exist
44d4ee4f
C
761 await testImage(url, attributes.previewfile, videoDetails.previewPath)
762 }
a20399c9
C
763}
764
df0b219d
C
765async function videoUUIDToId (url: string, id: number | string) {
766 if (validator.isUUID('' + id) === false) return id
767
768 const res = await getVideo(url, id)
769 return res.body.id
770}
771
b764380a 772async function uploadVideoAndGetId (options: {
a1587156
C
773 server: ServerInfo
774 videoName: string
775 nsfw?: boolean
776 privacy?: VideoPrivacy
b764380a 777 token?: string
a8537c62 778 fixture?: string
b764380a 779}) {
df0b219d
C
780 const videoAttrs: any = { name: options.videoName }
781 if (options.nsfw) videoAttrs.nsfw = options.nsfw
b764380a 782 if (options.privacy) videoAttrs.privacy = options.privacy
a8537c62 783 if (options.fixture) videoAttrs.fixture = options.fixture
df0b219d 784
df0b219d
C
785 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
786
d4a8e7a6 787 return res.body.video as { id: number, uuid: string, shortUUID: string }
df0b219d
C
788}
789
b764380a
C
790async function getLocalIdByUUID (url: string, uuid: string) {
791 const res = await getVideo(url, uuid)
792
793 return res.body.id
794}
795
8eb07b01
C
796// serverNumber starts from 1
797async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
798 const server = servers.find(s => s.serverNumber === serverNumber)
799 const res = await uploadRandomVideo(server, false, additionalParams)
800
801 await waitJobs(servers)
802
803 return res
804}
805
806async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
807 const prefixName = additionalParams.prefixName || ''
d4a8e7a6 808 const name = prefixName + buildUUID()
8eb07b01
C
809
810 const data = Object.assign({ name }, additionalParams)
811 const res = await uploadVideo(server.url, server.accessToken, data)
812
813 if (wait) await waitJobs([ server ])
814
815 return { uuid: res.body.video.uuid, name }
816}
817
0e1dc3e7
C
818// ---------------------------------------------------------------------------
819
820export {
9567011b 821 getVideoDescription,
0e1dc3e7 822 getVideoCategories,
8eb07b01 823 uploadRandomVideo,
0e1dc3e7 824 getVideoLicences,
df0b219d 825 videoUUIDToId,
11474c3c 826 getVideoPrivacies,
0e1dc3e7 827 getVideoLanguages,
11474c3c 828 getMyVideos,
6b738c7a
C
829 getAccountVideos,
830 getVideoChannelVideos,
0e1dc3e7 831 getVideo,
8319d6ae 832 getVideoFileMetadataUrl,
11474c3c 833 getVideoWithToken,
0e1dc3e7 834 getVideosList,
68e70a74 835 removeAllVideos,
f6d6e7f8 836 checkUploadVideoParam,
0e1dc3e7
C
837 getVideosListPagination,
838 getVideosListSort,
839 removeVideo,
0883b324 840 getVideosListWithToken,
0e1dc3e7 841 uploadVideo,
f6d6e7f8 842 sendResumableChunks,
d525fc39 843 getVideosWithFilters,
8eb07b01 844 uploadRandomVideoOnServers,
0e1dc3e7 845 updateVideo,
fdbda9e3 846 rateVideo,
1f3e9fec 847 viewVideo,
a20399c9 848 parseTorrentVideo,
066e94c5 849 getLocalVideos,
f05a1c30 850 completeVideoCheck,
418d092a 851 checkVideoFilesWereRemoved,
1fd61899 852 getMyVideosWithFilter,
b764380a 853 uploadVideoAndGetId,
696d83fd 854 getLocalIdByUUID,
f6d6e7f8 855 getVideoIdFromUUID,
856 prepareResumableUpload
857}
858
859// ---------------------------------------------------------------------------
860
861function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
862
863 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
864 if (attributes[key] !== undefined) {
865 req.field(key, attributes[key])
866 }
867 }
868
869 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
870 if (attributes[key] !== undefined) {
871 req.field(key, JSON.stringify(attributes[key]))
872 }
873 }
874
875 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
876 if (attributes[key] !== undefined) {
877 req.field(key, attributes[key].toString())
878 }
879 }
880
881 const tags = attributes.tags || []
882 for (let i = 0; i < tags.length; i++) {
883 req.field('tags[' + i + ']', attributes.tags[i])
884 }
885
886 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
887 if (attributes[key] !== undefined) {
888 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
889 }
890 }
891
892 if (attributes.scheduleUpdate) {
893 if (attributes.scheduleUpdate.updateAt) {
894 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
895 }
896
897 if (attributes.scheduleUpdate.privacy) {
898 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
899 }
900 }
0e1dc3e7 901}