diff options
author | Chocobozzz <me@florianbigard.com> | 2021-05-27 16:12:41 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-05-27 16:12:41 +0200 |
commit | 8f608a4cb22ab232cfab20665050764b38bac9c7 (patch) | |
tree | 6a6785aae79bf5939ad7b7a50a1bd8031268d2b4 /shared/extra-utils/videos/videos.ts | |
parent | 030ccfce59a8cb8f2fee6ea8dd363ba635c5c5c2 (diff) | |
parent | c215e627b575d2c4085ccb222f4ca8d0237b7552 (diff) | |
download | PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.tar.gz PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.tar.zst PeerTube-8f608a4cb22ab232cfab20665050764b38bac9c7.zip |
Merge branch 'develop' into shorter-URLs-channels-accounts
Diffstat (limited to 'shared/extra-utils/videos/videos.ts')
-rw-r--r-- | shared/extra-utils/videos/videos.ts | 258 |
1 files changed, 204 insertions, 54 deletions
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index a0143b0ef..e88256ac0 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { pathExists, readdir, readFile } from 'fs-extra' | 4 | import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' |
5 | import got, { Response as GotResponse } from 'got/dist/source' | ||
5 | import * as parseTorrent from 'parse-torrent' | 6 | import * as parseTorrent from 'parse-torrent' |
6 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
7 | import * as request from 'supertest' | 8 | import * as request from 'supertest' |
@@ -42,6 +43,7 @@ type VideoAttributes = { | |||
42 | channelId?: number | 43 | channelId?: number |
43 | privacy?: VideoPrivacy | 44 | privacy?: VideoPrivacy |
44 | fixture?: string | 45 | fixture?: string |
46 | support?: string | ||
45 | thumbnailfile?: string | 47 | thumbnailfile?: string |
46 | previewfile?: string | 48 | previewfile?: string |
47 | scheduleUpdate?: { | 49 | scheduleUpdate?: { |
@@ -364,8 +366,13 @@ async function checkVideoFilesWereRemoved ( | |||
364 | } | 366 | } |
365 | } | 367 | } |
366 | 368 | ||
367 | async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { | 369 | async function uploadVideo ( |
368 | const path = '/api/v1/videos/upload' | 370 | url: string, |
371 | accessToken: string, | ||
372 | videoAttributesArg: VideoAttributes, | ||
373 | specialStatus = HttpStatusCode.OK_200, | ||
374 | mode: 'legacy' | 'resumable' = 'legacy' | ||
375 | ) { | ||
369 | let defaultChannelId = '1' | 376 | let defaultChannelId = '1' |
370 | 377 | ||
371 | try { | 378 | try { |
@@ -391,74 +398,170 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg | |||
391 | fixture: 'video_short.webm' | 398 | fixture: 'video_short.webm' |
392 | }, videoAttributesArg) | 399 | }, videoAttributesArg) |
393 | 400 | ||
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 | |||
419 | function 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 | |||
431 | async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { | ||
432 | const path = '/api/v1/videos/upload' | ||
394 | const req = request(url) | 433 | const req = request(url) |
395 | .post(path) | 434 | .post(path) |
396 | .set('Accept', 'application/json') | 435 | .set('Accept', 'application/json') |
397 | .set('Authorization', 'Bearer ' + accessToken) | 436 | .set('Authorization', 'Bearer ' + token) |
398 | .field('name', attributes.name) | ||
399 | .field('nsfw', JSON.stringify(attributes.nsfw)) | ||
400 | .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) | ||
401 | .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled)) | ||
402 | .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding)) | ||
403 | .field('privacy', attributes.privacy.toString()) | ||
404 | .field('channelId', attributes.channelId) | ||
405 | |||
406 | if (attributes.support !== undefined) { | ||
407 | req.field('support', attributes.support) | ||
408 | } | ||
409 | 437 | ||
410 | if (attributes.description !== undefined) { | 438 | buildUploadReq(req, attributes) |
411 | req.field('description', attributes.description) | ||
412 | } | ||
413 | if (attributes.language !== undefined) { | ||
414 | req.field('language', attributes.language.toString()) | ||
415 | } | ||
416 | if (attributes.category !== undefined) { | ||
417 | req.field('category', attributes.category.toString()) | ||
418 | } | ||
419 | if (attributes.licence !== undefined) { | ||
420 | req.field('licence', attributes.licence.toString()) | ||
421 | } | ||
422 | 439 | ||
423 | const tags = attributes.tags || [] | 440 | if (attributes.fixture !== undefined) { |
424 | for (let i = 0; i < tags.length; i++) { | 441 | req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) |
425 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
426 | } | 442 | } |
427 | 443 | ||
428 | if (attributes.thumbnailfile !== undefined) { | 444 | return req.expect(specialStatus) |
429 | req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile)) | 445 | } |
430 | } | ||
431 | if (attributes.previewfile !== undefined) { | ||
432 | req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile)) | ||
433 | } | ||
434 | 446 | ||
435 | if (attributes.scheduleUpdate) { | 447 | async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) { |
436 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | 448 | let size = 0 |
449 | let videoFilePath: string | ||
450 | let mimetype = 'video/mp4' | ||
437 | 451 | ||
438 | if (attributes.scheduleUpdate.privacy) { | 452 | if (attributes.fixture) { |
439 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | 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' | ||
440 | } | 460 | } |
441 | } | 461 | } |
442 | 462 | ||
443 | if (attributes.originallyPublishedAt !== undefined) { | 463 | const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype }) |
444 | req.field('originallyPublishedAt', attributes.originallyPublishedAt) | 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 }) | ||
445 | } | 473 | } |
446 | 474 | ||
447 | const res = await req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture)) | 475 | const expectedInitStatus = specialStatus === HttpStatusCode.OK_200 |
448 | .expect(specialStatus) | 476 | ? HttpStatusCode.CREATED_201 |
477 | : specialStatus | ||
449 | 478 | ||
450 | // Wait torrent generation | 479 | expect(initStatus).to.equal(expectedInitStatus) |
451 | if (specialStatus === HttpStatusCode.OK_200) { | ||
452 | let video: VideoDetails | ||
453 | do { | ||
454 | const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid) | ||
455 | video = resVideo.body | ||
456 | 480 | ||
457 | await wait(50) | 481 | return initializeSessionRes |
458 | } while (!video.files[0].torrentUrl) | 482 | } |
483 | |||
484 | async 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) | ||
459 | } | 505 | } |
460 | 506 | ||
461 | return res | 507 | return req |
508 | } | ||
509 | |||
510 | function 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 | }) | ||
462 | } | 565 | } |
463 | 566 | ||
464 | function updateVideo ( | 567 | function updateVideo ( |
@@ -749,11 +852,13 @@ export { | |||
749 | getVideoWithToken, | 852 | getVideoWithToken, |
750 | getVideosList, | 853 | getVideosList, |
751 | removeAllVideos, | 854 | removeAllVideos, |
855 | checkUploadVideoParam, | ||
752 | getVideosListPagination, | 856 | getVideosListPagination, |
753 | getVideosListSort, | 857 | getVideosListSort, |
754 | removeVideo, | 858 | removeVideo, |
755 | getVideosListWithToken, | 859 | getVideosListWithToken, |
756 | uploadVideo, | 860 | uploadVideo, |
861 | sendResumableChunks, | ||
757 | getVideosWithFilters, | 862 | getVideosWithFilters, |
758 | uploadRandomVideoOnServers, | 863 | uploadRandomVideoOnServers, |
759 | updateVideo, | 864 | updateVideo, |
@@ -767,5 +872,50 @@ export { | |||
767 | getMyVideosWithFilter, | 872 | getMyVideosWithFilter, |
768 | uploadVideoAndGetId, | 873 | uploadVideoAndGetId, |
769 | getLocalIdByUUID, | 874 | getLocalIdByUUID, |
770 | getVideoIdFromUUID | 875 | getVideoIdFromUUID, |
876 | prepareResumableUpload | ||
877 | } | ||
878 | |||
879 | // --------------------------------------------------------------------------- | ||
880 | |||
881 | function buildUploadReq (req: request.Test, attributes: VideoAttributes) { | ||
882 | |||
883 | for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) { | ||
884 | if (attributes[key] !== undefined) { | ||
885 | req.field(key, attributes[key]) | ||
886 | } | ||
887 | } | ||
888 | |||
889 | for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) { | ||
890 | if (attributes[key] !== undefined) { | ||
891 | req.field(key, JSON.stringify(attributes[key])) | ||
892 | } | ||
893 | } | ||
894 | |||
895 | for (const key of [ 'language', 'privacy', 'category', 'licence' ]) { | ||
896 | if (attributes[key] !== undefined) { | ||
897 | req.field(key, attributes[key].toString()) | ||
898 | } | ||
899 | } | ||
900 | |||
901 | const tags = attributes.tags || [] | ||
902 | for (let i = 0; i < tags.length; i++) { | ||
903 | req.field('tags[' + i + ']', attributes.tags[i]) | ||
904 | } | ||
905 | |||
906 | for (const key of [ 'thumbnailfile', 'previewfile' ]) { | ||
907 | if (attributes[key] !== undefined) { | ||
908 | req.attach(key, buildAbsoluteFixturePath(attributes[key])) | ||
909 | } | ||
910 | } | ||
911 | |||
912 | if (attributes.scheduleUpdate) { | ||
913 | if (attributes.scheduleUpdate.updateAt) { | ||
914 | req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt) | ||
915 | } | ||
916 | |||
917 | if (attributes.scheduleUpdate.privacy) { | ||
918 | req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy) | ||
919 | } | ||
920 | } | ||
771 | } | 921 | } |