]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/videos.ts
Fix redundancy with HLS only files
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / videos.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3 import { HttpStatusCode } from '@shared/core-utils'
4 import { expect } from 'chai'
5 import { pathExists, readdir, readFile } from 'fs-extra'
6 import * as parseTorrent from 'parse-torrent'
7 import { extname, join } from 'path'
8 import * as request from 'supertest'
9 import { v4 as uuidv4 } from 'uuid'
10 import validator from 'validator'
11 import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
12 import { VideoDetails, VideoPrivacy } from '../../models/videos'
13 import { buildAbsoluteFixturePath, buildServerDirectory, dateIsValid, immutableAssign, testImage, webtorrentAdd } from '../miscs/miscs'
14 import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
15 import { waitJobs } from '../server/jobs'
16 import { ServerInfo } from '../server/servers'
17 import { getMyUserInformation } from '../users/users'
18
19 loadLanguages()
20
21 type VideoAttributes = {
22 name?: string
23 category?: number
24 licence?: number
25 language?: string
26 nsfw?: boolean
27 commentsEnabled?: boolean
28 downloadEnabled?: boolean
29 waitTranscoding?: boolean
30 description?: string
31 originallyPublishedAt?: string
32 tags?: string[]
33 channelId?: number
34 privacy?: VideoPrivacy
35 fixture?: string
36 thumbnailfile?: string
37 previewfile?: string
38 scheduleUpdate?: {
39 updateAt: string
40 privacy?: VideoPrivacy
41 }
42 }
43
44 function getVideoCategories (url: string) {
45 const path = '/api/v1/videos/categories'
46
47 return makeGetRequest({
48 url,
49 path,
50 statusCodeExpected: HttpStatusCode.OK_200
51 })
52 }
53
54 function getVideoLicences (url: string) {
55 const path = '/api/v1/videos/licences'
56
57 return makeGetRequest({
58 url,
59 path,
60 statusCodeExpected: HttpStatusCode.OK_200
61 })
62 }
63
64 function getVideoLanguages (url: string) {
65 const path = '/api/v1/videos/languages'
66
67 return makeGetRequest({
68 url,
69 path,
70 statusCodeExpected: HttpStatusCode.OK_200
71 })
72 }
73
74 function getVideoPrivacies (url: string) {
75 const path = '/api/v1/videos/privacies'
76
77 return makeGetRequest({
78 url,
79 path,
80 statusCodeExpected: HttpStatusCode.OK_200
81 })
82 }
83
84 function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
85 const path = '/api/v1/videos/' + id
86
87 return request(url)
88 .get(path)
89 .set('Accept', 'application/json')
90 .expect(expectedStatus)
91 }
92
93 async function getVideoIdFromUUID (url: string, uuid: string) {
94 const res = await getVideo(url, uuid)
95
96 return res.body.id
97 }
98
99 function getVideoFileMetadataUrl (url: string) {
100 return request(url)
101 .get('/')
102 .set('Accept', 'application/json')
103 .expect(HttpStatusCode.OK_200)
104 .expect('Content-Type', /json/)
105 }
106
107 function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
108 const path = '/api/v1/videos/' + id + '/views'
109
110 const req = request(url)
111 .post(path)
112 .set('Accept', 'application/json')
113
114 if (xForwardedFor) {
115 req.set('X-Forwarded-For', xForwardedFor)
116 }
117
118 return req.expect(expectedStatus)
119 }
120
121 function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
122 const path = '/api/v1/videos/' + id
123
124 return request(url)
125 .get(path)
126 .set('Authorization', 'Bearer ' + token)
127 .set('Accept', 'application/json')
128 .expect(expectedStatus)
129 }
130
131 function getVideoDescription (url: string, descriptionPath: string) {
132 return request(url)
133 .get(descriptionPath)
134 .set('Accept', 'application/json')
135 .expect(HttpStatusCode.OK_200)
136 .expect('Content-Type', /json/)
137 }
138
139 function getVideosList (url: string) {
140 const path = '/api/v1/videos'
141
142 return request(url)
143 .get(path)
144 .query({ sort: 'name' })
145 .set('Accept', 'application/json')
146 .expect(HttpStatusCode.OK_200)
147 .expect('Content-Type', /json/)
148 }
149
150 function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
151 const path = '/api/v1/videos'
152
153 return request(url)
154 .get(path)
155 .set('Authorization', 'Bearer ' + token)
156 .query(immutableAssign(query, { sort: 'name' }))
157 .set('Accept', 'application/json')
158 .expect(HttpStatusCode.OK_200)
159 .expect('Content-Type', /json/)
160 }
161
162 function getLocalVideos (url: string) {
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .query({ sort: 'name', filter: 'local' })
168 .set('Accept', 'application/json')
169 .expect(HttpStatusCode.OK_200)
170 .expect('Content-Type', /json/)
171 }
172
173 function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
174 const path = '/api/v1/users/me/videos'
175
176 const req = request(url)
177 .get(path)
178 .query({ start: start })
179 .query({ count: count })
180 .query({ search: search })
181
182 if (sort) req.query({ sort })
183
184 return req.set('Accept', 'application/json')
185 .set('Authorization', 'Bearer ' + accessToken)
186 .expect(HttpStatusCode.OK_200)
187 .expect('Content-Type', /json/)
188 }
189
190 function getAccountVideos (
191 url: string,
192 accessToken: string,
193 accountName: string,
194 start: number,
195 count: number,
196 sort?: string,
197 query: {
198 nsfw?: boolean
199 search?: string
200 } = {}
201 ) {
202 const path = '/api/v1/accounts/' + accountName + '/videos'
203
204 return makeGetRequest({
205 url,
206 path,
207 query: immutableAssign(query, {
208 start,
209 count,
210 sort
211 }),
212 token: accessToken,
213 statusCodeExpected: HttpStatusCode.OK_200
214 })
215 }
216
217 function getVideoChannelVideos (
218 url: string,
219 accessToken: string,
220 videoChannelName: string,
221 start: number,
222 count: number,
223 sort?: string,
224 query: { nsfw?: boolean } = {}
225 ) {
226 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
227
228 return makeGetRequest({
229 url,
230 path,
231 query: immutableAssign(query, {
232 start,
233 count,
234 sort
235 }),
236 token: accessToken,
237 statusCodeExpected: HttpStatusCode.OK_200
238 })
239 }
240
241 function getPlaylistVideos (
242 url: string,
243 accessToken: string,
244 playlistId: number | string,
245 start: number,
246 count: number,
247 query: { nsfw?: boolean } = {}
248 ) {
249 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
250
251 return makeGetRequest({
252 url,
253 path,
254 query: immutableAssign(query, {
255 start,
256 count
257 }),
258 token: accessToken,
259 statusCodeExpected: HttpStatusCode.OK_200
260 })
261 }
262
263 function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
264 const path = '/api/v1/videos'
265
266 const req = request(url)
267 .get(path)
268 .query({ start: start })
269 .query({ count: count })
270
271 if (sort) req.query({ sort })
272 if (skipCount) req.query({ skipCount })
273
274 return req.set('Accept', 'application/json')
275 .expect(HttpStatusCode.OK_200)
276 .expect('Content-Type', /json/)
277 }
278
279 function getVideosListSort (url: string, sort: string) {
280 const path = '/api/v1/videos'
281
282 return request(url)
283 .get(path)
284 .query({ sort: sort })
285 .set('Accept', 'application/json')
286 .expect(HttpStatusCode.OK_200)
287 .expect('Content-Type', /json/)
288 }
289
290 function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
291 const path = '/api/v1/videos'
292
293 return request(url)
294 .get(path)
295 .query(query)
296 .set('Accept', 'application/json')
297 .expect(HttpStatusCode.OK_200)
298 .expect('Content-Type', /json/)
299 }
300
301 function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
302 const path = '/api/v1/videos'
303
304 return request(url)
305 .delete(path + '/' + id)
306 .set('Accept', 'application/json')
307 .set('Authorization', 'Bearer ' + token)
308 .expect(expectedStatus)
309 }
310
311 async function removeAllVideos (server: ServerInfo) {
312 const resVideos = await getVideosList(server.url)
313
314 for (const v of resVideos.body.data) {
315 await removeVideo(server.url, server.accessToken, v.id)
316 }
317 }
318
319 async function checkVideoFilesWereRemoved (
320 videoUUID: string,
321 serverNumber: number,
322 directories = [
323 'redundancy',
324 'videos',
325 'thumbnails',
326 'torrents',
327 'previews',
328 'captions',
329 join('playlists', 'hls'),
330 join('redundancy', 'hls')
331 ]
332 ) {
333 for (const directory of directories) {
334 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
335
336 const directoryExists = await pathExists(directoryPath)
337 if (directoryExists === false) continue
338
339 const files = await readdir(directoryPath)
340 for (const file of files) {
341 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID)
342 }
343 }
344 }
345
346 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
347 const path = '/api/v1/videos/upload'
348 let defaultChannelId = '1'
349
350 try {
351 const res = await getMyUserInformation(url, accessToken)
352 defaultChannelId = res.body.videoChannels[0].id
353 } catch (e) { /* empty */ }
354
355 // Override default attributes
356 const attributes = Object.assign({
357 name: 'my super video',
358 category: 5,
359 licence: 4,
360 language: 'zh',
361 channelId: defaultChannelId,
362 nsfw: true,
363 waitTranscoding: false,
364 description: 'my super description',
365 support: 'my super support text',
366 tags: [ 'tag' ],
367 privacy: VideoPrivacy.PUBLIC,
368 commentsEnabled: true,
369 downloadEnabled: true,
370 fixture: 'video_short.webm'
371 }, videoAttributesArg)
372
373 const req = request(url)
374 .post(path)
375 .set('Accept', 'application/json')
376 .set('Authorization', 'Bearer ' + accessToken)
377 .field('name', attributes.name)
378 .field('nsfw', JSON.stringify(attributes.nsfw))
379 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
380 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
381 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
382 .field('privacy', attributes.privacy.toString())
383 .field('channelId', attributes.channelId)
384
385 if (attributes.support !== undefined) {
386 req.field('support', attributes.support)
387 }
388
389 if (attributes.description !== undefined) {
390 req.field('description', attributes.description)
391 }
392 if (attributes.language !== undefined) {
393 req.field('language', attributes.language.toString())
394 }
395 if (attributes.category !== undefined) {
396 req.field('category', attributes.category.toString())
397 }
398 if (attributes.licence !== undefined) {
399 req.field('licence', attributes.licence.toString())
400 }
401
402 const tags = attributes.tags || []
403 for (let i = 0; i < tags.length; i++) {
404 req.field('tags[' + i + ']', attributes.tags[i])
405 }
406
407 if (attributes.thumbnailfile !== undefined) {
408 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
409 }
410 if (attributes.previewfile !== undefined) {
411 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
412 }
413
414 if (attributes.scheduleUpdate) {
415 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
416
417 if (attributes.scheduleUpdate.privacy) {
418 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
419 }
420 }
421
422 if (attributes.originallyPublishedAt !== undefined) {
423 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
424 }
425
426 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
427 .expect(specialStatus)
428 }
429
430 function updateVideo (
431 url: string,
432 accessToken: string,
433 id: number | string,
434 attributes: VideoAttributes,
435 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
436 ) {
437 const path = '/api/v1/videos/' + id
438 const body = {}
439
440 if (attributes.name) body['name'] = attributes.name
441 if (attributes.category) body['category'] = attributes.category
442 if (attributes.licence) body['licence'] = attributes.licence
443 if (attributes.language) body['language'] = attributes.language
444 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
445 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
446 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
447 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
448 if (attributes.description) body['description'] = attributes.description
449 if (attributes.tags) body['tags'] = attributes.tags
450 if (attributes.privacy) body['privacy'] = attributes.privacy
451 if (attributes.channelId) body['channelId'] = attributes.channelId
452 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
453
454 // Upload request
455 if (attributes.thumbnailfile || attributes.previewfile) {
456 const attaches: any = {}
457 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
458 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
459
460 return makeUploadRequest({
461 url,
462 method: 'PUT',
463 path,
464 token: accessToken,
465 fields: body,
466 attaches,
467 statusCodeExpected
468 })
469 }
470
471 return makePutBodyRequest({
472 url,
473 path,
474 fields: body,
475 token: accessToken,
476 statusCodeExpected
477 })
478 }
479
480 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
481 const path = '/api/v1/videos/' + id + '/rate'
482
483 return request(url)
484 .put(path)
485 .set('Accept', 'application/json')
486 .set('Authorization', 'Bearer ' + accessToken)
487 .send({ rating })
488 .expect(specialStatus)
489 }
490
491 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
492 return new Promise<any>((res, rej) => {
493 const torrentName = videoUUID + '-' + resolution + '.torrent'
494 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
495
496 readFile(torrentPath, (err, data) => {
497 if (err) return rej(err)
498
499 return res(parseTorrent(data))
500 })
501 })
502 }
503
504 async function completeVideoCheck (
505 url: string,
506 video: any,
507 attributes: {
508 name: string
509 category: number
510 licence: number
511 language: string
512 nsfw: boolean
513 commentsEnabled: boolean
514 downloadEnabled: boolean
515 description: string
516 publishedAt?: string
517 support: string
518 originallyPublishedAt?: string
519 account: {
520 name: string
521 host: string
522 }
523 isLocal: boolean
524 tags: string[]
525 privacy: number
526 likes?: number
527 dislikes?: number
528 duration: number
529 channel: {
530 displayName: string
531 name: string
532 description
533 isLocal: boolean
534 }
535 fixture: string
536 files: {
537 resolution: number
538 size: number
539 }[]
540 thumbnailfile?: string
541 previewfile?: string
542 }
543 ) {
544 if (!attributes.likes) attributes.likes = 0
545 if (!attributes.dislikes) attributes.dislikes = 0
546
547 expect(video.name).to.equal(attributes.name)
548 expect(video.category.id).to.equal(attributes.category)
549 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
550 expect(video.licence.id).to.equal(attributes.licence)
551 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
552 expect(video.language.id).to.equal(attributes.language)
553 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
554 expect(video.privacy.id).to.deep.equal(attributes.privacy)
555 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
556 expect(video.nsfw).to.equal(attributes.nsfw)
557 expect(video.description).to.equal(attributes.description)
558 expect(video.account.id).to.be.a('number')
559 expect(video.account.host).to.equal(attributes.account.host)
560 expect(video.account.name).to.equal(attributes.account.name)
561 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
562 expect(video.channel.name).to.equal(attributes.channel.name)
563 expect(video.likes).to.equal(attributes.likes)
564 expect(video.dislikes).to.equal(attributes.dislikes)
565 expect(video.isLocal).to.equal(attributes.isLocal)
566 expect(video.duration).to.equal(attributes.duration)
567 expect(dateIsValid(video.createdAt)).to.be.true
568 expect(dateIsValid(video.publishedAt)).to.be.true
569 expect(dateIsValid(video.updatedAt)).to.be.true
570
571 if (attributes.publishedAt) {
572 expect(video.publishedAt).to.equal(attributes.publishedAt)
573 }
574
575 if (attributes.originallyPublishedAt) {
576 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
577 } else {
578 expect(video.originallyPublishedAt).to.be.null
579 }
580
581 const res = await getVideo(url, video.uuid)
582 const videoDetails: VideoDetails = res.body
583
584 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
585 expect(videoDetails.tags).to.deep.equal(attributes.tags)
586 expect(videoDetails.account.name).to.equal(attributes.account.name)
587 expect(videoDetails.account.host).to.equal(attributes.account.host)
588 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
589 expect(video.channel.name).to.equal(attributes.channel.name)
590 expect(videoDetails.channel.host).to.equal(attributes.account.host)
591 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
592 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
593 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
594 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
595 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
596
597 for (const attributeFile of attributes.files) {
598 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
599 expect(file).not.to.be.undefined
600
601 let extension = extname(attributes.fixture)
602 // Transcoding enabled: extension will always be .mp4
603 if (attributes.files.length > 1) extension = '.mp4'
604
605 expect(file.magnetUri).to.have.lengthOf.above(2)
606 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
607 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
608 expect(file.resolution.id).to.equal(attributeFile.resolution)
609 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
610
611 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
612 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
613 expect(
614 file.size,
615 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
616 ).to.be.above(minSize).and.below(maxSize)
617
618 const torrent = await webtorrentAdd(file.magnetUri, true)
619 expect(torrent.files).to.be.an('array')
620 expect(torrent.files.length).to.equal(1)
621 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
622 }
623
624 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
625
626 if (attributes.previewfile) {
627 await testImage(url, attributes.previewfile, videoDetails.previewPath)
628 }
629 }
630
631 async function videoUUIDToId (url: string, id: number | string) {
632 if (validator.isUUID('' + id) === false) return id
633
634 const res = await getVideo(url, id)
635 return res.body.id
636 }
637
638 async function uploadVideoAndGetId (options: {
639 server: ServerInfo
640 videoName: string
641 nsfw?: boolean
642 privacy?: VideoPrivacy
643 token?: string
644 fixture?: string
645 }) {
646 const videoAttrs: any = { name: options.videoName }
647 if (options.nsfw) videoAttrs.nsfw = options.nsfw
648 if (options.privacy) videoAttrs.privacy = options.privacy
649 if (options.fixture) videoAttrs.fixture = options.fixture
650
651 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
652
653 return { id: res.body.video.id, uuid: res.body.video.uuid }
654 }
655
656 async function getLocalIdByUUID (url: string, uuid: string) {
657 const res = await getVideo(url, uuid)
658
659 return res.body.id
660 }
661
662 // serverNumber starts from 1
663 async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
664 const server = servers.find(s => s.serverNumber === serverNumber)
665 const res = await uploadRandomVideo(server, false, additionalParams)
666
667 await waitJobs(servers)
668
669 return res
670 }
671
672 async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
673 const prefixName = additionalParams.prefixName || ''
674 const name = prefixName + uuidv4()
675
676 const data = Object.assign({ name }, additionalParams)
677 const res = await uploadVideo(server.url, server.accessToken, data)
678
679 if (wait) await waitJobs([ server ])
680
681 return { uuid: res.body.video.uuid, name }
682 }
683
684 // ---------------------------------------------------------------------------
685
686 export {
687 getVideoDescription,
688 getVideoCategories,
689 uploadRandomVideo,
690 getVideoLicences,
691 videoUUIDToId,
692 getVideoPrivacies,
693 getVideoLanguages,
694 getMyVideos,
695 getAccountVideos,
696 getVideoChannelVideos,
697 getVideo,
698 getVideoFileMetadataUrl,
699 getVideoWithToken,
700 getVideosList,
701 removeAllVideos,
702 getVideosListPagination,
703 getVideosListSort,
704 removeVideo,
705 getVideosListWithToken,
706 uploadVideo,
707 getVideosWithFilters,
708 uploadRandomVideoOnServers,
709 updateVideo,
710 rateVideo,
711 viewVideo,
712 parseTorrentVideo,
713 getLocalVideos,
714 completeVideoCheck,
715 checkVideoFilesWereRemoved,
716 getPlaylistVideos,
717 uploadVideoAndGetId,
718 getLocalIdByUUID,
719 getVideoIdFromUUID
720 }