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