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.

115 lines
3.0 KiB

  1. import {
  2. createVideoClipper,
  3. VideoType,
  4. CreateVideoClipperParams,
  5. } from '@modal/webvideo-clip-core';
  6. import { constants } from 'http2';
  7. import { RouteHandlerMethod } from 'fastify';
  8. export type ClipArgs = {
  9. url?: unknown,
  10. start?: string | number,
  11. end?: string | number,
  12. }
  13. const DURATION_STRING_REGEXP = /^\d\d:[0-5]\d:[0-5]\d(\.\d+)?$/;
  14. const validateRequestBody = (body: ClipArgs) => {
  15. const messages = [] as string[];
  16. const { url, start, end } = body;
  17. if (typeof url !== 'string') {
  18. messages.push('URL is required.');
  19. }
  20. const typeofStart = typeof start;
  21. if (typeofStart !== 'undefined') {
  22. if (!['string', 'number'].includes(typeofStart)) {
  23. messages.push('Invalid end value.');
  24. } else if (typeofStart === 'string' && !DURATION_STRING_REGEXP.test(start as string)) {
  25. messages.push('Invalid start value.');
  26. }
  27. }
  28. const typeofEnd = typeof end;
  29. if (typeofEnd !== 'undefined') {
  30. if (!['string', 'number'].includes(typeofEnd)) {
  31. messages.push('Invalid end value.');
  32. } else if (typeofEnd === 'string' && !DURATION_STRING_REGEXP.test(end as string)) {
  33. messages.push('Invalid end value.');
  34. }
  35. }
  36. return messages;
  37. };
  38. const getVideoType = (url: string) => {
  39. if (url.startsWith('https://www.youtube.com')) {
  40. return VideoType.YOUTUBE;
  41. }
  42. return null;
  43. };
  44. export const clip: RouteHandlerMethod = async (request, reply) => {
  45. const validationMessages = validateRequestBody(request.body as ClipArgs);
  46. if (validationMessages.length > 0) {
  47. reply
  48. .status(constants.HTTP_STATUS_BAD_REQUEST)
  49. .send({
  50. errors: validationMessages,
  51. });
  52. return;
  53. }
  54. const videoType = getVideoType((request.body as ClipArgs).url as string);
  55. if (videoType === null) {
  56. reply
  57. .status(constants.HTTP_STATUS_UNPROCESSABLE_ENTITY)
  58. .send({
  59. message: 'Unsupported URL.',
  60. });
  61. }
  62. const { url, start, end } = request.body as ClipArgs;
  63. const videoClipperArgs = {
  64. type: videoType,
  65. url,
  66. start,
  67. end,
  68. downloaderExecutablePath: process.env.YOUTUBE_DOWNLOADER_EXECUTABLE_PATH,
  69. } as CreateVideoClipperParams;
  70. const clipper = createVideoClipper(videoClipperArgs);
  71. clipper.on('process', (arg: Record<string, unknown>) => {
  72. request.server.log.info(`${arg.type as string}:${arg.phase as string}`);
  73. if (typeof arg.command === 'string') {
  74. request.server.log.debug(`> ${arg.command}`);
  75. }
  76. });
  77. let clipResult: Record<string, unknown>;
  78. clipper.on('success', (result: Record<string, unknown>) => {
  79. clipResult = result;
  80. });
  81. let theError: Error;
  82. clipper.on('error', (error: Error) => {
  83. theError = error;
  84. });
  85. clipper.on('end', () => {
  86. if (theError) {
  87. reply
  88. .status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR)
  89. .send({
  90. message: theError.message,
  91. });
  92. return;
  93. }
  94. reply
  95. .header('Content-Type', clipResult.type as string)
  96. .send(clipResult.output as Buffer);
  97. });
  98. clipper.process();
  99. };