|
- import { EventEmitter } from 'events';
- import { spawnSync } from 'child_process';
- import { readFileSync, unlinkSync } from 'fs';
- import { CreateBaseClipperParams, FILE_TYPES, VideoClipEventEmitter } from '../common';
- import { convertDurationToSeconds, convertSecondsToDuration } from '../duration';
-
- const normalizeStartSeconds = (start?: number | string) => {
- if (typeof start === 'undefined') {
- return 0;
- }
- if (typeof start === 'string') {
- return convertDurationToSeconds(start);
- }
-
- return start;
- };
-
- const FORMAT_DEFAULT_ARGS = [
- '--youtube-skip-dash-manifest',
- '-f', 'bestvideo+bestaudio',
- ];
-
- const getFileExtension = (downloaderExecutablePath: string, url: string) => {
- const result = spawnSync(
- downloaderExecutablePath,
- [
- ...FORMAT_DEFAULT_ARGS,
- '--print', 'filename',
- '-o', '%(ext)s',
- url,
- ],
- );
-
- if (result.error) {
- throw result.error;
- }
-
- return result.stdout.toString('utf-8');
- };
-
- const constructDownloadSectionsRegex = (start?: number | string, end?: number | string) => {
- const startSeconds = normalizeStartSeconds(start);
- if (typeof end !== 'undefined') {
- const endSeconds = (
- typeof end === 'string'
- ? convertDurationToSeconds(end)
- : end
- );
-
- return `*${convertSecondsToDuration(startSeconds)}-${convertSecondsToDuration(endSeconds)}`;
- }
-
- if (startSeconds > 0) {
- return `*${convertSecondsToDuration(startSeconds)}-inf`;
- }
-
- return null;
- };
-
- const constructDefaultDownloadArgs = (
- outputFilename: string,
- url: string,
- start?: number | string,
- end?: number | string,
- ) => {
- const defaultDownloadArgs = [
- ...FORMAT_DEFAULT_ARGS,
- '-o', outputFilename,
- ];
-
- const downloadSectionsRegex = constructDownloadSectionsRegex(start, end);
- if (typeof downloadSectionsRegex === 'string') {
- return [
- ...defaultDownloadArgs,
- '--force-keyframes-at-cuts',
- '--download-sections', downloadSectionsRegex,
- url,
- ];
- }
-
- return [
- ...defaultDownloadArgs,
- url,
- ];
- };
-
- export interface CreateYouTubeClipperParams extends CreateBaseClipperParams {
- downloaderExecutablePath?: string;
- }
-
- export class YouTubeVideoClipEventEmitter extends EventEmitter implements VideoClipEventEmitter {
- constructor(private readonly params: CreateYouTubeClipperParams) {
- super();
- }
-
- process() {
- if (!this.params.downloaderExecutablePath) {
- this.emit('error', new Error('Downloader not found.'));
- this.emit('end');
- return;
- }
-
- const fileExtension = getFileExtension(
- this.params.downloaderExecutablePath,
- this.params.url,
- );
- const cacheFilename = `output.${fileExtension}`; // todo label this on the cache
- const downloadArgs = constructDefaultDownloadArgs(
- cacheFilename,
- this.params.url,
- this.params.start,
- this.params.end,
- );
-
- this.emit('process', {
- type: 'download',
- phase: 'start',
- command: `${this.params.downloaderExecutablePath} ${downloadArgs.join(' ')}`,
- });
- const downloaderProcess = spawnSync(
- this.params.downloaderExecutablePath,
- downloadArgs,
- );
- if (downloaderProcess.error) {
- this.emit('error', downloaderProcess.error);
- this.emit('end');
- return;
- }
- this.emit('process', {
- type: 'download',
- phase: 'success',
- });
-
- const output = readFileSync(cacheFilename);
- unlinkSync(cacheFilename);
- this.emit('success', {
- type: FILE_TYPES[fileExtension],
- output,
- });
- this.emit('end');
- }
- }
|