UNPKG

n8n-nodes-youtube-audio

Version:

Download audio from YouTube videos in your n8n workflows

343 lines 16.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.YoutubeAudioNode = void 0; const n8n_workflow_1 = require("n8n-workflow"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const ytdl_core_1 = __importDefault(require("@distube/ytdl-core")); const fs_1 = require("fs"); const ffmpeg = require('fluent-ffmpeg'); const ffmpegPath = require('ffmpeg-static'); if (ffmpegPath) { ffmpeg.setFfmpegPath(ffmpegPath); } class YoutubeAudioNode { constructor() { this.description = { displayName: 'Youtube Audio', name: 'youtubeAudioNode', icon: 'file:youTube.png', group: ['transform'], version: 1, description: 'Download audio from YouTube videos', defaults: { name: 'Youtube Audio', }, inputs: ['main'], outputs: ['main'], properties: [ { displayName: 'Youtube Video ID or URL', name: 'youtubeUrl', type: 'string', default: '', placeholder: 'https://www.youtube.com/watch?v=xxxxxxxx', description: 'The URL or ID of the YouTube video to download audio from', required: true, }, { displayName: 'Output Format', name: 'outputFormat', type: 'options', options: [ { name: 'MP3', value: 'mp3', }, { name: 'WAV', value: 'wav', }, ], default: 'mp3', description: 'The format of the output audio file', }, { displayName: 'Quality', name: 'quality', type: 'options', options: [ { name: 'Highest Audio', value: 'highestaudio', }, { name: 'Lowest Audio', value: 'lowestaudio', }, ], default: 'highestaudio', description: 'The quality of the audio to download', }, { displayName: 'Use Proxy', name: 'useProxy', type: 'boolean', default: false, description: 'Whether to use a proxy for the download', }, { displayName: 'Proxy URL', name: 'proxyUrl', type: 'string', default: '', placeholder: 'http://username:password@host:port', description: 'The URL of the proxy to use', displayOptions: { show: { useProxy: [true], }, }, }, { displayName: 'Proxy Type', name: 'proxyType', type: 'options', options: [ { name: 'HTTP/HTTPS', value: 'http', }, { name: 'SOCKS', value: 'socks', }, ], default: 'http', description: 'The type of proxy to use', displayOptions: { show: { useProxy: [true], }, }, }, { displayName: 'Binary Property', name: 'binaryPropertyName', type: 'string', default: 'data', description: 'The name of the binary property to which to write the data of the read file', }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; let item; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { item = items[itemIndex]; const youtubeUrl = this.getNodeParameter('youtubeUrl', itemIndex); const outputFormat = this.getNodeParameter('outputFormat', itemIndex); const quality = this.getNodeParameter('quality', itemIndex); const useProxy = this.getNodeParameter('useProxy', itemIndex); const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex); let proxyType = ''; if (useProxy) { const proxyUrl = this.getNodeParameter('proxyUrl', itemIndex); proxyType = this.getNodeParameter('proxyType', itemIndex); console.log(`Proxy will be used: ${proxyUrl} (${proxyType})`); } let videoId = youtubeUrl; if (youtubeUrl.includes('youtube.com') || youtubeUrl.includes('youtu.be')) { console.log('Extracting video ID from URL:', youtubeUrl); try { const url = new URL(youtubeUrl); if (url.hostname === 'youtu.be') { videoId = url.pathname.slice(1); } else { const v = url.searchParams.get('v'); if (!v) { throw new n8n_workflow_1.ApplicationError(`The provided URL doesn't contain a valid YouTube video identifier. URL: ${youtubeUrl}`); } videoId = v; } } catch (error) { throw new n8n_workflow_1.ApplicationError(`Invalid YouTube URL: ${youtubeUrl}`); } } console.log(`Downloading audio for video ID: ${videoId} with quality: ${quality}`); console.log('Getting video info...'); const videoInfo = await ytdl_core_1.default.getInfo(videoId); if (useProxy) { console.log('Proxy will be used for downloading...'); } const videoTitle = videoInfo.videoDetails.title; const cleanTitle = videoTitle.replace(/[\\/:*?"<>|]/g, '_'); console.log(`Video title: ${videoTitle}`); const tempDir = os.tmpdir(); const tempFilePath = path.join(tempDir, `${cleanTitle}_temp.mp4`); const outputFilePath = path.join(tempDir, `${cleanTitle}.${outputFormat}`); console.log(`Temporary file: ${tempFilePath}`); console.log(`Output file: ${outputFilePath}`); const options = { quality, filter: 'audioonly', highWaterMark: 1 << 25, }; return new Promise(async (resolve, reject) => { try { console.log('Starting download process...'); if (useProxy) { process.env.HTTP_PROXY = proxyType === 'http' ? this.getNodeParameter('proxyUrl', itemIndex) : ''; process.env.HTTPS_PROXY = proxyType === 'http' ? this.getNodeParameter('proxyUrl', itemIndex) : ''; process.env.SOCKS_PROXY = proxyType === 'socks' ? this.getNodeParameter('proxyUrl', itemIndex) : ''; console.log('Using environment proxy settings for download'); } const audioStream = (0, ytdl_core_1.default)(videoId, options); const writeStream = (0, fs_1.createWriteStream)(tempFilePath); let downloadedBytes = 0; const startTime = Date.now(); audioStream.on('progress', (_, downloaded, total) => { downloadedBytes = downloaded; if (total) { const percent = ((downloaded / total) * 100).toFixed(2); const downloadedMB = (downloaded / 1024 / 1024).toFixed(2); const totalMB = (total / 1024 / 1024).toFixed(2); console.log(`Progress: ${percent}% (${downloadedMB}MB / ${totalMB}MB)`); } }); audioStream.on('info', (info, format) => { console.log(`Selected format: ${format.audioBitrate || '?'}kbps ${format.audioCodec || 'unknown codec'}`); }); audioStream.on('error', (error) => { console.error('Download error:', error); reject(error); }); writeStream.on('error', (error) => { console.error('Write error:', error); reject(error); }); audioStream.pipe(writeStream); writeStream.on('finish', async () => { const elapsedTime = (Date.now() - startTime) / 1000; const totalMB = (downloadedBytes / 1024 / 1024).toFixed(2); const averageSpeed = (parseFloat(totalMB) / elapsedTime).toFixed(2); console.log(`Download complete in ${elapsedTime.toFixed(2)}s`); console.log(`File size: ${totalMB}MB, Avg. Speed: ${averageSpeed} MB/s`); console.log(`Converting to ${outputFormat}...`); try { await new Promise((resolveConversion, rejectConversion) => { ffmpeg(tempFilePath) .output(outputFilePath) .audioCodec(outputFormat === 'mp3' ? 'libmp3lame' : 'pcm_s16le') .audioBitrate(outputFormat === 'mp3' ? '192k' : '1411k') .on('end', () => { console.log('Conversion finished'); resolveConversion(); }) .on('error', (err) => { console.error('Conversion error:', err); rejectConversion(err); }) .run(); }); const fileContent = await fs.promises.readFile(outputFilePath); const fileSize = fileContent.length; console.log(`File read successfully, size: ${(fileSize / 1024 / 1024).toFixed(2)}MB`); const newItem = { json: { ...item.json, videoId, title: videoTitle, fileName: `${cleanTitle}.${outputFormat}`, fileSize, format: outputFormat, durationSeconds: parseInt(videoInfo.videoDetails.lengthSeconds, 10), }, binary: {}, pairedItem: { item: itemIndex, }, }; newItem.binary[binaryPropertyName] = await this.helpers.prepareBinaryData(fileContent, `${cleanTitle}.${outputFormat}`, `audio/${outputFormat}`); returnData.push(newItem); try { await fs.promises.unlink(tempFilePath); await fs.promises.unlink(outputFilePath); if (useProxy) { process.env.HTTP_PROXY = ''; process.env.HTTPS_PROXY = ''; process.env.SOCKS_PROXY = ''; } console.log('Temporary files cleaned up'); } catch (cleanupError) { console.error('Error cleaning up temp files:', cleanupError); } resolve([returnData]); } catch (conversionError) { console.error('Error during conversion:', conversionError); reject(new Error(`Error converting audio: ${conversionError.message}`)); } }); } catch (downloadError) { console.error('Download process error:', downloadError); reject(downloadError); } }); } catch (error) { console.error('Node execution error:', error); if (this.continueOnFail()) { returnData.push({ json: { error: error.message, }, pairedItem: { item: itemIndex, }, }); continue; } throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex, }); } } return [returnData]; } } exports.YoutubeAudioNode = YoutubeAudioNode; //# sourceMappingURL=YoutubeAudioNode.node.js.map