Clip Web videos.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

91 lines
2.2 KiB

  1. import {
  2. createVideoClipper,
  3. YouTube,
  4. VideoType,
  5. ClipVideoParams,
  6. } from '@modal/webvideo-clip-core';
  7. import { constants } from 'http2';
  8. import { RouteHandlerMethod } from 'fastify';
  9. export type ClipArgs = {
  10. url?: unknown,
  11. start?: string | number,
  12. end?: string | number,
  13. }
  14. const DURATION_STRING_REGEXP = /^\d\d:[0-5]\d:[0-5]\d(\.\d+)?$/;
  15. const validateRequestBody = (body: ClipArgs) => {
  16. const messages = [] as string[];
  17. const { url, start, end } = body;
  18. if (typeof url !== 'string') {
  19. messages.push('URL is required.');
  20. }
  21. const typeofStart = typeof start;
  22. if (typeofStart !== 'undefined') {
  23. if (
  24. !['string', 'number'].includes(typeofStart)
  25. || (typeofStart === 'string' && !DURATION_STRING_REGEXP.test(start as string))
  26. ) {
  27. messages.push('Invalid start value.');
  28. }
  29. }
  30. const typeofEnd = typeof end;
  31. if (typeofEnd !== 'undefined') {
  32. if (
  33. !['string', 'number'].includes(typeofEnd)
  34. || (typeofEnd === 'string' && !DURATION_STRING_REGEXP.test(end as string))
  35. ) {
  36. messages.push('Invalid end value.');
  37. }
  38. }
  39. return messages;
  40. };
  41. const getVideoType = (url: string) => {
  42. if (url.startsWith('https://www.youtube.com')) {
  43. return YouTube.VIDEO_TYPE as VideoType;
  44. }
  45. return null;
  46. };
  47. export const clip: RouteHandlerMethod = async (request, reply) => {
  48. const validationMessages = validateRequestBody(request.body as ClipArgs);
  49. if (validationMessages.length > 0) {
  50. reply
  51. .status(constants.HTTP_STATUS_BAD_REQUEST)
  52. .send({
  53. errors: validationMessages,
  54. });
  55. return;
  56. }
  57. const videoType = getVideoType((request.body as ClipArgs).url as string);
  58. if (videoType === null) {
  59. reply
  60. .status(constants.HTTP_STATUS_UNPROCESSABLE_ENTITY)
  61. .send({
  62. errors: ['Unsupported URL.'],
  63. });
  64. }
  65. const clipper = createVideoClipper({
  66. type: videoType as VideoType,
  67. downloaderExecutablePath: process.env.YOUTUBE_DOWNLOADER_EXECUTABLE_PATH as string,
  68. });
  69. const { url, start, end } = request.body as ClipVideoParams;
  70. const clipResult = await clipper({
  71. url,
  72. start,
  73. end,
  74. });
  75. reply
  76. .header('Content-Type', clipResult.contentType)
  77. .send(clipResult.content);
  78. };