n8n-nodes-youtube-audio
Version:
Download audio from YouTube videos in your n8n workflows
343 lines • 16.5 kB
JavaScript
;
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