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